import { Router } from 'express'; import { v4 as uuidv4 } from 'uuid'; import { getUserById, getUserByUsername, registerUser, updateUserPasswordHash, updateUserSigningPublicKey } from '../cqrs'; import { hashPasswordForStorage, verifyPassword } from '../services/password-auth.service'; import { issueSessionToken, revokeSessionToken } from '../services/session-auth.service'; import { getAuthenticatedUserId, requireAuth } from '../middleware/require-auth'; import { isDuplicateUsernameError } from './user-registration.rules'; const router = Router(); function buildAuthResponse(user: { id: string; username: string; displayName: string; }, token: string, expiresAt: number) { return { id: user.id, username: user.username, displayName: user.displayName, token, expiresAt }; } router.post('/register', async (req, res) => { const { username, password, displayName } = req.body; if (!username || !password) return res.status(400).json({ error: 'Missing username/password' }); const existing = await getUserByUsername(username); if (existing) return res.status(409).json({ error: 'Username taken' }); const user = { id: uuidv4(), username, passwordHash: await hashPasswordForStorage(password), displayName: displayName || username, createdAt: Date.now() }; try { await registerUser(user); } catch (error) { if (isDuplicateUsernameError(error)) { return res.status(409).json({ error: 'Username taken' }); } throw error; } const session = await issueSessionToken(user.id); res.status(201).json(buildAuthResponse(user, session.token, session.expiresAt)); }); router.post('/login', async (req, res) => { const { username, password } = req.body; const user = await getUserByUsername(username); if (!user || !(await verifyPassword(password, user.passwordHash))) return res.status(401).json({ error: 'Invalid credentials' }); const upgradedHash = await hashPasswordForStorage(password, user.passwordHash); if (upgradedHash !== user.passwordHash) { await updateUserPasswordHash(user.id, upgradedHash); } const session = await issueSessionToken(user.id); res.json(buildAuthResponse(user, session.token, session.expiresAt)); }); router.put('/me/signing-key', requireAuth, async (req, res) => { const { publicKeyJwk } = req.body; const userId = getAuthenticatedUserId(req); if (!publicKeyJwk || typeof publicKeyJwk !== 'object') { return res.status(400).json({ error: 'Missing publicKeyJwk', errorCode: 'INVALID_SIGNING_KEY' }); } await updateUserSigningPublicKey(userId, JSON.stringify(publicKeyJwk)); res.json({ ok: true }); }); router.get('/:id/signing-public-key', async (req, res) => { const user = await getUserById(req.params.id); if (!user?.signingPublicKey) { return res.status(404).json({ error: 'Signing public key not found', errorCode: 'SIGNING_KEY_NOT_FOUND' }); } res.json({ publicKeyJwk: JSON.parse(user.signingPublicKey) }); }); router.post('/logout', requireAuth, async (req, res) => { if (req.authToken) { await revokeSessionToken(req.authToken); } res.json({ ok: true }); }); export default router;