Skip to content

Commit

Permalink
Have svelte put token into cookie for client
Browse files Browse the repository at this point in the history
  • Loading branch information
sneakycrow committed Oct 31, 2024
1 parent b0e253d commit 7845b1c
Show file tree
Hide file tree
Showing 9 changed files with 238 additions and 3 deletions.
1 change: 1 addition & 0 deletions packages/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"name": "web",
"version": "0.0.1",
"type": "module",
"license": "MIT",
"scripts": {
"dev": "vite dev",
"build": "vite build",
Expand Down
4 changes: 3 additions & 1 deletion packages/web/src/app.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
declare global {
namespace App {
// interface Error {}
// interface Locals {}
interface Locals {
authenticated: boolean;
}
// interface PageData {}
// interface PageState {}
// interface Platform {}
Expand Down
10 changes: 10 additions & 0 deletions packages/web/src/hooks.server.ts
Original file line number Diff line number Diff line change
@@ -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);
};
86 changes: 86 additions & 0 deletions packages/web/src/lib/auth.ts
Original file line number Diff line number Diff line change
@@ -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<AuthResponse> {
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<AuthResponse> {
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);
}
}
3 changes: 3 additions & 0 deletions packages/web/src/lib/stores/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { writable } from 'svelte/store';

export const isAuthenticated = writable<boolean>(false);
7 changes: 7 additions & 0 deletions packages/web/src/routes/+layout.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { LayoutServerLoad } from './$types';

export const load: LayoutServerLoad = async ({ locals }) => {
return {
authenticated: locals.authenticated
};
};
9 changes: 8 additions & 1 deletion packages/web/src/routes/+layout.svelte
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
<script lang="ts">
import '../app.css';
import { isAuthenticated } from '$lib/stores/auth';
import type { LayoutData } from './$types';
import type { Snippet } from 'svelte';
let { data, children }: { data: LayoutData; children: Snippet } = $props();
import ThemeToggler from '$lib/components/ThemeToggler.svelte';
import Header from '$lib/components/Header.svelte';
let { children } = $props();
</script>

<Header />
<main>
{#if isAuthenticated}
<pre>{JSON.stringify(data, null, 2)}</pre>
{/if}
<div class="fixed right-4 top-4 z-50">
<ThemeToggler />
</div>
Expand Down
47 changes: 47 additions & 0 deletions packages/web/src/routes/login/+page.server.ts
Original file line number Diff line number Diff line change
@@ -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;
74 changes: 73 additions & 1 deletion packages/web/src/routes/login/+page.svelte
Original file line number Diff line number Diff line change
@@ -1 +1,73 @@
login page
<script lang="ts">
import Alert from '$lib/components/Alert.svelte';
import { enhance } from '$app/forms';
import type { ActionResult } from '@sveltejs/kit';
export let form: ActionResult;
let isLoading = false;
</script>

<form
method="POST"
use:enhance={() => {
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}
<Alert type="error" message={form.error} />
{/if}

<label class="mb-4 block">
<span class="text-sm text-secondary-800 dark:text-secondary-100">Username</span>
<input
name="username"
type="text"
value={form?.username ?? ''}
class="mt-1 block w-full
rounded-md border-secondary-200 bg-white
text-base shadow-sm
focus:border-accent-500 focus:ring focus:ring-accent-200 focus:ring-opacity-50
dark:border-secondary-800 dark:bg-secondary-900 dark:text-secondary-100
dark:focus:border-accent-400 dark:focus:ring-accent-900"
/>
</label>

<label class="mb-4 block">
<span class="text-sm text-secondary-800 dark:text-secondary-100">Password</span>
<input
name="password"
type="password"
class="mt-1 block w-full
rounded-md border-secondary-200 bg-white
text-base shadow-sm
focus:border-accent-500 focus:ring focus:ring-accent-200 focus:ring-opacity-50
dark:border-secondary-800 dark:bg-secondary-900 dark:text-secondary-100
dark:focus:border-accent-400 dark:focus:ring-accent-900"
/>
</label>

<button
type="submit"
disabled={isLoading}
class="flex w-full items-center
justify-center gap-2 rounded-md
bg-accent-500
px-4 py-2 text-sm font-medium
text-white hover:bg-accent-600 focus:outline-none focus:ring-2
focus:ring-accent-400 focus:ring-offset-2 disabled:cursor-not-allowed
disabled:opacity-50 dark:bg-accent-600
dark:hover:bg-accent-500
dark:focus:ring-accent-300 dark:focus:ring-offset-secondary-950"
>
{#if isLoading}
<span>Logging in...</span>
{:else}
<span>Login</span>
{/if}
</button>
</form>

0 comments on commit 7845b1c

Please sign in to comment.