## Demo: login ### Login.svelte ```svelte

With {'{@attach enhance(...)}'}

Plain HTML

``` ### EnhancedLoginForm.svelte ```svelte {#if currentUser}

Signed in as {currentUser}.{isHydratable ? ' No page reloads happened on the way here.' : ''}

{:else}
{#if errorMessage} {/if}

The password is hunter2. Any username works.

{/if} ``` ### session.ts ```ts import { createHmac, timingSafeEqual } from 'node:crypto'; import { getMochiConfig } from 'mochi-framework'; const CONTEXT = 'mochi-demo-session'; const DEFAULT_MAX_AGE_SEC = 7 * 24 * 60 * 60; function sign(data: string): string { const { secretKey } = getMochiConfig(); return createHmac('sha256', secretKey).update(CONTEXT).update(':').update(data).digest().subarray(0, 16).toString('base64url'); } export interface SessionData { username: string; exp: number; } export function createSessionToken(username: string, maxAgeSec: number = DEFAULT_MAX_AGE_SEC): { token: string; maxAgeSec: number; exp: number } { const exp = Math.floor(Date.now() / 1000) + maxAgeSec; const payload = Buffer.from(JSON.stringify({ username, exp })).toString('base64url'); return { token: `${payload}.${sign(payload)}`, maxAgeSec, exp }; } export function verifySessionToken(token: string | undefined): SessionData | null { if (!token) { return null; } const dot = token.lastIndexOf('.'); if (dot === -1) { return null; } const payload = token.slice(0, dot); const sig = token.slice(dot + 1); const expected = sign(payload); if (sig.length !== expected.length) { return null; } try { if (!timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) { return null; } } catch { return null; } let parsed: SessionData; try { parsed = JSON.parse(Buffer.from(payload, 'base64url').toString('utf-8')); } catch { return null; } if (typeof parsed.username !== 'string' || typeof parsed.exp !== 'number') { return null; } if (parsed.exp < Math.floor(Date.now() / 1000)) { return null; } return parsed; } ``` ### routes.ts ```ts import { Mochi, fail, redirect, success, getRequestContext } from 'mochi-framework'; import type { MochiRouteValue } from 'mochi-framework'; import { createSessionToken, verifySessionToken } from './session'; const SESSION_COOKIE = 'mochi_login_session'; export const routes: Record = { '/demos/login': Mochi.page('./src/demos/login/Login.svelte', { serverProps: () => { const { cookies } = getRequestContext(); const session = verifySessionToken(cookies.get(SESSION_COOKIE)); return { currentUser: session?.username ?? null }; }, actions: { default: async ({ formData, cookies }) => { const username = String(formData.get('username') ?? '').trim(); const password = String(formData.get('password') ?? ''); if (!username) { return fail(400, { error: 'Username required', username }); } if (password !== 'hunter2') { return fail(401, { error: 'Bad credentials', username }); } const { token, maxAgeSec } = createSessionToken(username); cookies.set(SESSION_COOKIE, token, { path: '/', httpOnly: true, sameSite: 'Lax', maxAge: maxAgeSec, }); return success({ username }); }, logout: async ({ cookies }) => { cookies.delete(SESSION_COOKIE, { path: '/' }); return redirect(303, '/demos/login'); }, }, }), }; ``` ### index.ts ```ts import { Mochi, logger } from 'mochi-framework'; await Mochi.serve({ port: 3333, development: process.env.MODE === 'development', routes: { '/': Mochi.page('./src/Home.svelte'), }, }); logger.info('Server running at http://localhost:3333'); ```