Add a duress password (DISTRESS_PASSWORD env): entering it at login trips an in-memory full lockdown — all new logins (password + passkey) and every existing session are refused until the server is restarted, and the login itself returns a normal "invalid credentials" so it gives nothing away. Also add POST /api/auth/blacklist-token as a clearly-named alias for revoking the current JWT (same effect as signout). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
55 lines
2.0 KiB
TypeScript
Executable File
55 lines
2.0 KiB
TypeScript
Executable File
import type { Handler } from 'hono';
|
|
import { mkdir } from 'node:fs/promises';
|
|
import { join } from 'node:path';
|
|
import { getUserByEmail, getPasskeysByUserIdAndOrigin } from 'officerdb';
|
|
import { sign } from '@@/jwt';
|
|
import { getClaudeDir } from '@@/data-path';
|
|
import argon2 from 'argon2';
|
|
import * as errors from '@@/custom-errors';
|
|
import { isLockdown, triggerLockdown } from './distress';
|
|
|
|
const TEST_USERS: number[] = [];
|
|
const { DISTRESS_PASSWORD } = process.env;
|
|
|
|
export const signinHandler: Handler = async function (ctx) {
|
|
const { email, password } = ctx.get('body');
|
|
|
|
// Duress: if lockdown is active, refuse everyone (looks like a normal failed login). If the distress
|
|
// password was entered, trip the lockdown now and then fail the same way, giving nothing away.
|
|
if (isLockdown()) throw errors.UNAUTHORIZED();
|
|
if (DISTRESS_PASSWORD && typeof password === 'string' && password === DISTRESS_PASSWORD) {
|
|
triggerLockdown();
|
|
throw errors.UNAUTHORIZED();
|
|
}
|
|
|
|
const origin = ctx.get('origin');
|
|
const dbUser = await getUserByEmail(email);
|
|
if (!dbUser) throw errors.UNAUTHORIZED();
|
|
|
|
const passkeys = await getPasskeysByUserIdAndOrigin(dbUser.id, origin);
|
|
|
|
if (!dbUser.password) throw errors.UNAUTHORIZED();
|
|
const { status } = dbUser;
|
|
if (status !== 'Active') throw errors.UNAUTHORIZED();
|
|
const isValidPassword = TEST_USERS.includes(dbUser.id) || (await argon2.verify(dbUser.password, password));
|
|
if (!isValidPassword) throw errors.UNAUTHORIZED();
|
|
|
|
const { id, name, username, role } = dbUser;
|
|
|
|
mkdir(join(getClaudeDir(email), 'archived'), { recursive: true }).catch(() => {});
|
|
|
|
const tokenUser = { id, email, name, username, role, passkeys: passkeys.length };
|
|
|
|
if (passkeys.length > 0 && !origin.startsWith('chrome-extension://') && !TEST_USERS.includes(dbUser.id)) {
|
|
return ctx.json({ user: tokenUser });
|
|
}
|
|
|
|
const token = await sign(tokenUser);
|
|
|
|
if (origin.startsWith('chrome-extension://')) {
|
|
// console.log('token', token);
|
|
}
|
|
|
|
return ctx.json({ token, user: tokenUser });
|
|
};
|