Feat: Add user login / logout

This commit is contained in:
globuzma 2024-11-19 00:03:05 +01:00
parent 40c451b2c0
commit ac3cf943f2
32 changed files with 7315 additions and 0 deletions

View file

@ -0,0 +1,48 @@
'use client'
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { useEffect } from "react"
import { useRouter } from 'next/navigation'
import { setCookie, getCookie } from "@/lib/utils"
export default function Signin() {
const router = useRouter()
useEffect(() => {
if(getCookie('JWT') == "") {
router.refresh()
router.push('/')
}
},[])
function disconnect(){
setCookie('JWT','',-1)
// INFO : We need to hard refresh for the NavigationBar to be updated
const base_url = window.location.origin
window.location.replace(base_url)
}
return (
<>
<div className="flex flex-col items-center mt-24" >
<h1 className="text-3xl mb-4">Mon compte</h1>
<Card className="w-full max-w-xs">
<CardHeader>
<CardTitle>Profile</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<Button className="w-auto" disabled={true}>Changer mon mot de passe</Button>
<Button onClick={disconnect} className="w-auto">Deconnexion</Button>
</CardContent>
</Card>
</div>
</>
);
}

View file

@ -0,0 +1,63 @@
'use client'
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
import { useRef, useEffect } from "react"
import { useRouter } from 'next/navigation'
import { setCookie, getCookie } from "@/lib/utils"
export default function Signin() {
const router = useRouter()
const usernameField = useRef<HTMLInputElement>(null)
const passwordField = useRef<HTMLInputElement>(null)
useEffect(() => {
if(getCookie('JWT') != "") {
router.refresh()
router.push('/')
}
}, [])
async function signIn() {
const data = {
"email": usernameField.current!.value,
"password": passwordField.current!.value
}
console.log(usernameField.current!.value + " - " + passwordField.current!.value)
const response = await fetch("http://localhost:3000/api/auth/signin",
{
method: "post",
body: JSON.stringify(data)
})
if(response.status == 200) {
const responseData = await response.json()
setCookie('JWT', responseData["JWT"], 7)
const base_url = window.location.origin
window.location.replace(base_url)
}
}
return (
<>
<div className="flex flex-col items-center mt-24" >
<Card className="w-full max-w-xs">
<CardHeader>
<CardTitle>Connectez vous</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<Input ref={usernameField} placeholder="Nom d'utilisateur·rice" />
<Input ref={passwordField} placeholder="Mot de passe" type="password"/>
<Button onClick={signIn} className="w-auto">Connexion</Button>
</CardContent>
</Card>
</div>
</>
);
}

View file

@ -0,0 +1,44 @@
import { NextResponse, NextRequest } from 'next/server'
import { createToken } from '@/lib/jwt'
import { createHmac } from "crypto"
import { db } from '@/lib/db'
const secret = process.env.PASSWORD_SECRET ? process.env.PASSWORD_SECRET : ""
export async function POST(req: NextRequest) {
try {
const { email, password } = await req.json()
const user = await db.utilisateurice.findFirst({
where: {
email
}
})
if (user !== undefined){
if(createHmac('sha256',secret).update(password).digest('hex') == user!.password) {
const token = createToken({data: {username: user!.username, admin: user!.admin}, maxAge: 60*60*24*7})
return NextResponse.json({"JWT": token},{
status: 200,
});
} else {
return NextResponse.json({error: "Wrong credentials"},{
status: 401,
});
}
} else {
return NextResponse.json({error: "Wrong credentials"},{
status: 401,
});
}
} catch (error) {
console.log(error)
return NextResponse.json(
{ error: "Failed, check console" },
{
status: 500,
}
);
}
}

View file

@ -0,0 +1,32 @@
import { NextResponse, NextRequest } from 'next/server'
import { validateToken } from '@/lib/jwt'
export async function POST(req: NextRequest) {
try {
const token = req?.headers.get("authorization")?.split(" ")[1]
if(token !== undefined) {
if(validateToken(token)) {
return NextResponse.json({"message": "Your token is valid!"},{
status: 200,
});
} else {
return NextResponse.json({"message": "Your token is not valid."},{
status: 401,
});
}
} else {
return NextResponse.json({"message": "You did not provide a token."},{
status: 401,
});
}
} catch (error) {
console.log(error)
return NextResponse.json(
{ error: "Failed, check console" },
{
status: 500,
}
);
}
}

BIN
app/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

BIN
app/app/fonts/GeistVF.woff Normal file

Binary file not shown.

78
app/app/globals.css Normal file
View file

@ -0,0 +1,78 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
font-family: Arial, Helvetica, sans-serif;
}
@layer utilities {
.text-balance {
text-wrap: balance;
}
}
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 240 10% 3.9%;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--radius: 0.5rem;
}
.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

48
app/app/layout.tsx Normal file
View file

@ -0,0 +1,48 @@
import type { Metadata } from "next";
import localFont from "next/font/local";
import "./globals.css";
import { cookies } from 'next/headers'
import { NavigationBar } from "@/components/ui/navigation-bar"
import { decryptToken } from '@/lib/jwt'
const geistSans = localFont({
src: "./fonts/GeistVF.woff",
variable: "--font-geist-sans",
weight: "100 900",
});
const geistMono = localFont({
src: "./fonts/GeistMonoVF.woff",
variable: "--font-geist-mono",
weight: "100 900",
});
export const metadata: Metadata = {
title: "Brawl Set",
description: "Un mode de jeu MTG 100% rennais",
};
export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const cookieStore = await cookies()
const JWT = cookieStore.get("JWT")
let username = ""
if(JWT !== undefined){
username = decryptToken(JWT.value)?.username
}
return (
<html lang="fr">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<NavigationBar username={username} isLoggedIn={JWT !== undefined} />
{children}
</body>
</html>
);
}

9
app/app/page.tsx Normal file
View file

@ -0,0 +1,9 @@
export default function Home() {
return (
<>
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<h1>Main Page !</h1>
</div>
</>
);
}