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>
This commit is contained in:
2026-10-07 19:52:07 -04:00
commit c3cf0f3eb8
147 changed files with 21713 additions and 0 deletions

107
server/src/app.ts Normal file
View File

@@ -0,0 +1,107 @@
import { OpenAPIHono } from '@hono/zod-openapi';
import type { MiddlewareHandler } from 'hono';
import { HTTPException } from 'hono/http-exception';
import { secureHeaders } from 'hono/secure-headers';
import { sessionMiddleware } from './auth/guards';
import type { AppContext } from './context';
import { docsRoutes } from './docs';
import { fail, requestOrigin, type Env } from './http';
import { accountRoutes } from './routes/accounts';
import { authRoutes } from './routes/auth';
import { backupRoutes } from './routes/backups';
import { deviceRoutes } from './routes/devices';
import { ideaRoutes } from './routes/ideas';
import { importRoutes } from './routes/imports';
import { passkeyRoutes } from './routes/passkeys';
import { peopleRoutes } from './routes/people';
import { systemRoutes } from './routes/system';
import { transactionRoutes } from './routes/transactions';
import { validationHook } from './schemas';
import { webApp } from './static';
/** Changes must come from Spendlight's own pages, never from another site. */
function sameOriginWrites(ctx: AppContext): MiddlewareHandler<Env> {
return async (c, next) => {
if (!['GET', 'HEAD', 'OPTIONS'].includes(c.req.method)) {
const origin = c.req.header('origin');
if (origin) {
const allowed = new Set([requestOrigin(c, ctx)]);
if (ctx.config.publicUrl) allowed.add(ctx.config.publicUrl.origin);
if (!allowed.has(origin)) fail(403, 'wrong_origin', 'This request didn’t come from Spendlight.');
}
}
await next();
};
}
export function createApp(ctx: AppContext) {
const app = new OpenAPIHono<Env>({ defaultHook: validationHook });
app.onError((error, c) => {
if (error instanceof HTTPException) {
const code = (error.cause as { code?: string } | undefined)?.code ?? 'error';
return c.json({ error: { code, message: error.message } }, error.status);
}
ctx.log.error(`${c.req.method} ${new URL(c.req.url).pathname} failed`, error);
return c.json({ error: { code: 'server_error', message: 'Something went wrong on our side. Please try again.' } }, 500);
});
app.use(
'*',
secureHeaders({
contentSecurityPolicy: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:', 'blob:'],
fontSrc: ["'self'", 'data:'],
connectSrc: ["'self'"],
manifestSrc: ["'self'"],
workerSrc: ["'self'"],
objectSrc: ["'none'"],
baseUri: ["'self'"],
formAction: ["'self'"],
frameAncestors: ["'none'"],
},
crossOriginEmbedderPolicy: false,
referrerPolicy: 'same-origin',
}),
);
app.use('*', async (c, next) => {
await next();
c.header('X-Spendlight-Version', ctx.config.version);
});
app.use('/api/*', async (c, next) => {
await next();
c.header('Cache-Control', 'no-store');
});
app.use('/api/*', sessionMiddleware(ctx));
app.use('/api/*', sameOriginWrites(ctx));
app.route('/api/v1', systemRoutes(ctx));
app.route('/api/v1', authRoutes(ctx));
app.route('/api/v1', passkeyRoutes(ctx));
app.route('/api/v1', deviceRoutes(ctx));
app.route('/api/v1', peopleRoutes(ctx));
app.route('/api/v1', backupRoutes(ctx));
app.route('/api/v1', accountRoutes(ctx));
app.route('/api/v1', importRoutes(ctx));
app.route('/api/v1', transactionRoutes(ctx));
if (ctx.config.ideasEnabled) app.route('/api/v1', ideaRoutes(ctx));
app.doc31('/api/v1/openapi.json', {
openapi: '3.1.0',
info: {
title: 'Spendlight API',
version: ctx.config.version,
description:
'Everything the Spendlight screens do goes through this API. Sign in at /api/v1/auth/login; the session cookie authenticates the rest.',
},
});
app.route('/api/docs', docsRoutes());
app.all('/api/*', (c) => c.json({ error: { code: 'not_found', message: 'No such address in the Spendlight API.' } }, 404));
app.use('*', webApp(ctx.config.webDist));
return app;
}