Files
Spendlight/server/src/auth/guards.ts
Chris Olson c3cf0f3eb8 Milestones 0 and 1: foundation and money in (20261007.10)
Foundation: single arm64 container (Hono + SQLite via Drizzle, React + Vite),
local sign-in with long-lived sessions and passkeys, roles (read-only, full
access, admin), nightly and pre-upgrade backups, light and dark themes,
date-based version numbers, and the ideas lightbulb behind IDEAS_ENABLED.

Money in: accounts with balances, CSV import with bank detection, a column
mapping pop-up for unknown layouts, duplicate checks, transfer pairing and
undo; one-tap review with learned rules; activity with search, splits, tags,
notes and cash entries. Statement files are blocked from commits by a
pre-commit hook; tests use synthetic fixtures only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 19:52:07 -04:00

58 lines
2.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { Context, MiddlewareHandler } from 'hono';
import { getCookie } from 'hono/cookie';
import type { AppContext } from '../context';
import type { UserRow } from '../db/schema';
import { clientIp, fail, hostnameOf, isLocalHost, requestHost, requestProto, type Env } from '../http';
import { clearSessionCookie, loadSession, SESSION_COOKIE, setSessionCookie, touchSession } from './sessions';
/** Attach the signed-in person (if any) to every API request, and keep their session fresh. */
export function sessionMiddleware(ctx: AppContext): MiddlewareHandler<Env> {
return async (c, next) => {
c.set('user', null);
c.set('session', null);
const token = getCookie(c, SESSION_COOKIE);
if (token) {
const found = loadSession(ctx, token);
if (found) {
c.set('user', found.user);
c.set('session', found.session);
if (touchSession(ctx, found.session, clientIp(c, ctx))) setSessionCookie(c, ctx, token);
} else {
clearSessionCookie(c);
}
}
await next();
};
}
export function requireUser(c: Context<Env>): UserRow {
const user = c.get('user');
if (!user) fail(401, 'signed_out', 'Please sign in to continue.');
return user;
}
/** Changing household data needs Full access; Read-only people can look but not touch. */
export function requireWrite(c: Context<Env>): UserRow {
const user = requireUser(c);
if (user.access !== 'full') fail(403, 'read_only', 'Your access is read-only, so this change isn’t available.');
return user;
}
export function requireAdmin(c: Context<Env>): UserRow {
const user = requireUser(c);
if (!user.isAdmin) fail(403, 'admin_only', 'Only an admin can do that.');
return user;
}
/**
* Refuse password sign-in over plain HTTP from anywhere but this machine, so
* passwords and cookies only ever travel encrypted.
*/
export function assertSecureSignIn(c: Context<Env>, ctx: AppContext): void {
if (ctx.config.allowInsecureSignin) return;
if (requestProto(c, ctx) === 'https') return;
if (isLocalHost(hostnameOf(requestHost(c, ctx)))) return;
const where = ctx.config.publicUrl ? `: ${ctx.config.publicUrl.origin}` : '';
fail(400, 'insecure_connection', `For your security, open Spendlight at its secure (https) address to sign in${where}.`);
}