Feat: Add user login / logout
This commit is contained in:
parent
40c451b2c0
commit
ac3cf943f2
32 changed files with 7315 additions and 0 deletions
48
app/app/account/profile/page.tsx
Normal file
48
app/app/account/profile/page.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
63
app/app/account/signin/page.tsx
Normal file
63
app/app/account/signin/page.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
44
app/app/api/auth/signin/route.ts
Normal file
44
app/app/api/auth/signin/route.ts
Normal 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,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
32
app/app/api/auth/validateToken/route.ts
Normal file
32
app/app/api/auth/validateToken/route.ts
Normal 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
BIN
app/app/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
BIN
app/app/fonts/GeistMonoVF.woff
Normal file
BIN
app/app/fonts/GeistMonoVF.woff
Normal file
Binary file not shown.
BIN
app/app/fonts/GeistVF.woff
Normal file
BIN
app/app/fonts/GeistVF.woff
Normal file
Binary file not shown.
78
app/app/globals.css
Normal file
78
app/app/globals.css
Normal 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
48
app/app/layout.tsx
Normal 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
9
app/app/page.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue