Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions src/lib/server/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { prisma } from '@lucia-auth/adapter-prisma';
import { lucia } from 'lucia';
import { sveltekit } from 'lucia/middleware';

import { db } from './db';

import { dev } from '$app/environment';

export const auth = lucia({
env: dev ? 'DEV' : 'PROD',
middleware: sveltekit(),
adapter: prisma(db, {
user: 'user',
key: 'userKey',
session: 'session',
}),
sessionCookie: {
expires: false,
},
getUserAttributes: (data) => {
return {
username: data.username,
disallowedIngredients: data.disallowedIngredients,
};
},
});

export type Auth = typeof auth;
52 changes: 52 additions & 0 deletions src/routes/login/+page.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { fail, redirect } from '@sveltejs/kit';
import { LuciaError } from 'lucia';

import { auth } from '$lib/server/auth';

import type { Actions, PageServerLoad } from './$types';

export const load = (async () => {
return {};
}) satisfies PageServerLoad;

export const actions = {
login: async ({ request, cookies }) => {
const data = await request.formData();
const username = (data.get('username') ?? '') as string;
const password = (data.get('password') ?? '') as string;

if (username.length < 1 || password.length < 1 || password.length > 255) {
return fail(400, { error: 'Identifiants invalides.' });
}

try {
const key = await auth.useKey('username', username.toLowerCase(), password);
const newSession = await auth.createSession({
userId: key.userId,
attributes: {},
});
const sessionCookie = auth.createSessionCookie(newSession);

cookies.set(sessionCookie.name, sessionCookie.value, {
...sessionCookie.attributes,
path: sessionCookie.attributes.path ?? '/',
});
} catch (e) {
if (
e instanceof LuciaError &&
(e.message === 'AUTH_INVALID_KEY_ID' || e.message === 'AUTH_INVALID_PASSWORD')
) {
return fail(400, { error: 'Identifiants invalides.' });
}

// eslint-disable-next-line no-console
console.error('Error logging in:', e);

return fail(500, {
error: "Oups... Quelque chose s'est mal passé. Veuillez réessayer plus tard.",
});
}

redirect(303, '/');
},
} satisfies Actions;
10 changes: 9 additions & 1 deletion src/routes/login/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
<script lang="ts">
import Card from '$lib/components/Card.svelte';

import type { ActionData } from './$types';

export let form: ActionData;
</script>

<section class="section">
<Card title="Se connecter">
<form method="POST" class="form">
<form method="POST" action="?/login" class="form">
<div>
<label for="username">Nom d'utilisateur</label>
<input type="username" name="username" id="username" required={true} />
Expand All @@ -20,6 +24,10 @@
/>
</div>

{#if form?.error}
<p class="text-sm font-light text-red-600">{form.error}</p>
{/if}

<button class="btn" type="submit">Se connecter</button>
<p class="text-sm font-light text-gray-500">
Pas encore de compte ?
Expand Down
47 changes: 47 additions & 0 deletions src/routes/recipes/[slug]/+page.server.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { db } from '$lib/server/db';
import { jsonValueToArray } from '$lib/utils/json';

import type { PageServerLoad } from './$types';
Expand All @@ -15,3 +16,49 @@ export const load = (async ({ parent }) => {
isFavourite: false,
};
}) satisfies PageServerLoad;

export const actions = {
favourite: async ({ params }) => {
// Get favourite
const favourite = await db.favourite.findFirst({
where: {
recipe: {
slug: params.slug,
},
userId: '1',
},
include: {
recipe: true,
},
});

// Remove favourite
if (favourite) {
await db.favourite.delete({
where: {
id: favourite.id,
},
});

return {};
}

// Add favourite
await db.favourite.create({
data: {
user: {
connect: {
id: '1',
},
},
recipe: {
connect: {
slug: params.slug,
},
},
},
});

return {};
},
};
4 changes: 2 additions & 2 deletions src/routes/recipes/[slug]/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@
</script>

<div class="flex gap-2 items-center justify-center">
<form method="POST" class="relative">
<h1 class="h1 first-letter:capitalize">
<form method="POST" action="?/favourite" class="relative">
<h1 class="h1 first-letter:capitalize" style="view-transition-name: {data.recipe.slug};">
{data.recipe.dish}
</h1>
<button
Expand Down
77 changes: 77 additions & 0 deletions src/routes/register/+page.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';
import { fail, redirect } from '@sveltejs/kit';
import { LuciaError } from 'lucia';

import { auth } from '$lib/server/auth';

import type { Actions, PageServerLoad } from './$types';

export const load = (async () => {
return {};
}) satisfies PageServerLoad;

export const actions = {
register: async ({ request, cookies }) => {
const data = await request.formData();
const username = (data.get('username') ?? '') as string;
const password = (data.get('password') ?? '') as string;
const passwordRepeat = (data.get('password-repeat') ?? '') as string;

if (username.length < 3) {
return fail(422, {
error: "Votre nom d'utilisateur est trop court. Il doit faire au moins 3 caractères.",
});
}

if (password.length < 8) {
return fail(422, {
error: 'Votre mot de passe est trop court. Il doit faire au moins 8 caractères.',
});
}

if (password !== passwordRepeat) {
return fail(422, { error: 'Les mots de passe ne correspondent pas.' });
}

try {
const user = await auth.createUser({
key: {
providerId: 'username',
providerUserId: username.toLowerCase(),
password,
},
attributes: {
username,
disallowedIngredients: null,
},
});
const newSession = await auth.createSession({
userId: user.userId,
attributes: {},
});
const sessionCookie = auth.createSessionCookie(newSession);

cookies.set(sessionCookie.name, sessionCookie.value, {
...sessionCookie.attributes,
path: sessionCookie.attributes.path ?? '/',
});
} catch (e) {
if (e instanceof PrismaClientKnownRequestError && e.code === 'P2002') {
return fail(400, { error: "Ce nom d'utilisateur est déjà pris." });
}

if (e instanceof LuciaError && e.message === `AUTH_DUPLICATE_KEY_ID`) {
return fail(400, { error: "Ce nom d'utilisateur est déjà pris." });
}

// eslint-disable-next-line no-console
console.error('Error registering:', e);

return fail(500, {
error: "Oups... Quelque chose s'est mal passé. Veuillez réessayer plus tard.",
});
}

redirect(303, '/');
},
} satisfies Actions;
10 changes: 9 additions & 1 deletion src/routes/register/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
<script lang="ts">
import Card from '$lib/components/Card.svelte';

import type { ActionData } from './$types';

export let form: ActionData;
</script>

<section class="section">
<Card title="Créer un compte">
<form method="POST" class="form">
<form action="?/register" method="POST" class="form">
<div>
<label for="username">Votre nom d'utilisateur</label>
<input type="text" name="username" id="username" required={true} />
Expand All @@ -30,6 +34,10 @@
/>
</div>

{#if form?.error}
<p class="text-sm font-light text-red-600">{form.error}</p>
{/if}

<button class="btn" type="submit">Créer un compte</button>
<p class="text-sm font-light text-gray-500">
Déjà membre ?
Expand Down
73 changes: 73 additions & 0 deletions src/routes/search/+page.server.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';
import { fail, redirect } from '@sveltejs/kit';

import { openai } from '$lib/server/GPT';
import { db } from '$lib/server/db';
import { slugify } from '$lib/utils/slug';

import type { PageServerLoad } from './$types';

Expand Down Expand Up @@ -94,3 +98,72 @@ export const load = (async ({ url }) => {
query,
};
}) satisfies PageServerLoad;

export const actions = {
generate: async ({ request }) => {
const data = await request.formData();
const dish = (data.get('dish') ?? '') as string;
const result = await openai.chat.completions.create({
model: 'gpt-3.5-turbo',
stream: false,
messages: [
{
role: 'system',
content: `
TU NE DOIS RETOURNER QUE DU JSON.
À partir de maintenant, tu es un assistant de cuisine personnel.
Je vais te donner une demande utilisateur, et tu me donneras une recette de cuisine pour cette dernière en français.
Si l'utilisateur demande le nom d'un plat, alors tu dois générer une recette pour ce plat.
Si l'utilisateur demande le nom d'un ingrédient, une description de ce qu'il souhaite manger, ou une recette pour une occasion spéciale, alors tu dois générer une recette qui correspond à cette demande.
Ton but final est uniquement de générer une recette de cuisine si possible. Cette recette pourra ensuite être consultée par n'importe quel utilisateur, il n'y a pas de lien entre la demande de l'utilisateur et la recette générée.
Si tu juges que la demande ne peut pas être satisfaite, ce n'est pas grave, retourne "null" et ignore la demande. N'essaie pas de générer une recette qui n'existe pas ou qui n'a pas de sens.
Si tu juges que la demande de l'utilisateur n'est pas valide, est obscène, insultante, ou ne correspond pas à une recette de cuisine, retourne "null" et ignore la demande.
Sinon, donne-moi une recette, et formate ta sortie en JSON avec le format suivant :
{
"description": string,
"dish": string,
"ingredients": string[],
"shoppingList": string[],
"slug": string,
"steps": string[]
}
`,
},
{
role: 'user',
content: dish,
},
],
});
const recipe = JSON.parse(result.choices[0].message.content ?? '');

if (!recipe) {
return fail(400, { error: "Votre demande n'est pas valide. Veuillez réessayer." });
}

try {
await db.recipe.create({
select: {
slug: true,
},
data: {
...recipe,
slug: slugify(recipe.dish),
},
});
} catch (e) {
if (e instanceof PrismaClientKnownRequestError && e.code === 'P2002') {
return fail(400, { error: 'Cette recette existe déjà.' });
}

// eslint-disable-next-line no-console
console.error('Error creating recipe:', e);

return fail(500, {
error: "Oups... Quelque chose s'est mal passé. Veuillez réessayer plus tard.",
});
}

redirect(303, `/recipes/${slugify(recipe.dish)}`);
},
};
9 changes: 7 additions & 2 deletions src/routes/search/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
import Search from '$lib/components/Search.svelte';
import ArrowRight from '$lib/svg/ArrowRight.svelte';

import type { PageData } from './$types';
import type { PageData, ActionData } from './$types';

export let data: PageData;
export let form: ActionData;
</script>

<h1 class="h1">Recherche</h1>
Expand All @@ -23,7 +24,7 @@
<Card>
{#if data.query.trim() !== ''}
<p class="text-gray-500 text-center" role="status">Aucun résulat pour "{data.query}".</p>
<form method="POST" class="form">
<form method="POST" action="?/generate" class="form">
<input type="hidden" name="dish" value={data.query} class="!hidden" />
<button type="submit" class="btn"> Générer la recette </button>
</form>
Expand Down Expand Up @@ -70,4 +71,8 @@
Oups! Quelque chose s'est mal passé. Veuillez réessayer plus tard.
</p>
{/await}

{#if form?.error}
<p class="text-sm font-light text-red-600 text-center">{form.error}</p>
{/if}
</div>