Merge pull request #9 from Peer-at-Code/feat/auth

Adding auth v1
This commit is contained in:
Théo 2023-02-27 17:38:48 +01:00 committed by GitHub
commit 0186d46bd5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 272 additions and 28 deletions

View file

@ -1 +1,2 @@
NEXT_PUBLIC_API_URL=API
NEXT_PUBLIC_API_URL=API
NEXT_PUBLIC_SITE_URL=SITE

View file

@ -1,15 +1,84 @@
import UserAuthForm from '@/ui/UserAuthForm';
'use client';
export const metadata = {
title: 'Connexion - Peer-at Code'
import AppLink from '@/ui/AppLink';
import Button from '@/ui/Button';
import Input from '@/ui/Input';
import cookies from 'js-cookie';
import { useRouter } from 'next/navigation';
import { useForm } from 'react-hook-form';
type LoginData = {
pseudo: string;
passwd: string;
};
export default function Page() {
const {
register,
handleSubmit,
formState: { errors },
setError
} = useForm<LoginData>({
defaultValues: {
pseudo: '',
passwd: ''
}
});
const router = useRouter();
async function onSubmit(data: LoginData) {
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/login`, {
method: 'POST',
body: JSON.stringify(data)
});
if (res.ok) {
const token = res.headers.get('Authorization')?.split(' ')[1];
if (token) cookies.set('token', token);
if (cookies.get('token')) router.push('/dashboard');
} else {
setError('passwd', {
type: 'manual',
message: "Nom d'utilisateur ou mot de passe incorrect"
});
}
}
return (
<>
<div className="flex flex-col justify-start space-y-4">
<h2 className="mx-auto text-xl font-bold">Connexion</h2>
<UserAuthForm />
<form
className="flex w-52 flex-col justify-center space-y-4 sm:w-72"
onSubmit={handleSubmit(onSubmit)}
>
<Input
label="Nom d'utilisateur"
type="text"
placeholder="PeerAt"
required
error={errors.pseudo?.message}
{...register('pseudo')}
/>
<Input
label="Mot de passe"
type="password"
placeholder="MotDePasse123"
required
error={errors.passwd?.message}
{...register('passwd')}
/>
<Button type="submit" kind="brand">
Se connecter
</Button>
<p className="flex flex-col items-center text-sm text-muted">
Vous n&apos;avez pas de compte?
<AppLink className="text-brand underline" href={'/sign-up'}>
S&apos;inscrire maintenant
</AppLink>
</p>
</form>
</div>
</>
);

View file

@ -1,15 +1,132 @@
import UserAuthForm from '@/ui/UserAuthForm';
'use client';
export const metadata = {
title: 'Inscription - Peer-at Code'
import AppLink from '@/ui/AppLink';
import Button from '@/ui/Button';
import Input from '@/ui/Input';
import cookies from 'js-cookie';
import { useRouter } from 'next/navigation';
import { useForm } from 'react-hook-form';
type RegisterData = {
pseudo: string;
email: string;
passwd: string;
firstname: string;
lastname: string;
description: string;
sgroup: string;
avatar: string;
};
export default function Page() {
const {
register,
handleSubmit,
formState: { errors },
setError
} = useForm<RegisterData>({
defaultValues: {
pseudo: '',
email: '',
passwd: '',
firstname: '',
lastname: '',
description: '',
sgroup: '',
avatar: ''
}
});
const router = useRouter();
async function onSubmit(data: RegisterData) {
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/register`, {
method: 'POST',
body: JSON.stringify(data)
});
const { username_valid, email_valid } = await res.json();
if (!username_valid || !email_valid) {
if (!username_valid) {
setError('pseudo', {
type: 'manual',
message: "Nom d'utilisateur indisponible"
});
}
if (!email_valid) {
setError('email', {
type: 'manual',
message: 'Email déjà utilisé'
});
}
}
console.log(errors);
console.log(username_valid, email_valid);
if (res.ok) {
const token = res.headers.get('Authorization')?.split(' ')[1];
if (token) cookies.set('token', token);
if (cookies.get('token')) router.push('/dashboard');
}
}
return (
<>
<div className="flex flex-col justify-start space-y-4">
<h2 className="mx-auto text-xl font-bold">Créer un compte</h2>
<UserAuthForm />
<form
className="flex w-52 flex-col justify-center space-y-4 sm:w-72"
onSubmit={handleSubmit(onSubmit)}
>
<Input
label="Adresse e-mail"
type="email"
placeholder="peer-at@exemple.be"
required
error={errors.email?.message}
{...register('email')}
/>
<Input label="Nom" type="lastname" placeholder="Doe" {...register('lastname')} />
<Input label="Prénom" type="firstname" placeholder="John" {...register('firstname')} />
<Input
label="Nom d'utilisateur"
type="text"
placeholder="PeerAt"
required
error={errors.pseudo?.message}
{...register('pseudo')}
/>
<Input
label="Mot de passe"
type="password"
placeholder="MotDePasse123"
required
{...register('passwd', {
minLength: {
value: 4,
message: 'Le mot de passe doit contenir au moins 8 caractères'
}
})}
/>
<Button type="submit" kind="brand">
S&apos;inscrire
</Button>
{/* <p className="items-center text-sm text-gray-400">
En cliquant sur continuer, vous acceptez les{' '}
<AppLink className="text-white underline" href="/privacy-policy" target="_blank">
Politique de confidentialité
</AppLink>
.
</p> */}
<p className="flex flex-col items-center text-sm text-muted">
Vous possédez un compte?
<AppLink className="text-brand underline" href={'/sign-in'}>
Se connecter
</AppLink>
</p>
</form>
</div>
</>
);

View file

@ -2,7 +2,8 @@ import AppLink from '@/ui/AppLink';
import Console from '@/ui/Console';
import Image from 'next/image';
export default function Home() {
export default function Page() {
// TODO: Fix this (image)
return (
<div>
<div className="flex h-screen w-full">

View file

@ -1,9 +1,11 @@
/**
* Un élément de navigation.
* A navigation item.
*
* @typedef {Object} NavItem
* @property {string} name - Le nom de l'élément de navigation.
* @property {string} slug - Le slug de l'élément de navigation.
* @property {boolean} [disabled] - Si l'élément de navigation est désactivé.
*
* @property {string} name - The name of the navigation item.
* @property {string} slug - The slug of the navigation item.
* @property {boolean} [disabled] - Whether the navigation item is disabled.
*/
export type NavItem = {
name: string;
@ -13,7 +15,8 @@ export type NavItem = {
};
/**
* Les éléments de navigation.
* Navigation items.
*
* @type {NavItem[]}
*/
export const navItems: NavItem[] = [

View file

@ -2,22 +2,23 @@ import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
/**
* Permet de créer une classe Tailwind avec clsx et tailwind-merge
* pour éviter d'avoir des conflits de classes.
* Create a Tailwind class with clsx and tailwind-merge to avoid
* class conflicts.
*
* @param inputs - Les classes à ajouter à la classe Tailwind.
* @param inputs - Tailwind classes to merge.
*
* @returns La classe Tailwind.
* @returns A Tailwind class.
*/
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
/**
* Permet de convertir une chaîne de caractères en majuscules.
* Convert a string to title case.
*
* @param string - La chaîne de caractères à convertir.
* @returns La chaîne de caractères convertie.
* @param string - The string to convert.
*
* @returns A title case string.
*/
export function titleCase(string: string) {
return string
@ -26,3 +27,27 @@ export function titleCase(string: string) {
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
}
/**
* Convert a string to a slug.
*
* @param pathname - The pathname to append to the URL.
*
* @returns The full URL.
*/
export const getURL = (pathname?: string) => {
let url =
process.env.NEXT_PUBLIC_SITE_URL ?? // Set this to your site URL in production env.
process.env.NEXT_PUBLIC_VERCEL_URL ?? // Automatically set by Vercel.
'http://localhost:3000/';
// Make sure to include `https://` when not localhost.
url = url.includes('http') ? url : `https://${url}`;
// Make sure to including trailing `/`.
url = url.charAt(url.length - 1) === '/' ? url : `${url}/`;
if (pathname) {
// Add pathname without starting `/`
url += pathname.slice(1);
}
return url;
};

View file

@ -1,19 +1,33 @@
import { type NextRequest, NextResponse } from 'next/server';
import { NextResponse, type NextRequest } from 'next/server';
import { getURL } from './lib/utils';
/**
* Permet de créer un middleware Next.js qui sera exécuté avant chaque requête.
*
* @param req - La requête.
* @param req - La requête Next.js
*/
export async function middleware(req: NextRequest) {
const res = NextResponse.next();
console.log('Res', res);
// on donne accès à l'API depuis n'importe quelle origine
res.headers.set('Access-Control-Allow-Origin', '*');
res.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
const token = req.cookies.get('token')?.value;
if (req.nextUrl.pathname.includes('dashboard') && !token)
return NextResponse.redirect(getURL('/sign-in'));
else if (req.nextUrl.pathname.includes('sign') && token)
return NextResponse.redirect(getURL('/dashboard'));
res.headers.set('Authorization', `Bearer ${token}`);
return res;
}
export const config = {
matcher: [
// On exclut les routes de l'API, les fichiers statiques, les images, les assets, le favicon et le service worker.
// '/((?!api|_next/static|_next/image|assets|favicon|sw.js).*)'
'/((?!api|_next/static|_next/image|assets|favicon|sw.js).*)'
]
};

View file

@ -24,6 +24,7 @@
"axios": "^1.3.4",
"boring-avatars": "^1.7.0",
"clsx": "^1.2.1",
"js-cookie": "^3.0.1",
"next": "13.2.1",
"react": "18.2.0",
"react-dom": "18.2.0",
@ -36,6 +37,7 @@
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.3",
"@types/js-cookie": "^3.0.3",
"@types/node": "18.11.18",
"@types/react": "18.0.27",
"@types/react-dom": "18.0.10",

13
pnpm-lock.yaml generated
View file

@ -2,6 +2,7 @@ lockfileVersion: 5.4
specifiers:
'@tailwindcss/forms': ^0.5.3
'@types/js-cookie': ^3.0.3
'@types/node': 18.11.18
'@types/react': 18.0.27
'@types/react-dom': 18.0.10
@ -15,6 +16,7 @@ specifiers:
eslint-config-next: 13.2.1
eslint-config-prettier: ^8.6.0
eslint-plugin-prettier: ^4.2.1
js-cookie: ^3.0.1
next: 13.2.1
postcss: ^8.4.21
prettier: ^2.8.3
@ -34,6 +36,7 @@ dependencies:
axios: 1.3.4
boring-avatars: 1.7.0
clsx: 1.2.1
js-cookie: 3.0.1
next: 13.2.1_biqbaboplfbrettd7655fr4n2y
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
@ -46,6 +49,7 @@ dependencies:
devDependencies:
'@tailwindcss/forms': 0.5.3_tailwindcss@3.2.7
'@types/js-cookie': 3.0.3
'@types/node': 18.11.18
'@types/react': 18.0.27
'@types/react-dom': 18.0.10
@ -299,6 +303,10 @@ packages:
'@types/unist': 2.0.6
dev: false
/@types/js-cookie/3.0.3:
resolution: {integrity: sha512-Xe7IImK09HP1sv2M/aI+48a20VX+TdRJucfq4vfRVy6nWN8PYPOEnlMRSgxJAgYQIXJVL8dZ4/ilAM7dWNaOww==}
dev: true
/@types/json-schema/7.0.11:
resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==}
dev: true
@ -1862,6 +1870,11 @@ packages:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
dev: true
/js-cookie/3.0.1:
resolution: {integrity: sha512-+0rgsUXZu4ncpPxRL+lNEptWMOWl9etvPHc/koSRp6MPwpRYAhmk0dUG00J4bxVV3r9uUzfo24wW0knS07SKSw==}
engines: {node: '>=12'}
dev: false
/js-sdsl/4.3.0:
resolution: {integrity: sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==}
dev: true

View file

@ -1,7 +1,6 @@
export default function DefaultTags() {
return (
<>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link href="/favicon.ico" rel="shortcut icon" />
</>
);

View file

@ -1,3 +1,3 @@
export default function ErrorMessage({ children }: { children: React.ReactNode }) {
return <p className="text-xs text-orange-500">{children}</p>;
return <p className="text-xs text-warning">{children}</p>;
}