Let's add login and permission checks to the app.
Quick recap
- What we're building: read the session cookie, inject
locals.user, and guard the(app)layout based on authentication state. - Why it matters: once APIs return user-specific data, you need a single source of truth for auth state.
- Watch out for: if the
handlehook throws, the entire request fails. Treat token errors asnullinstead of hard exceptions.
Key terms
- Session: the login token issued by the server so the app remembers the user after refresh.
- Cookie: the browser-stored chunk of data that keeps the session token via
cookies.set. - Route guard: a rule that blocks access to certain pages or layouts unless the user passes an auth check.
event.locals: the shared object between hooks and load functions where you can stashuserinfo.
Core ideas
Sessions let the server remember each user. Cookies store the session token so a reload doesn't reset auth. Route guards verify whether the user is allowed to see a page or layout. event.locals is the perfect place to put the resolved user so every load function can read it.
Data flow
👉 Mini recap: every request first passes through hooks.server.ts to find the user in the cookie. The result lands in event.locals.user. Guarded layouts read it and redirect to /login when it's missing. The login action writes a fresh cookie and sends the user back to the page they expected.
Code examples
Server hook, layout guard, and login action all play a part.
// src/hooks.server.ts
const attachUser = async ({ event, resolve }) => {
const token = event.cookies.get('session');
if (!token) {
event.locals.user = null;
return resolve(event);
}
try {
event.locals.user = await getUserByToken(token);
} catch (error) {
console.error('Token verification failed', error);
event.locals.user = null;
event.cookies.delete('session', { path: '/' });
}
return resolve(event);
};
export const handle = sequence(attachUser);
// src/routes/(app)/+layout.server.ts
export const load = async ({ locals, url }) => {
if (!locals.user) {
throw redirect(303, `/login?redirectTo=${encodeURIComponent(url.pathname)}`);
}
return { user: locals.user };
};
<!-- src/routes/(app)/+layout.svelte -->
<script lang="ts">
export let data;
</script>
<AppShell user={data.user}>
<slot />
</AppShell>
// src/routes/login/+page.server.ts
export const load = ({ locals }) => {
if (locals.user) {
throw redirect(303, '/dashboard');
}
return {};
};
export const actions = {
default: async ({ request, cookies, url }) => {
const form = await request.formData();
const email = form.get('email');
const password = form.get('password');
const token = await authenticate(String(email), String(password));
if (!token) {
return fail(400, { message: 'Check your email or password.' });
}
cookies.set('session', token, { path: '/', httpOnly: true, maxAge: 60 * 60 * 24 * 7 });
const redirectTo = url.searchParams.get('redirectTo') ?? '/dashboard';
throw redirect(303, redirectTo);
}
};
<!-- src/routes/login/+page.svelte -->
<script lang="ts">
import { enhance } from '$app/forms';
import { page } from '$app/stores';
import { derived } from 'svelte/store';
const errors = derived(page, ($page) => $page.form?.message);
</script>
<form method="POST" use:enhance>
<label>
Email
<input name="email" type="email" autocomplete="email" required />
</label>
<label>
Password
<input name="password" type="password" autocomplete="current-password" required />
</label>
{#if $errors}
<p role="alert">{$errors}</p>
{/if}
<button type="submit">Log in</button>
</form>
<form method="POST" action="?/logout" class="logout">
<button type="submit">Log out</button>
</form>
// src/routes/(app)/+layout.server.ts (add logout action)
export const actions = {
logout: async ({ cookies }) => {
cookies.delete('session', { path: '/' });
return { success: true };
}
};
💬 댓글
이 글에 대한 의견을 남겨주세요