Merge pull request #2 from NabilOuldHamou/features/auth

Features/auth
This commit is contained in:
Nabil Ould Hamou 2024-12-01 15:39:19 +01:00 committed by GitHub
commit 7cc52ddb85
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 272 additions and 24 deletions

View file

@ -1,14 +1,14 @@
services:
mongodb:
image: mongo:latest
build: ./mongodb_rs
hostname: mongodb
restart: always
env_file:
- .env
environment:
- MONGO_INITDB_ROOT_USERNAME=temp-root-username
- MONGO_INITDB_ROOT_PASSWORD=temp-password
- MONGO_INITDB_DATABASE=chat_projetweb
- MONGO_REPLICA_HOST=localhost
- MONGO_REPLICA_PORT=27017
ports:
- "27017:27017"
volumes:

11
mongodb_rs/Dockerfile Normal file
View file

@ -0,0 +1,11 @@
FROM mongo:5
# we take over the default & start mongo in replica set mode in a background task
ENTRYPOINT mongod --port $MONGO_REPLICA_PORT --replSet rs0 --bind_ip 0.0.0.0 & MONGOD_PID=$!; \
# we prepare the replica set with a single node and prepare the root user config
INIT_REPL_CMD="rs.initiate({ _id: 'rs0', members: [{ _id: 0, host: '$MONGO_REPLICA_HOST:$MONGO_REPLICA_PORT' }] })"; \
INIT_USER_CMD="db.getUser('$MONGO_INITDB_ROOT_USERNAME') || db.createUser({ user: '$MONGO_INITDB_ROOT_USERNAME', pwd: '$MONGO_INITDB_ROOT_PASSWORD', roles: ['root'] })"; \
# we wait for the replica set to be ready and then submit the command just above
until (mongo admin --port $MONGO_REPLICA_PORT --eval "$INIT_REPL_CMD && $INIT_USER_CMD"); do sleep 1; done; \
# we are done but we keep the container by waiting on signals from the mongo task
echo "REPLICA SET ONLINE"; wait $MONGOD_PID;

View file

@ -38,6 +38,7 @@
"dependencies": {
"@prisma/client": "^5.22.0",
"@types/node": "^22.10.1",
"argon2": "^0.41.1",
"prisma": "^5.22.0",
"redis": "^4.7.0",
"svelte-radix": "^2.0.1"

31
pnpm-lock.yaml generated
View file

@ -14,6 +14,9 @@ importers:
'@types/node':
specifier: ^22.10.1
version: 22.10.1
argon2:
specifier: ^0.41.1
version: 0.41.1
prisma:
specifier: ^5.22.0
version: 5.22.0
@ -344,6 +347,10 @@ packages:
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'}
'@phc/format@1.0.0':
resolution: {integrity: sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==}
engines: {node: '>=10'}
'@pkgjs/parseargs@0.11.0':
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
@ -647,6 +654,10 @@ packages:
arg@5.0.2:
resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
argon2@0.41.1:
resolution: {integrity: sha512-dqCW8kJXke8Ik+McUcMDltrbuAWETPyU6iq+4AhxqKphWi7pChB/Zgd/Tp/o8xRLbg8ksMj46F/vph9wnxpTzQ==}
engines: {node: '>=16.17.0'}
argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
@ -1141,6 +1152,14 @@ packages:
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
node-addon-api@8.3.0:
resolution: {integrity: sha512-8VOpLHFrOQlAH+qA0ZzuGRlALRA6/LVh8QJldbrC4DY0hXoMP0l4Acq8TzFC018HztWiRqyCEj2aTWY2UvnJUg==}
engines: {node: ^18 || ^20 || >= 21}
node-gyp-build@4.8.4:
resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
hasBin: true
node-releases@2.0.18:
resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==}
@ -1844,6 +1863,8 @@ snapshots:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.17.1
'@phc/format@1.0.0': {}
'@pkgjs/parseargs@0.11.0':
optional: true
@ -2133,6 +2154,12 @@ snapshots:
arg@5.0.2: {}
argon2@0.41.1:
dependencies:
'@phc/format': 1.0.0
node-addon-api: 8.3.0
node-gyp-build: 4.8.4
argparse@2.0.1: {}
aria-query@5.3.2: {}
@ -2619,6 +2646,10 @@ snapshots:
natural-compare@1.4.0: {}
node-addon-api@8.3.0: {}
node-gyp-build@4.8.4: {}
node-releases@2.0.18: {}
normalize-path@3.0.0: {}

View file

@ -14,7 +14,7 @@ datasource db {
}
model User {
id String @id @default(cuid()) @map("_id") @db.ObjectId
id String @id @default(auto()) @map("_id") @db.ObjectId
username String @unique
surname String
name String
@ -28,7 +28,7 @@ model User {
}
model Channel {
id String @id @default(cuid()) @map("_id") @db.ObjectId
id String @id @default(auto()) @map("_id") @db.ObjectId
name String
topic String
users User[] @relation(fields: [userIDs], references: [id])
@ -39,7 +39,7 @@ model Channel {
}
model Message {
id String @id @default(cuid()) @map("_id") @db.ObjectId
id String @id @default(auto()) @map("_id") @db.ObjectId
user User @relation(fields: [userId], references: [id])
userId String @db.ObjectId
channel Channel @relation(fields: [channelId], references: [id])

View file

@ -0,0 +1,18 @@
import { Tabs as TabsPrimitive } from "bits-ui";
import Content from "./tabs-content.svelte";
import List from "./tabs-list.svelte";
import Trigger from "./tabs-trigger.svelte";
const Root = TabsPrimitive.Root;
export {
Root,
Content,
List,
Trigger,
//
Root as Tabs,
Content as TabsContent,
List as TabsList,
Trigger as TabsTrigger,
};

View file

@ -0,0 +1,21 @@
<script lang="ts">
import { Tabs as TabsPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = TabsPrimitive.ContentProps;
let className: $$Props["class"] = undefined;
export let value: $$Props["value"];
export { className as class };
</script>
<TabsPrimitive.Content
class={cn(
"ring-offset-background focus-visible:ring-ring mt-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2",
className
)}
{value}
{...$$restProps}
>
<slot />
</TabsPrimitive.Content>

View file

@ -0,0 +1,19 @@
<script lang="ts">
import { Tabs as TabsPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = TabsPrimitive.ListProps;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<TabsPrimitive.List
class={cn(
"bg-muted text-muted-foreground inline-flex h-10 items-center justify-center rounded-md p-1",
className
)}
{...$$restProps}
>
<slot />
</TabsPrimitive.List>

View file

@ -0,0 +1,23 @@
<script lang="ts">
import { Tabs as TabsPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = TabsPrimitive.TriggerProps;
type $$Events = TabsPrimitive.TriggerEvents;
let className: $$Props["class"] = undefined;
export let value: $$Props["value"];
export { className as class };
</script>
<TabsPrimitive.Trigger
class={cn(
"ring-offset-background focus-visible:ring-ring data-[state=active]:bg-background data-[state=active]:text-foreground inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm",
className
)}
{value}
{...$$restProps}
on:click
>
<slot />
</TabsPrimitive.Trigger>

View file

@ -0,0 +1,80 @@
import { type Actions, json } from '@sveltejs/kit';
import prismaClient from '$lib/prismaClient';
import * as argon2 from 'argon2';
import { redirect, error } from '@sveltejs/kit';
export const actions: Actions = {
login: async ({request}) => {
const formData = await request.formData();
// @ts-ignore Can't be empty
const username = formData.get('username').toString();
// @ts-ignore Can't be empty
const password = formData.get('password').toString();
const user = await prismaClient.user.findFirst({
where: {
username: username,
}
});
if (user == null) {
return error(400, {message: "Nom d'utilisateur ou mot de passe invalide."});
}
try {
// @ts-ignore Already checked for null
if (await argon2.verify(user.password, password)) {
console.log("login succesful")
} else {
return error(400, {message: "Nom d'utilisateur ou mot de passe invalide."});
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
return error(500, {message: "Erreur interne."})
}
},
register: async ({request}) => {
const formData = await request.formData();
// @ts-ignore Can't be empty
const username = formData.get('username').toString();
// @ts-ignore Can't be empty
const email = formData.get('email').toString();
// @ts-ignore Can't be empty
const password = formData.get('password').toString();
const user = await prismaClient.user.findFirst({
where: {
OR: [
{
email: email
},
{
username: username
}
]
}
});
if (user != null) {
return error(400, { message: "Un compte avec cette email ou nom d'utilisateur éxiste déjà." });
}
const hash = await argon2.hash(password);
await prismaClient.user.create({
data: {
email: email,
username: username,
name: "",
surname: "",
password: hash,
}
});
return redirect(302, "/chat");
}
}

View file

@ -1,28 +1,72 @@
<script lang="ts">
import { Label } from "$lib/components/ui/label";
import { Button } from "$lib/components/ui/button";
import { Input } from "$lib/components/ui/input";
import * as Card from "$lib/components/ui/card";
import { Label } from "$lib/components/ui/label";
import { Button } from "$lib/components/ui/button";
import { Input } from "$lib/components/ui/input";
import * as Card from "$lib/components/ui/card";
import { enhance } from '$app/forms';
import * as Tabs from "$lib/components/ui/tabs";
</script>
<div class="w-full h-full flex justify-center items-center">
<Card.Root class="w-[450px]">
<Tabs.Root value="login" class="w-[450px]">
<Tabs.List class="grid w-full grid-cols-2">
<Tabs.Trigger value="login">Se connecter</Tabs.Trigger>
<Tabs.Trigger value="register">S'inscrire</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="login">
<Card.Root>
<Card.Header>
<Card.Title>🌳</Card.Title>
<Card.Description>Un chat collaboratif</Card.Description>
<Card.Title>🌳 - Arbres</Card.Title>
<Card.Description>Connectez vous pour chatter!</Card.Description>
</Card.Header>
<Card.Content>
<div class="grid w-full max-w-sm items-center gap-1.5">
<form method="POST" action="?/login" use:enhance>
<Card.Content>
<div class="grid w-full max-w-sm items-center gap-1.5">
<Label for="username">Nom d'utilisateur</Label>
<Input type="text" id="username" />
</div>
<div class="pt-4 grid w-full max-w-sm items-center gap-1.5">
<Input type="text" name="username" id="username" />
</div>
<div class="pt-4 grid w-full max-w-sm items-center gap-1.5">
<Label for="password">Mot de passe</Label>
<Input type="password" id="password" />
</div>
</Card.Content>
<Card.Footer>
<Button>Se connecter</Button>
</Card.Footer>
<Input type="password" name="password" id="password" />
</div>
</Card.Content>
<Card.Footer>
<Button type="submit">Se connecter</Button>
</Card.Footer>
</form>
</Card.Root>
</Tabs.Content>
<Tabs.Content value="register">
<Card.Root>
<Card.Header>
<Card.Title>🌳 - Arbres</Card.Title>
<Card.Description>Inscrivez-vous pour chatter!</Card.Description>
</Card.Header>
<form method="POST" action="?/register" use:enhance>
<Card.Content>
<div class="grid w-full max-w-sm items-center gap-1.5">
<Label for="username">Nom d'utilisateur</Label>
<Input type="text" name="username" id="username" />
</div>
<div class="pt-4 grid w-full max-w-sm items-center gap-1.5">
<Label for="email">Adresse email</Label>
<Input type="text" name="email" id="email" />
</div>
<div class="pt-4 grid w-full max-w-sm items-center gap-1.5">
<Label for="password">Mot de passe</Label>
<Input type="password" name="password" id="password" />
</div>
</Card.Content>
<Card.Footer>
<Button type="submit">S'inscrire</Button>
</Card.Footer>
</form>
</Card.Root>
</Tabs.Content>
</Tabs.Root>
</div>