diff --git a/packages/web/package.json b/packages/web/package.json index 61587ea..0f06ad9 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -2,6 +2,7 @@ "name": "web", "version": "0.0.1", "type": "module", + "license": "MIT", "scripts": { "dev": "vite dev", "build": "vite build", diff --git a/packages/web/src/app.d.ts b/packages/web/src/app.d.ts index da08e6d..3f2166e 100644 --- a/packages/web/src/app.d.ts +++ b/packages/web/src/app.d.ts @@ -3,7 +3,9 @@ declare global { namespace App { // interface Error {} - // interface Locals {} + interface Locals { + authenticated: boolean; + } // interface PageData {} // interface PageState {} // interface Platform {} diff --git a/packages/web/src/hooks.server.ts b/packages/web/src/hooks.server.ts new file mode 100644 index 0000000..7b52b93 --- /dev/null +++ b/packages/web/src/hooks.server.ts @@ -0,0 +1,10 @@ +import type { Handle } from '@sveltejs/kit'; + +export const handle: Handle = async ({ event, resolve }) => { + // Get the JWT from cookies + const jwt = event.cookies.get('jwt'); + // Set the user as authenticated in the locals object if JWT exists + event.locals.authenticated = !!jwt; + + return await resolve(event); +}; diff --git a/packages/web/src/lib/auth.ts b/packages/web/src/lib/auth.ts new file mode 100644 index 0000000..3ddc879 --- /dev/null +++ b/packages/web/src/lib/auth.ts @@ -0,0 +1,86 @@ +import { isAuthenticated } from './stores/auth'; +import { goto } from '$app/navigation'; +import { env } from '$env/dynamic/private'; + +interface AuthResponse { + ok: boolean; + error?: string; +} + +export async function login(username: string, password: string): Promise { + try { + const response = await fetch(`${env.API_URL}/auth/login`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ username, password }) + }); + + if (response.ok) { + isAuthenticated.set(true); + return { ok: true }; + } + + return { + ok: false, + error: 'Invalid credentials' + }; + } catch (error) { + console.error('Could not login', error); + return { + ok: false, + error: 'An error occurred during login' + }; + } +} + +export async function register( + username: string, + email: string, + password: string, + passwordConfirmation: string +): Promise { + try { + const response = await fetch(`${env.API_URL}/auth/register`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + username, + email, + password, + password_confirmation: passwordConfirmation + }) + }); + + if (response.ok) { + isAuthenticated.set(true); + return { ok: true }; + } + + return { + ok: false, + error: 'Registration failed' + }; + } catch (error) { + console.error('Could not register', error); + return { + ok: false, + error: 'An error occurred during registration' + }; + } +} + +export async function logout() { + try { + await fetch(`${env.API_URL}/auth/logout`, { + method: 'POST' + }); + isAuthenticated.set(false); + goto('/login'); + } catch (error) { + console.error('Logout failed:', error); + } +} diff --git a/packages/web/src/lib/stores/auth.ts b/packages/web/src/lib/stores/auth.ts new file mode 100644 index 0000000..509934f --- /dev/null +++ b/packages/web/src/lib/stores/auth.ts @@ -0,0 +1,3 @@ +import { writable } from 'svelte/store'; + +export const isAuthenticated = writable(false); diff --git a/packages/web/src/routes/+layout.server.ts b/packages/web/src/routes/+layout.server.ts new file mode 100644 index 0000000..0be9caf --- /dev/null +++ b/packages/web/src/routes/+layout.server.ts @@ -0,0 +1,7 @@ +import type { LayoutServerLoad } from './$types'; + +export const load: LayoutServerLoad = async ({ locals }) => { + return { + authenticated: locals.authenticated + }; +}; diff --git a/packages/web/src/routes/+layout.svelte b/packages/web/src/routes/+layout.svelte index 08bf6e4..72ebddb 100644 --- a/packages/web/src/routes/+layout.svelte +++ b/packages/web/src/routes/+layout.svelte @@ -1,12 +1,19 @@
+ {#if isAuthenticated} +
{JSON.stringify(data, null, 2)}
+ {/if}
diff --git a/packages/web/src/routes/login/+page.server.ts b/packages/web/src/routes/login/+page.server.ts new file mode 100644 index 0000000..6254239 --- /dev/null +++ b/packages/web/src/routes/login/+page.server.ts @@ -0,0 +1,47 @@ +import { fail, redirect } from '@sveltejs/kit'; +import type { Actions } from './$types'; +import { env } from '$env/dynamic/private'; + +export const actions = { + default: async ({ request, cookies }) => { + const data = await request.formData(); + const username = data.get('username'); + const password = data.get('password'); + + // Basic validation + if (!username || !password) { + return fail(400, { + error: 'Username and password are required', + username: username?.toString() + }); + } + + const response = await fetch(`${env.API_URL}/auth/login`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + username: username.toString(), + password: password.toString() + }) + }); + + if (!response.ok) { + const errorData = await response.json(); + return fail(response.status, { + error: errorData.message || 'Invalid credentials', + username: username.toString() + }); + } + const { token } = await response.json(); + + cookies.set('jwt', token, { + path: '/', + expires: new Date(Date.now() + 1000 * 60 * 60 * 24), // 24 hours + sameSite: true + }); + + throw redirect(303, '/'); + } +} satisfies Actions; diff --git a/packages/web/src/routes/login/+page.svelte b/packages/web/src/routes/login/+page.svelte index 15bbee0..23f038d 100644 --- a/packages/web/src/routes/login/+page.svelte +++ b/packages/web/src/routes/login/+page.svelte @@ -1 +1,73 @@ -login page + + +
{ + isLoading = true; + return async ({ update }) => { + await update(); + isLoading = false; + }; + }} + class="mx-auto mt-8 max-w-sm rounded-lg bg-white p-6 shadow-md dark:bg-secondary-950 dark:shadow-xl" +> + {#if form?.error} + + {/if} + + + + + + +