## 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}
```
### 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');
```