[Svelte Series 13] Authentication Flow and Protected UI

한국어 버전

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 handle hook throws, the entire request fails. Treat token errors as null instead of hard exceptions.

Key terms

  1. Session: the login token issued by the server so the app remembers the user after refresh.
  2. Cookie: the browser-stored chunk of data that keeps the session token via cookies.set.
  3. Route guard: a rule that blocks access to certain pages or layouts unless the user passes an auth check.
  4. event.locals: the shared object between hooks and load functions where you can stash user info.

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

Login formhooks.server handleevent.locals.user(app)/+layout.server(app)/+layoutdashboard routes/loginAuth service request + session cookieverify tokenuser|nullevent.locals.userload()data.user or redirectPOST credentialsauthenticatetokenSet-Cookiesubsequent requests

👉 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 };
  }
};
http://localhost:5173/dashboard
DEV

UI Preview

Dashboard access after login only

Once you log in, AppShell renders immediately. Unauthenticated users are redirected to the login page.

session cookielayout guard

Login

POST /login

Stores session cookie.

Redirect

redirectTo param

Returns to the original path after login.

Guard

layout.server.ts

Redirects with 303 when locals.user is missing.

👉 Mini recap: the “Login” tab highlights the cookie-writing action, “Redirect” shows the redirectTo query, and “Guard” demonstrates the layout check. All of them depend on the same cookie.

Optional: logout and role control

Only wire up logout actions or role-based branching (user.role === 'admin') when the infrastructure is ready. For school projects, it's fine to leave a note stating “Add logout and role guards later” if time is tight.

The login form uses use:enhance to manage submission state. Once the cookie is set, the guarded layout renders AppShell. The logout action deletes the cookie server-side to end the session cleanly.

Why it matters

Even a school project must protect personal data. Loading the session centrally keeps every page in sync and reduces mistakes. Guarded layouts stop unauthenticated users from seeing sensitive content even for a split second.

Practice tasks

  • Follow along: read the session cookie in hooks.server.ts, assign locals.user, and guard the layout.
  • Extend it: toggle header buttons or admin tools based on user.role.
  • Debugging: if auth drops randomly, double-check cookie options (path, httpOnly) and your parent() order.
  • Done when: logging in/out swaps the UI immediately and unauthenticated visitors only see guidance screens.

Wrap-up

With sessions and guards in place the app gains a solid foundation. Next we'll tackle file uploads and media-heavy flows.

💬 댓글

이 글에 대한 의견을 남겨주세요