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:
+55
-2
@@ -15,16 +15,17 @@ async function request(method, path, body) {
|
||||
const opts = {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
};
|
||||
if (body !== undefined) opts.body = JSON.stringify(body);
|
||||
const res = await fetch(`${BASE}${path}`, opts);
|
||||
const data = await res.json().catch(() => null);
|
||||
if (!res.ok) {
|
||||
return { ok: false, error: data?.error ?? `HTTP ${res.status}` };
|
||||
return { ok: false, status: res.status, error: data?.error ?? `HTTP ${res.status}` };
|
||||
}
|
||||
return { ok: true, data };
|
||||
} catch (err) {
|
||||
return { ok: false, error: err?.message ?? 'Network error' };
|
||||
return { ok: false, status: null, error: err?.message ?? 'Network error' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,4 +76,56 @@ export async function isServerReachable() {
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Auth
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Returns the signed-in user, or null if not signed in.
|
||||
* Returns { ok: false } (with status: null) only on a genuine network failure,
|
||||
* so callers can distinguish "not logged in" from "server unreachable".
|
||||
*/
|
||||
export async function getCurrentUser() {
|
||||
const result = await request('GET', '/auth/me');
|
||||
if (result.ok) return { ok: true, user: result.data };
|
||||
if (result.status === 401) return { ok: true, user: null };
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function registerUser(email, password) {
|
||||
return request('POST', '/auth/register', { email, password });
|
||||
}
|
||||
|
||||
export async function loginUser(email, password) {
|
||||
return request('POST', '/auth/login', { email, password });
|
||||
}
|
||||
|
||||
export async function logoutUser() {
|
||||
return request('POST', '/auth/logout');
|
||||
}
|
||||
|
||||
/** Submits the code from the auth gate's post-password MFA step. Pass token or backupCode. */
|
||||
export async function verifyMfaLogin({ token, backupCode }) {
|
||||
return request('POST', '/auth/mfa/verify', { token, backupCode });
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Two-factor auth setup (Account Settings page)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Starts 2FA setup: returns { secret, qrCodeDataUrl } for the user to scan. */
|
||||
export async function startMfaSetup() {
|
||||
return request('POST', '/auth/mfa/setup');
|
||||
}
|
||||
|
||||
/** Confirms the scanned code and turns 2FA on. Returns { backupCodes } (shown once). */
|
||||
export async function confirmMfaSetup(token) {
|
||||
return request('POST', '/auth/mfa/enable', { token });
|
||||
}
|
||||
|
||||
/** Turns 2FA off. Requires the current password as a safety check. */
|
||||
export async function disableMfa(password) {
|
||||
return request('POST', '/auth/mfa/disable', { password });
|
||||
}
|
||||
Reference in New Issue
Block a user