add: full authentication with NextAuth

This commit is contained in:
Elie Baier 2023-12-18 14:59:34 +01:00
parent c48f1e23a3
commit 226100f25a
12 changed files with 273 additions and 130 deletions

34
actions/grades.ts Normal file
View File

@ -0,0 +1,34 @@
"use server";
import prisma from "@/lib/prisma";
export async function getGrade(testId: number, userId: number) {
const grade = await prisma.grade.findFirst({
select: {
grade: true,
id: true,
testId: true,
note: true,
createdAt: true,
},
where: {
testId: testId,
userId: userId
}
});
return grade;
}
export async function addGrade(testId: number, userId: number, grade: number, note: string | null) {
const newGrade = await prisma.grade.create({
data: {
grade: grade,
note: note,
testId: testId,
userId: userId,
}
});
return newGrade;
}

View File

@ -10,3 +10,65 @@ export async function setTestActive(id: number, active: boolean) {
return true; return true;
} }
export async function getActiveTest(date: Date) {
return await prisma.test.findFirst({
select: {
id: true,
isActive: true,
isPassed: true,
testOf: {
select: {
id: true,
firstName: true,
lastName: true,
isTeacher: true,
},
},
createdAt: true,
testOn: true,
},
where: {
isActive: true,
isPassed: false,
testOn: new Date(date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + (date.getDate().toString().length === 1 ? '0' + date.getDate() : date.getDate())),
},
});
}
export async function getActiveTestWithGrade(date: Date, userId: number) {
return await prisma.test.findFirst({
select: {
id: true,
isActive: true,
isPassed: true,
testOf: {
select: {
id: true,
firstName: true,
lastName: true,
isTeacher: true,
},
},
grades: {
select: {
id: true,
grade: true,
note: true,
createdAt: true,
},
take: 1,
where: {
userId: userId
}
},
createdAt: true,
testOn: true,
},
where: {
isActive: true,
isPassed: false,
testOn: new Date(date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + (date.getDate().toString().length === 1 ? '0' + date.getDate() : date.getDate())),
},
});
}

View File

@ -1,49 +1,20 @@
'use client';
import { Poppins } from 'next/font/google';
import { Check } from 'lucide-react';
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import logo from '../images/logo.svg'; import logo from '../images/logo.svg';
import Image from 'next/image'; import Image from 'next/image';
import { Input } from '@components/ui/input'; import { LoginForm } from '@/components/forms/LoginForm';
import { white } from 'next/dist/lib/picocolors'; import { getAuthServerSession } from '@/lib/authenticate';
import { signIn } from 'next-auth/react'; import { redirect } from 'next/navigation';
export default function Home() { export default async function Home() {
const [password, setPassword] = useState(''); const session = await getAuthServerSession();
const router = useRouter();
useEffect(() => { if (session != null) {
if (localStorage.getItem('@password')) { redirect('/play');
router.push('/play');
}
}, [router]);
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
// localStorage.setItem('@password', password);
// router.push('/play');
console.log('Trying to sign in');
const result = await signIn('credentials', {
key: password,
callbackUrl: '/play',
});
} }
return ( return (
<div className={'w-full h-screen text-[#F0F0F0] bg-black p-12 flex flex-col items-center justify-center gap-y-28'}> <div className={'w-full h-screen text-[#F0F0F0] bg-black p-12 flex flex-col items-center justify-center gap-y-28'}>
<Image src={logo} alt={'Logo'} width={100} height={200} className={'mx-auto w-full md:w-[400px]'} /> <Image src={logo} alt={'Logo'} width={100} height={200} className={'mx-auto w-full md:w-[400px]'} />
<form onSubmit={handleSubmit} className={'flex flex-row gap-4 w-full md:w-[400px]'}> <LoginForm />
<Input type="password" placeholder="Mot de passe" id="password" name="password" value={password} onChange={(e) => setPassword(e.target.value)}></Input>
<input type="submit" value="Submit" id="submit" className={'hidden'} />
<label htmlFor="submit" className={'w-[54px] h-[54px] bg-secondary rounded-[20px] contents-none grid place-content-center'}>
<Check width={24} height={24} color={'white'} />
</label>
</form>
</div> </div>
); );
} }

47
app/play/TodayTest.tsx Normal file
View File

@ -0,0 +1,47 @@
'use client';
import { getActiveTestWithGrade } from '@/actions/mangeTest';
import { TestCard } from '@/components/custom';
import { Session } from 'next-auth';
import { signOut } from 'next-auth/react';
import { useEffect, useState } from 'react';
import Image from 'next/image';
import LogOut from '@images/logout.svg';
import { GradingForm } from '@/components/forms/GradingForm';
import YourGrade from '@images/your-grade.svg';
export function TodayTest({ session }: { session: Session }) {
const [activeTest, setActiveTest] = useState<{ data: any | null; error: Error | null; isLoading: boolean }>({ isLoading: true, data: null, error: null });
useEffect(() => {
getActiveTestWithGrade(new Date(), session.user.id)
.catch((err) => setActiveTest({ data: null, error: err, isLoading: false }))
.then((data) => setActiveTest({ data, error: null, isLoading: false }));
}, [session.user.id]);
return (
<>
<TestCard data={activeTest.data} error={activeTest.error} isLoading={activeTest.isLoading} />
{activeTest.data &&
(activeTest.data.grades[0] ? (
<div className={'w-full md:w-[400px]'}>
<div className={'flex flex-row justify-evenly'}>
<Image src={YourGrade} alt={'Your Grade'} width={100} />
<span className={'w-[54px] h-[54px] bg-accent rounded-[20px] contents-none grid place-content-center'}>{activeTest.data.grades[0].grade}</span>
</div>
</div>
) : (
<GradingForm session={session} testId={activeTest.data.id} />
))}
<div>
<button
onClick={async () => {
await signOut();
}}
>
<Image src={LogOut} alt={'Logo'} width={50} height={50} className={'mx-auto w-full md:w-[400px]'} />
</button>
</div>
</>
);
}

12
app/play/layout.tsx Normal file
View File

@ -0,0 +1,12 @@
import { getAuthServerSession } from '@/lib/authenticate';
import { redirect } from 'next/navigation';
export default async function Layout({ children }: { children: React.ReactNode }) {
const session = await getAuthServerSession();
if (session == null) {
redirect('/');
}
return <>{children}</>;
}

View File

@ -1,43 +1,35 @@
'use client';
import { GradingForm, TestCard } from '@/components/custom';
import { useRouter } from 'next/navigation';
import { useState, useEffect } from 'react';
import useSWR from 'swr';
import { fetcher } from '@/lib/fetcher';
import Image from 'next/image'; import Image from 'next/image';
import Logo from '/images/logo.svg'; import Logo from '/images/logo.svg';
import LogOut from '@images/logout.svg'; import { getAuthServerSession } from '@/lib/authenticate';
import { redirect } from 'next/navigation';
import { getActiveTest } from '@/actions/mangeTest';
import { TodayTest } from './TodayTest';
export default function Play() { export default async function Play() {
const router = useRouter(); const session = await getAuthServerSession();
const [password, setPassword] = useState<string | null>('');
const { data, error, isLoading } = useSWR('/api/test?key=' + password, fetcher); if (session == null) {
redirect('/');
}
// useEffect(() => { const now = new Date();
// const pass = localStorage.getItem('@password'); const todayTest = await getActiveTest(now);
// setPassword(pass);
// if (!pass) {
// router.push('/');
// }
// }, [router]);
return ( return (
<div className={'w-full h-[100vh] text-[#F0F0F0] bg-black p-12 flex flex-col items-center justify-center gap-y-28'}> <div className={'w-full h-[100vh] text-[#F0F0F0] bg-black p-12 flex flex-col items-center justify-center gap-y-28'}>
<Image src={Logo} alt={'Logo'} width={100} height={200} className={'mx-auto w-full md:w-[400px]'} /> <Image src={Logo} alt={'Logo'} width={100} height={200} className={'mx-auto w-full md:w-[400px]'} />
<TestCard data={data} error={error} isLoading={isLoading} /> <TodayTest session={session} />
{data && data.status == 200 && password && <GradingForm password={password} data={data} />} {/* <TestCard data={data} error={error} isLoading={isLoading} /> */}
<div> {/* {data && data.status == 200 && password && <GradingForm password={password} data={data} />} */}
{/* <div>
<button <button
onClick={() => { onClick={async () => {
localStorage.clear(); await signOut();
router.push('/');
}} }}
> >
<Image src={LogOut} alt={'Logo'} width={50} height={50} className={'mx-auto w-full md:w-[400px]'} /> <Image src={LogOut} alt={'Logo'} width={50} height={50} className={'mx-auto w-full md:w-[400px]'} />
</button> </button>
</div> </div> */}
</div> </div>
); );
} }

View File

@ -1,64 +0,0 @@
'use client';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import Image from 'next/image';
import YourGrade from '@images/your-grade.svg';
import {Check} from "lucide-react";
export function GradingForm({ password, data }: { password: string; data: any }) {
const [grade, setGrade] = useState<number>(0);
const [hasVoted, setHasVoted] = useState<boolean>(false);
const router = useRouter();
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
const response = await fetch('/api/grade', {
method: 'POST',
body: JSON.stringify({ grade, key: password, testId: data.id }),
});
const json = await response.json();
if (response.status == 200) {
setHasVoted(true);
} else {
alert(json.message);
}
}
if (data.vote.hasVoted || hasVoted) {
return (
<div className={"w-full md:w-[400px]"}>
<div className={"flex flex-row justify-evenly"}>
<Image src={YourGrade} alt={YourGrade} width={100}/>
<span className={"w-[54px] h-[54px] bg-accent rounded-[20px] contents-none grid place-content-center"}>{data.vote.hasVoted ? data.vote.grade : grade}</span>
</div>
</div>
);
}
return (
<div className={"w-full md:w-[400px]"}>
<form onSubmit={handleSubmit}>
<div>
<input
type="range"
min="2"
max="12"
id="gradeSelector"
className={"w-full h-4 bg-white range range:bg-white text-white"}
onChange={(e) => setGrade(Number(e.target.value) / 2)}
/>
</div>
<div className={"flex flex-row justify-evenly"}>
<Image src={YourGrade} alt={YourGrade} width={100}/>
<span className={"w-[54px] h-[54px] bg-accent rounded-[20px] contents-none grid place-content-center"}>{grade}</span>
<input type="submit" value={'Envoyer'} id="submit" className={"hidden"}></input>
<label htmlFor="submit" className={"w-[54px] h-[54px] bg-secondary rounded-[20px] contents-none grid place-content-center"}>
<Check width={24} height={24} color={"white"}/>
</label>
</div>
</form>
</div>
);
}

View File

@ -1,4 +1,3 @@
export { DayCard as DayCard } from './dayCard'; export { DayCard as DayCard } from './dayCard';
export { TestCard as TestCard } from './testCard'; export { TestCard as TestCard } from './testCard';
export { GradingForm } from "./gradingForm";
export { Santa } from "./santa"; export { Santa } from "./santa";

View File

@ -6,7 +6,6 @@ import Image from 'next/image';
import { Ban, Snowflake, CircleOff } from 'lucide-react'; import { Ban, Snowflake, CircleOff } from 'lucide-react';
export function TestCard({ data, error, isLoading }: { data: any; error: any; isLoading: boolean }) { export function TestCard({ data, error, isLoading }: { data: any; error: any; isLoading: boolean }) {
//console.log(data);
if (isLoading) if (isLoading)
return ( return (
<div className={'w-fit h-fit relative'}> <div className={'w-fit h-fit relative'}>
@ -27,7 +26,7 @@ export function TestCard({ data, error, isLoading }: { data: any; error: any; is
</div> </div>
</div> </div>
); );
if (data.status == 404) if (!data)
return ( return (
<div className={'w-fit h-fit relative'}> <div className={'w-fit h-fit relative'}>
<Image src={Calendar} alt={'Calendrier absolute'} /> <Image src={Calendar} alt={'Calendrier absolute'} />

View File

@ -0,0 +1,55 @@
'use client';
import { useState } from 'react';
import Image from 'next/image';
import YourGrade from '@images/your-grade.svg';
import { Check } from 'lucide-react';
import { Session } from 'next-auth';
import { addGrade } from '@/actions/grades';
export function GradingForm({ session, testId }: { session: Session; testId: number }) {
const [grade, setGrade] = useState<number>(0);
const [hasVoted, setHasVoted] = useState<boolean>(false);
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
const result = await addGrade(testId, session.user.id, grade, null);
if (result.id) {
console.log('ok');
setHasVoted(true);
setGrade(result.grade);
} else {
console.log('error');
}
}
if (hasVoted && grade != 0) {
return (
<div className={'w-full md:w-[400px]'}>
<div className={'flex flex-row justify-evenly'}>
<Image src={YourGrade} alt={YourGrade} width={100} />
<span className={'w-[54px] h-[54px] bg-accent rounded-[20px] contents-none grid place-content-center'}>{grade}</span>
</div>
</div>
);
}
return (
<div className={'w-full md:w-[400px]'}>
<form onSubmit={handleSubmit}>
<div>
<input type="range" min="2" max="12" id="gradeSelector" className={'w-full h-4 bg-white range range:bg-white text-white'} onChange={(e) => setGrade(Number(e.target.value) / 2)} />
</div>
<div className={'flex flex-row justify-evenly'}>
<Image src={YourGrade} alt={YourGrade} width={100} />
<span className={'w-[54px] h-[54px] bg-accent rounded-[20px] contents-none grid place-content-center'}>{grade}</span>
<input type="submit" value={'Envoyer'} id="submit" className={'hidden'}></input>
<label htmlFor="submit" className={'w-[54px] h-[54px] bg-secondary rounded-[20px] contents-none grid place-content-center'}>
<Check width={24} height={24} color={'white'} />
</label>
</div>
</form>
</div>
);
}

View File

@ -0,0 +1,30 @@
'use client';
import { signIn } from 'next-auth/react';
import { useState } from 'react';
import { Input } from '../ui/input';
import { Check } from 'lucide-react';
export function LoginForm() {
const [password, setPassword] = useState('');
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
console.log('Trying to sign in');
await signIn('credentials', {
key: password,
callbackUrl: '/play',
});
}
return (
<form onSubmit={handleSubmit} className={'flex flex-row gap-4 w-full md:w-[400px]'}>
<Input type="password" placeholder="Mot de passe" id="password" name="password" value={password} onChange={(e) => setPassword(e.target.value)}></Input>
<input type="submit" value="Submit" id="submit" className={'hidden'} />
<label htmlFor="submit" className={'w-[54px] h-[54px] bg-secondary rounded-[20px] contents-none grid place-content-center'}>
<Check width={24} height={24} color={'white'} />
</label>
</form>
);
}

View File

@ -1,6 +1,7 @@
import Credentials from "next-auth/providers/credentials"; import Credentials from "next-auth/providers/credentials";
import prisma from "./prisma"; import prisma from "./prisma";
import { getServerSession, RequestInternal, type NextAuthOptions, User } from "next-auth"; import { getServerSession, RequestInternal, type NextAuthOptions, User } from "next-auth";
import type { GetServerSidePropsContext, NextApiRequest, NextApiResponse } from "next"
export async function authenticate(key: string) { export async function authenticate(key: string) {
console.log("Running authenticate function with key: " + key); console.log("Running authenticate function with key: " + key);
@ -72,4 +73,9 @@ export const authOptions: NextAuthOptions = {
], ],
}; };
// Use it in server contexts
export function auth(...args: [GetServerSidePropsContext["req"], GetServerSidePropsContext["res"]] | [NextApiRequest, NextApiResponse] | []) {
return getServerSession(...args, authOptions)
}
export const getAuthServerSession = () => getServerSession(authOptions); export const getAuthServerSession = () => getServerSession(authOptions);