Add multi-user accounts with per-user data scoping and 2FA

Projects were previously global to anyone who could reach the server.
Adds email/password accounts with httpOnly cookie sessions, scopes
every project (both SQLite and localStorage) to the signed-in user,
and auto-claims pre-existing unowned projects for whoever registers
first. Also adds optional TOTP two-factor auth with backup codes,
managed from a new Account Settings page, since there's no
password-reset flow to fall back on otherwise.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
nmemmert
2026-07-06 08:49:33 -04:00
parent bf83dc7cc4
commit 5140958305
13 changed files with 1524 additions and 127 deletions
+48
View File
@@ -0,0 +1,48 @@
import session from 'express-session';
import { getSession, setSession, destroySession, pruneExpiredSessions } from './db.js';
const DAY_MS = 24 * 60 * 60 * 1000;
/**
* express-session store backed by the same SQLite database as everything else,
* so logins survive a server restart without adding another dependency.
*/
export class SqliteSessionStore extends session.Store {
constructor() {
super();
// Sweep expired sessions periodically instead of on every request.
this._interval = setInterval(() => pruneExpiredSessions(), DAY_MS);
this._interval.unref?.();
}
get(sid, cb) {
try {
cb(null, getSession(sid));
} catch (err) {
cb(err);
}
}
set(sid, sessionData, cb) {
try {
const maxAge = sessionData.cookie?.maxAge ?? DAY_MS * 30;
setSession(sid, sessionData, Date.now() + maxAge);
cb?.(null);
} catch (err) {
cb?.(err);
}
}
destroy(sid, cb) {
try {
destroySession(sid);
cb?.(null);
} catch (err) {
cb?.(err);
}
}
touch(sid, sessionData, cb) {
this.set(sid, sessionData, cb);
}
}