Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
name: CI/CD Pipeline

on:
pull_request:
branches:
- main

jobs:
lint-and-build:
name: Lint and Build
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Node.js (Use Required Version)
uses: actions/setup-node@v4
with:
node-version: 20.18.0 # Set to the required version

- name: Install dependencies
run: npm install

- name: Set up environment variables
run: |
echo "NEXTAUTH_SECRET=${{ secrets.NEXTAUTH_SECRET }}" >> .env
echo "NEXTAUTH_URL=${{ secrets.NEXTAUTH_URL }}" >> .env
echo "NEXT_PUBLIC_APP_URL=${{ secrets.NEXT_PUBLIC_APP_URL }}" >> .env
echo "RESEND_API_KEY=${{ secrets.RESEND_API_KEY }}" >> .env
echo "DATABASE_URL=${{ secrets.DATABASE_URL }}" >> .env
echo "DATABASE_URL_UNPOOLED=${{ secrets.DATABASE_URL_UNPOOLED }}" >> .env
echo "PGHOST=${{ secrets.PGHOST }}" >> .env
echo "PGHOST_UNPOOLED=${{ secrets.PGHOST_UNPOOLED }}" >> .env
echo "PGUSER=${{ secrets.PGUSER }}" >> .env
echo "PGDATABASE=${{ secrets.PGDATABASE }}" >> .env
echo "PGPASSWORD=${{ secrets.PGPASSWORD }}" >> .env
echo "POSTGRES_URL=${{ secrets.POSTGRES_URL }}" >> .env
echo "POSTGRES_URL_NON_POOLING=${{ secrets.POSTGRES_URL_NON_POOLING }}" >> .env
echo "POSTGRES_USER=${{ secrets.POSTGRES_USER }}" >> .env
echo "POSTGRES_HOST=${{ secrets.POSTGRES_HOST }}" >> .env
echo "POSTGRES_PASSWORD=${{ secrets.POSTGRES_PASSWORD }}" >> .env
echo "POSTGRES_DATABASE=${{ secrets.POSTGRES_DATABASE }}" >> .env
echo "POSTGRES_URL_NO_SSL=${{ secrets.POSTGRES_URL_NO_SSL }}" >> .env
echo "POSTGRES_PRISMA_URL=${{ secrets.POSTGRES_PRISMA_URL }}" >> .env
echo "GOOGLE_CLIENT_ID=${{ secrets.GOOGLE_CLIENT_ID }}" >> .env
echo "GOOGLE_CLIENT_SECRET=${{ secrets.GOOGLE_CLIENT_SECRET }}" >> .env
echo "GITHUB_ID=${{ secrets.GH_ID }}" >> .env
echo "GITHUB_SECRET=${{ secrets.GH_SECRET }}" >> .env

- name: Debug File Listing
run: |
echo "Checking for matching files..."
find . -type f \( -name "*.js" -o -name "*.ts" -o -name "*.jsx" -o -name "*.tsx" -o -name "*.json" -o -name "*.md" -o -name "*.css" -o -name "*.scss" \) \
! -path "./node_modules/*" ! -path "./dist/*" ! -path "./.git/*" ! -path "./coverage/*" \
|| echo "No matching files found."

- name: Run Biome Formatter & Linter
run: |
FILES=$(find . -type f \( -name "*.js" -o -name "*.ts" -o -name "*.jsx" -o -name "*.tsx" -o -name "*.json" -o -name "*.md" -o -name "*.css" -o -name "*.scss" \) \
! -path "./node_modules/*" ! -path "./dist/*" ! -path "./.git/*" ! -path "./coverage/*")

if [ -n "$FILES" ]; then
echo "Running Biome on found files..."
echo "$FILES" | xargs npx biome format --write
echo "$FILES" | xargs npx biome lint --write
else
echo "No files found to format or lint. Skipping."
fi

- name: Run Build
run: npm run build
57 changes: 29 additions & 28 deletions app/admin/page.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,34 @@
import { authOptions } from "@/lib/auth.config"
import { getServerSession } from "next-auth/next"
import { redirect } from "next/navigation"
import { authOptions } from "@/lib/auth.config";
import { getServerSession } from "next-auth/next";
import { redirect } from "next/navigation";

export default async function AdminDashboard() {
const session = await getServerSession(authOptions)
const session = await getServerSession(authOptions);

if (!session || session.user.role !== "ADMIN") {
redirect("/unauthorized")
}
if (!session || session.user.role !== "ADMIN") {
redirect("/unauthorized");
}

return (
<div className="min-h-screen bg-gray-100 py-6 flex flex-col justify-center sm:py-12">
<div className="relative py-3 sm:max-w-xl sm:mx-auto">
<div className="absolute inset-0 bg-gradient-to-r from-purple-400 to-pink-500 shadow-lg transform -skew-y-6 sm:skew-y-0 sm:-rotate-6 sm:rounded-3xl"></div>
<div className="relative px-4 py-10 bg-white shadow-lg sm:rounded-3xl sm:p-20">
<div className="max-w-md mx-auto">
<div>
<h1 className="text-2xl font-semibold">Welcome to the Admin Dashboard!</h1>
</div>
<div className="divide-y divide-gray-200">
<div className="py-8 text-base leading-6 space-y-4 text-gray-700 sm:text-lg sm:leading-7">
<p>You are signed in as an admin: {session.user.email}</p>
<p>Your role is: {session.user.role}</p>
</div>
</div>
</div>
</div>
</div>
</div>
)
return (
<div className="min-h-screen bg-gray-100 py-6 flex flex-col justify-center sm:py-12">
<div className="relative py-3 sm:max-w-xl sm:mx-auto">
<div className="absolute inset-0 bg-gradient-to-r from-purple-400 to-pink-500 shadow-lg transform -skew-y-6 sm:skew-y-0 sm:-rotate-6 sm:rounded-3xl" />
<div className="relative px-4 py-10 bg-white shadow-lg sm:rounded-3xl sm:p-20">
<div className="max-w-md mx-auto">
<div>
<h1 className="text-2xl font-semibold">
Welcome to the Admin Dashboard!
</h1>
</div>
<div className="divide-y divide-gray-200">
<div className="py-8 text-base leading-6 space-y-4 text-gray-700 sm:text-lg sm:leading-7">
<p>You are signed in as an admin: {session.user.email}</p>
<p>Your role is: {session.user.role}</p>
</div>
</div>
</div>
</div>
</div>
</div>
);
}

174 changes: 97 additions & 77 deletions app/auth/forgot-password/page.tsx
Original file line number Diff line number Diff line change
@@ -1,89 +1,109 @@
"use client"
"use client";

import { useState } from "react"
import { motion } from "framer-motion"
import Image from "next/image"
import { useRouter } from "next/navigation"
import { useState } from "react";
import { motion } from "framer-motion";
import Image from "next/image";
import { useRouter } from "next/navigation";

export default function ForgotPassword() {
const [email, setEmail] = useState("")
const [error, setError] = useState("")
const [success, setSuccess] = useState("")
const [isLoading, setIsLoading] = useState(false)
const router = useRouter()
const [email, setEmail] = useState("");
const [error, setError] = useState("");
const [success, setSuccess] = useState("");
const [isLoading, setIsLoading] = useState(false);
const router = useRouter();

const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
setError("")
setSuccess("")
setIsLoading(true)
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setError("");
setSuccess("");
setIsLoading(true);

try {
const response = await fetch("/api/auth/forgot-password", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email }),
})
try {
const response = await fetch("/api/auth/forgot-password", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email }),
});

if (response.ok) {
setSuccess("Password reset link sent to your email.")
} else {
const data = await response.json()
setError(data.message || "Failed to send reset link.")
}
} catch (error) {
setError(`An error occurred. Please try again: ${error instanceof Error ? error.message : String(error)}`)
} finally {
setIsLoading(false)
}
}
if (response.ok) {
setSuccess("Password reset link sent to your email.");
} else {
const data = await response.json();
setError(data.message || "Failed to send reset link.");
}
} catch (error) {
setError(
`An error occurred. Please try again: ${error instanceof Error ? error.message : String(error)}`,
);
} finally {
setIsLoading(false);
}
};

return (
<div className="min-h-screen flex items-center justify-center bg-[#dffce8] py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-6 bg-white p-8 rounded-lg shadow-lg">
<div className="flex flex-col items-center">
<Image src="/logo.svg" alt="Boundless" width={180} height={180} className="mb-4" />
<h2 className="mt-2 text-center text-3xl font-extrabold text-[#194247]">Forgot Password</h2>
<p className="mt-2 text-center text-sm text-gray-600">Enter your email to receive a password reset link.</p>
</div>
return (
<div className="min-h-screen flex items-center justify-center bg-[#dffce8] py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-6 bg-white p-8 rounded-lg shadow-lg">
<div className="flex flex-col items-center">
<Image
src="/logo.svg"
alt="Boundless"
width={180}
height={180}
className="mb-4"
/>
<h2 className="mt-2 text-center text-3xl font-extrabold text-[#194247]">
Forgot Password
</h2>
<p className="mt-2 text-center text-sm text-gray-600">
Enter your email to receive a password reset link.
</p>
</div>

<form onSubmit={handleSubmit} className="mt-8 space-y-6">
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700">Email address</label>
<input
id="email"
name="email"
type="email"
autoComplete="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-[#194247] focus:border-[#194247]"
/>
</div>
<form onSubmit={handleSubmit} className="mt-8 space-y-6">
<div>
<label
htmlFor="email"
className="block text-sm font-medium text-gray-700"
>
Email address
</label>
<input
id="email"
name="email"
type="email"
autoComplete="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-[#194247] focus:border-[#194247]"
/>
</div>

{error && <p className="text-red-500 text-sm text-center">{error}</p>}
{success && <p className="text-green-500 text-sm text-center">{success}</p>}
{error && <p className="text-red-500 text-sm text-center">{error}</p>}
{success && (
<p className="text-green-500 text-sm text-center">{success}</p>
)}

<motion.button
whileTap={{ scale: 0.95 }}
type="submit"
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-[#194247] hover:bg-[#153a3f] focus:outline-none"
disabled={isLoading}
>
{isLoading ? "Sending..." : "Send Reset Link"}
</motion.button>
</form>
<motion.button
whileTap={{ scale: 0.95 }}
type="submit"
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-[#194247] hover:bg-[#153a3f] focus:outline-none"
disabled={isLoading}
>
{isLoading ? "Sending..." : "Send Reset Link"}
</motion.button>
</form>

<div className="text-center">
<button
onClick={() => router.push("/auth/signin")}
className="text-sm text-[#194247] hover:underline"
>
Back to Sign In
</button>
</div>
</div>
</div>
)
<div className="text-center">
<button
type="button"
onClick={() => router.push("/auth/signin")}
className="text-sm text-[#194247] hover:underline"
>
Back to Sign In
</button>
</div>
</div>
</div>
);
}
Loading