feat: Security
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user