Skip to content

Commit

Permalink
Merge pull request #71 from chingu-voyages/feature/issue-45-admin-aut…
Browse files Browse the repository at this point in the history
…hentication

Feature/issue 45 admin authentication
  • Loading branch information
EslemOuederni authored Nov 26, 2024
2 parents ee3faac + d7fae1c commit b764878
Show file tree
Hide file tree
Showing 22 changed files with 8,530 additions and 3,478 deletions.
7 changes: 7 additions & 0 deletions suncityla/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100,
"tabWidth": 2,
"semi": true
}
File renamed without changes.
18 changes: 18 additions & 0 deletions suncityla/app/admin/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { authOptions } from "@/lib/auth";
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";

export const dynamic = "force-dynamic";

export default async function AdminsPage() {
const session = await getServerSession(authOptions);
if (!session || !session.user) {
redirect("/admin/signin");
}
return (
<div>
<h1>Admins Dashboard</h1>
<h2>Welcome back {session.user.username}</h2>
</div>
);
}
18 changes: 18 additions & 0 deletions suncityla/app/admin/register/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import SignUpForm from "@/app/components/AdminForm/SignUpForm";
import Footer from "@/app/components/Footer";
// import getAdmins from "./action";

export const dynamic = "force-dynamic";

export default async function AdminsRegisterPage() {
// const admins = await getAdmins();
return (
<>
<div className=" flex flex-col justify-center items-center mt-6">
<h1 className=" text-2xl font-semibold">Admin Registration</h1>
<SignUpForm />
</div>
<Footer />
</>
);
}
10 changes: 10 additions & 0 deletions suncityla/app/admin/signin/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import SignInForm from "@/app/components/AdminForm/SignInForm";

export default function AdminsSignInPage() {
return (
<div className="flex flex-col justify-center items-center mt-6">
<h1 className="text-2xl font-semibold">Admin Sign In</h1>
<SignInForm />
</div>
)
}
13 changes: 0 additions & 13 deletions suncityla/app/admins/page.tsx

This file was deleted.

6 changes: 6 additions & 0 deletions suncityla/app/api/auth/[...nextauth]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { authOptions } from "@/lib/auth"
import NextAuth from "next-auth"

const handler = NextAuth(authOptions)

export { handler as GET, handler as POST }
37 changes: 37 additions & 0 deletions suncityla/app/api/auth/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import prisma from "@/prisma/prismaClient";
import { NextResponse } from "next/server";
import {hash} from "bcrypt";

export async function POST(req: Request){
try {
const {username,password} = await req.json();
// verify if the user already exists
const alreadyExists = await prisma.admin.findUnique({
where: {
username : username
}
});
if(alreadyExists){
return NextResponse.json({admin:null,message: "User already exists"},{status:400});
}

// hash the password
const hashedPassword = await hash(password,10);
// create a new admin
const admin = await prisma.admin.create({
data: {
username,
password : hashedPassword
}
});

// return the admin without the password
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const {password: adminPassword,...adminWithoutPassword} = admin;
return NextResponse.json({admin: adminWithoutPassword,message: "Admin created successfully"},{status:201});

} catch (error) {
return NextResponse.json({admin:null,message: error},{status:500});
}
}

78 changes: 78 additions & 0 deletions suncityla/app/components/AdminForm/SignInForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"use client"
import { useRouter } from "next/navigation"
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import * as z from "zod";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { signIn } from "next-auth/react"
import { useState } from "react";

//validation schema with Zod
const AdminSchema = z
.object({
username: z.string().min(1, "Username is required"),
password: z
.string()
.min(1, "Password is required")
.min(6, "Password must be at least 6 characters long"),
});

type AdminFormData = z.infer<typeof AdminSchema>;

export default function SignInForm() {
const router = useRouter()
const [authError, setAuthError] = useState<string | null>(null);

const { register, handleSubmit, formState : {errors} } = useForm<AdminFormData>({
resolver: zodResolver(AdminSchema),
defaultValues: {
username: "",
password: "",
}
})

const onSubmit = async (data: AdminFormData) => {
setAuthError(null)
const signInData = await signIn("credentials", {
username: data.username,
password: data.password,
redirect: false,
})

if(signInData?.error === "CredentialsSignin") {
setAuthError("Invalid username or password")
}else{
router.push('/admin')
router.refresh() //refresh the page after successful login
}
}

return (
<form onSubmit={handleSubmit(onSubmit)} className="regForm">
<div>
<label htmlFor="username">Username</label>
<Input
{...register("username")}
id="username"
placeholder="Enter your username"
className="mt-1 bg-white"
/>
{errors.username && <p className="text-red-600">{errors.username.message}</p>}
</div>
<div>
<label htmlFor="password">Password</label>
<Input
{...register("password")}
id="password"
type="password"
placeholder="Enter your password"
className="mt-1 bg-white"
/>
{errors.password && <p className="text-red-600">{errors.password.message}</p>}
{authError && <p className="text-red-600">{authError}</p>}
</div>
<Button type="submit" variant="default">Sign In</Button>
</form>
)
}
105 changes: 105 additions & 0 deletions suncityla/app/components/AdminForm/SignUpForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
'use client';
import { useRouter } from 'next/navigation';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import Link from 'next/link';

// Define validation schema with Zod
const AdminRegistrationSchema = z
.object({
username: z.string().min(1, 'Username is required'),
password: z
.string()
.min(1, 'Password is required')
.min(6, 'Password must be at least 6 characters long'),
confirmPassword: z.string().min(1, 'Confirm Password is required'),
})
.refine((data) => data.password === data.confirmPassword, {
path: ['confirmPassword'],
message: 'Password do not match',
});

type AdminRegistrationFormData = z.infer<typeof AdminRegistrationSchema>;

export default function SignUpForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<AdminRegistrationFormData>({
resolver: zodResolver(AdminRegistrationSchema),
defaultValues: {
username: '',
password: '',
confirmPassword: '',
},
});
const router = useRouter();

const onSubmit = async (data: AdminRegistrationFormData) => {
const response = await fetch('/api/auth', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
if (response.ok) {
router.push('/admin/signin');
} else {
alert('An error occurred while registering');
}
};

return (
<form onSubmit={handleSubmit(onSubmit)} className="regForm">
<div>
<label htmlFor="username">Username</label>
<Input
{...register('username')}
id="username"
placeholder="Enter your username"
className="mt-1 bg-white"
/>
{errors.username && <p className="text-red-500">{errors.username.message}</p>}
</div>
<div>
<label htmlFor="password">Password</label>
<Input
{...register('password')}
id="password"
type="password"
placeholder="Enter your password"
className="mt-1 bg-white"
/>
{errors.password && <p className="text-red-600">{errors.password.message}</p>}
</div>
<div>
<label htmlFor="confirmPassword">Confirm Password</label>
<Input
{...register('confirmPassword')}
id="confirmPassword"
type="password"
placeholder="Confirm your password"
className="mt-1 bg-white"
/>
{errors.confirmPassword && <p className="text-red-600">{errors.confirmPassword.message}</p>}
</div>
<Button type="submit" variant="default">
Register
</Button>
<div>
<h2 className="text-center">
Do you already have an account?{' '}
<Link href="/admin/signin" className=" font-semibold text-white">
{' '}
please Login
</Link>
</h2>
</div>
</form>
);
}
2 changes: 1 addition & 1 deletion suncityla/app/components/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ export default function Footer() {
</Link>
</li>
<li>
<Link href="/admins" className="text-blue-500 hover:underline">
<Link href="/admin" className="text-blue-500 hover:underline">
Admin Login
</Link>
</li>
Expand Down
29 changes: 7 additions & 22 deletions suncityla/app/components/Navbar.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,9 @@
import { Button } from "@/components/ui/button";
import Link from "next/link";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import NavbarToggle from "./NavbarToggle";

export default function Navbar() {
return (
<nav className="flex flex-row justify-between items-center mx-9 py-9">
<div>
<Link href="/" className="text-4xl font-semibold">
SunCityLA
</Link>
</div>
<div className="flex flex-row justify-end items-center gap-6 font-semibold">
<Link href="/">Home</Link>
<Link href="/about">About</Link>
<Link href="/bookings">Schedule an Evaluation</Link>
<Link href="/admins">Admin Login</Link>
<Link href="/Support">Support</Link>
<Button>
<Link href="/bookings/new">New booking</Link>
</Button>
</div>
</nav>
);
export default async function Navbar() {
const session = await getServerSession(authOptions);

return <NavbarToggle session={session} />;
}
Loading

0 comments on commit b764878

Please sign in to comment.