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
115 lines
3.1 KiB
TypeScript
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;
|