Files
Toju/server/src/routes/users.ts
Myx eb51f043ac
All checks were successful
Queue Release Build / prepare (push) Successful in 19s
Deploy Web Apps / deploy (push) Successful in 8m12s
Queue Release Build / build-windows (push) Successful in 27m44s
Queue Release Build / build-linux (push) Successful in 48m1s
Queue Release Build / build-android (push) Successful in 22m7s
Queue Release Build / finalize (push) Successful in 2m42s
fix: Major bug cleanup pass 1
2026-06-09 17:59:54 +02:00

115 lines
3.1 KiB
TypeScript

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;