feat: Security

This commit is contained in:
2026-06-05 18:34:01 +02:00
parent ee293d7daf
commit 45675192a5
134 changed files with 4128 additions and 446 deletions

View File

@@ -1,13 +1,30 @@
import crypto from 'crypto';
import { Router } from 'express';
import { v4 as uuidv4 } from 'uuid';
import { getUserByUsername, registerUser } from '../cqrs';
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';
const router = Router();
function hashPassword(pw: string): string {
return crypto.createHash('sha256').update(pw)
.digest('hex');
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) => {
@@ -24,23 +41,64 @@ router.post('/register', async (req, res) => {
const user = {
id: uuidv4(),
username,
passwordHash: hashPassword(password),
passwordHash: await hashPasswordForStorage(password),
displayName: displayName || username,
createdAt: Date.now()
};
await registerUser(user);
res.status(201).json({ id: user.id, username: user.username, displayName: user.displayName });
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 || user.passwordHash !== hashPassword(password))
if (!user || !(await verifyPassword(password, user.passwordHash)))
return res.status(401).json({ error: 'Invalid credentials' });
res.json({ id: user.id, username: user.username, displayName: user.displayName });
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;