feat: Security
This commit is contained in:
@@ -4,6 +4,7 @@ import {
|
||||
listDeviceTokensForUser,
|
||||
upsertDeviceToken
|
||||
} from '../services/push-dispatch.service';
|
||||
import { requireAuth } from '../middleware/require-auth';
|
||||
|
||||
export interface DeviceTokenRecord {
|
||||
userId: string;
|
||||
@@ -14,6 +15,8 @@ export interface DeviceTokenRecord {
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use(requireAuth);
|
||||
|
||||
router.post('/', async (req, res) => {
|
||||
const { userId, platform, token } = req.body as Partial<DeviceTokenRecord>;
|
||||
|
||||
@@ -21,12 +24,20 @@ router.post('/', async (req, res) => {
|
||||
return res.status(400).json({ error: 'Missing or invalid userId/platform/token' });
|
||||
}
|
||||
|
||||
if (userId !== req.authUserId) {
|
||||
return res.status(403).json({ error: 'Not authorized', errorCode: 'NOT_AUTHORIZED' });
|
||||
}
|
||||
|
||||
await upsertDeviceToken({ userId, platform, token });
|
||||
|
||||
res.status(201).json({ ok: true });
|
||||
});
|
||||
|
||||
router.get('/:userId', async (req, res) => {
|
||||
if (req.params.userId !== req.authUserId) {
|
||||
return res.status(403).json({ error: 'Not authorized', errorCode: 'NOT_AUTHORIZED' });
|
||||
}
|
||||
|
||||
const records = await listDeviceTokensForUser(req.params.userId);
|
||||
|
||||
res.json({
|
||||
@@ -40,6 +51,10 @@ router.get('/:userId', async (req, res) => {
|
||||
});
|
||||
|
||||
router.post('/:userId/dispatch', async (req, res) => {
|
||||
if (req.params.userId !== req.authUserId) {
|
||||
return res.status(403).json({ error: 'Not authorized', errorCode: 'NOT_AUTHORIZED' });
|
||||
}
|
||||
|
||||
const { title, body, data } = req.body as {
|
||||
title?: string;
|
||||
body?: string;
|
||||
|
||||
@@ -7,20 +7,29 @@ import {
|
||||
} from '../cqrs';
|
||||
import { notifyUser } from '../websocket/broadcast';
|
||||
import { resolveServerPermission } from '../services/server-permissions.service';
|
||||
import { getAuthenticatedUserId, requireAuth } from '../middleware/require-auth';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.put('/:id', async (req, res) => {
|
||||
router.put('/:id', requireAuth, async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { ownerId, status } = req.body;
|
||||
const authenticatedUserId = getAuthenticatedUserId(req);
|
||||
const request = await getJoinRequestById(id);
|
||||
|
||||
if (!request)
|
||||
return res.status(404).json({ error: 'Request not found' });
|
||||
|
||||
if (ownerId && ownerId !== authenticatedUserId) {
|
||||
return res.status(400).json({
|
||||
error: 'ownerId must match the authenticated user',
|
||||
errorCode: 'USER_ID_MISMATCH'
|
||||
});
|
||||
}
|
||||
|
||||
const server = await getServerById(request.serverId);
|
||||
|
||||
if (!server || !ownerId || !resolveServerPermission(server, String(ownerId), 'manageServer'))
|
||||
if (!server || !resolveServerPermission(server, authenticatedUserId, 'manageServer'))
|
||||
return res.status(403).json({ error: 'Not authorized' });
|
||||
|
||||
await updateJoinRequestStatus(id, status as JoinRequestPayload['status']);
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
upsertPluginRequirement
|
||||
} from '../services/plugin-support.service';
|
||||
import { broadcastToServer } from '../websocket/broadcast';
|
||||
import { getAuthenticatedUserId, requireAuth } from '../middleware/require-auth';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -21,8 +22,28 @@ function sendPluginSupportError(error: unknown, res: Response): void {
|
||||
res.status(500).json({ error: 'Internal server error', errorCode: 'INTERNAL_ERROR' });
|
||||
}
|
||||
|
||||
function readActorUserId(value: unknown): string {
|
||||
return typeof value === 'string' ? value.trim() : '';
|
||||
type AuthenticatedRequest = Parameters<typeof getAuthenticatedUserId>[0] & { body: { actorUserId?: unknown } };
|
||||
|
||||
function readOptionalActorUserId(req: AuthenticatedRequest, res: Response): string | null {
|
||||
let authenticatedUserId: string;
|
||||
|
||||
try {
|
||||
authenticatedUserId = getAuthenticatedUserId(req);
|
||||
} catch {
|
||||
res.status(401).json({ error: 'Missing or invalid authorization token', errorCode: 'UNAUTHORIZED' });
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof req.body.actorUserId === 'string' && req.body.actorUserId.trim() && req.body.actorUserId !== authenticatedUserId) {
|
||||
res.status(400).json({
|
||||
error: 'actorUserId must match the authenticated user',
|
||||
errorCode: 'USER_ID_MISMATCH'
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return authenticatedUserId;
|
||||
}
|
||||
|
||||
async function broadcastRequirementsSnapshot(serverId: string): Promise<void> {
|
||||
@@ -43,12 +64,17 @@ router.get('/:serverId/plugins', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.put('/:serverId/plugins/:pluginId/requirement', async (req, res) => {
|
||||
router.put('/:serverId/plugins/:pluginId/requirement', requireAuth, async (req, res) => {
|
||||
const { serverId, pluginId } = req.params;
|
||||
const actorUserId = readOptionalActorUserId(req, res);
|
||||
|
||||
if (!actorUserId) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const requirement = await upsertPluginRequirement({
|
||||
actorUserId: readActorUserId(req.body.actorUserId),
|
||||
actorUserId,
|
||||
installUrl: req.body.installUrl,
|
||||
manifest: req.body.manifest,
|
||||
pluginId,
|
||||
@@ -66,12 +92,17 @@ router.put('/:serverId/plugins/:pluginId/requirement', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/:serverId/plugins/:pluginId/requirement', async (req, res) => {
|
||||
router.delete('/:serverId/plugins/:pluginId/requirement', requireAuth, async (req, res) => {
|
||||
const { serverId, pluginId } = req.params;
|
||||
const actorUserId = readOptionalActorUserId(req, res);
|
||||
|
||||
if (!actorUserId) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await deletePluginRequirement({
|
||||
actorUserId: readActorUserId(req.body.actorUserId),
|
||||
actorUserId,
|
||||
pluginId,
|
||||
serverId
|
||||
});
|
||||
@@ -83,12 +114,17 @@ router.delete('/:serverId/plugins/:pluginId/requirement', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.put('/:serverId/plugins/:pluginId/events/:eventName', async (req, res) => {
|
||||
router.put('/:serverId/plugins/:pluginId/events/:eventName', requireAuth, async (req, res) => {
|
||||
const { serverId, pluginId, eventName } = req.params;
|
||||
const actorUserId = readOptionalActorUserId(req, res);
|
||||
|
||||
if (!actorUserId) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const eventDefinition = await upsertPluginEventDefinition({
|
||||
actorUserId: readActorUserId(req.body.actorUserId),
|
||||
actorUserId,
|
||||
direction: req.body.direction,
|
||||
eventName,
|
||||
maxPayloadBytes: req.body.maxPayloadBytes,
|
||||
@@ -106,12 +142,17 @@ router.put('/:serverId/plugins/:pluginId/events/:eventName', async (req, res) =>
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/:serverId/plugins/:pluginId/events/:eventName', async (req, res) => {
|
||||
router.delete('/:serverId/plugins/:pluginId/events/:eventName', requireAuth, async (req, res) => {
|
||||
const { serverId, pluginId, eventName } = req.params;
|
||||
const actorUserId = readOptionalActorUserId(req, res);
|
||||
|
||||
if (!actorUserId) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await deletePluginEventDefinition({
|
||||
actorUserId: readActorUserId(req.body.actorUserId),
|
||||
actorUserId,
|
||||
eventName,
|
||||
pluginId,
|
||||
serverId
|
||||
|
||||
@@ -35,6 +35,11 @@ import {
|
||||
canModerateServerMember,
|
||||
resolveServerPermission
|
||||
} from '../services/server-permissions.service';
|
||||
import {
|
||||
getAuthenticatedUserId,
|
||||
requireAuth,
|
||||
rejectSpoofedUserId
|
||||
} from '../middleware/require-auth';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -185,7 +190,7 @@ router.get('/trending', async (req, res) => {
|
||||
res.json({ servers: enrichedResults, total: enrichedResults.length, limit });
|
||||
});
|
||||
|
||||
router.post('/', async (req, res) => {
|
||||
router.post('/', requireAuth, async (req, res) => {
|
||||
const {
|
||||
id: clientId,
|
||||
name,
|
||||
@@ -204,12 +209,17 @@ router.post('/', async (req, res) => {
|
||||
if (!name || !ownerId || !ownerPublicKey)
|
||||
return res.status(400).json({ error: 'Missing required fields' });
|
||||
|
||||
if (!rejectSpoofedUserId(req, res, ownerId, 'ownerId')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const authenticatedOwnerId = getAuthenticatedUserId(req);
|
||||
const passwordHash = passwordHashForInput(password);
|
||||
const server: ServerPayload = {
|
||||
id: clientId || uuidv4(),
|
||||
name,
|
||||
description,
|
||||
ownerId,
|
||||
ownerId: authenticatedOwnerId,
|
||||
ownerPublicKey,
|
||||
hasPassword: !!passwordHash,
|
||||
passwordHash,
|
||||
@@ -225,12 +235,12 @@ router.post('/', async (req, res) => {
|
||||
};
|
||||
|
||||
await upsertServer(server);
|
||||
await ensureServerMembership(server.id, ownerId);
|
||||
await ensureServerMembership(server.id, authenticatedOwnerId);
|
||||
|
||||
res.status(201).json(await enrichServer(server, getRequestOrigin(req)));
|
||||
});
|
||||
|
||||
router.put('/:id', async (req, res) => {
|
||||
router.put('/:id', requireAuth, async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const {
|
||||
currentOwnerId,
|
||||
@@ -242,13 +252,16 @@ router.put('/:id', async (req, res) => {
|
||||
...updates
|
||||
} = req.body;
|
||||
const existing = await getServerById(id);
|
||||
const authenticatedOwnerId = currentOwnerId ?? req.body.ownerId;
|
||||
const authenticatedOwnerId = getAuthenticatedUserId(req);
|
||||
|
||||
if (!existing)
|
||||
return res.status(404).json({ error: 'Server not found' });
|
||||
|
||||
if (!authenticatedOwnerId) {
|
||||
return res.status(400).json({ error: 'Missing currentOwnerId' });
|
||||
if (currentOwnerId && currentOwnerId !== authenticatedOwnerId) {
|
||||
return res.status(400).json({
|
||||
error: 'currentOwnerId must match the authenticated user',
|
||||
errorCode: 'USER_ID_MISMATCH'
|
||||
});
|
||||
}
|
||||
|
||||
if (!canManageServerUpdate(existing, authenticatedOwnerId, {
|
||||
@@ -276,18 +289,22 @@ router.put('/:id', async (req, res) => {
|
||||
res.json(await enrichServer(server, getRequestOrigin(req)));
|
||||
});
|
||||
|
||||
router.post('/:id/join', async (req, res) => {
|
||||
router.post('/:id/join', requireAuth, async (req, res) => {
|
||||
const { id: serverId } = req.params;
|
||||
const { userId, password, inviteId } = req.body;
|
||||
const authenticatedUserId = getAuthenticatedUserId(req);
|
||||
|
||||
if (!userId) {
|
||||
return res.status(400).json({ error: 'Missing userId', errorCode: 'MISSING_USER' });
|
||||
if (userId && userId !== authenticatedUserId) {
|
||||
return res.status(400).json({
|
||||
error: 'userId must match the authenticated user',
|
||||
errorCode: 'USER_ID_MISMATCH'
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await joinServerWithAccess({
|
||||
serverId,
|
||||
userId: String(userId),
|
||||
userId: authenticatedUserId,
|
||||
password: typeof password === 'string' ? password : undefined,
|
||||
inviteId: typeof inviteId === 'string' ? inviteId : undefined
|
||||
});
|
||||
@@ -305,12 +322,16 @@ router.post('/:id/join', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/:id/invites', async (req, res) => {
|
||||
router.post('/:id/invites', requireAuth, async (req, res) => {
|
||||
const { id: serverId } = req.params;
|
||||
const { requesterUserId, requesterDisplayName } = req.body;
|
||||
const authenticatedUserId = getAuthenticatedUserId(req);
|
||||
|
||||
if (!requesterUserId) {
|
||||
return res.status(400).json({ error: 'Missing requesterUserId', errorCode: 'MISSING_USER' });
|
||||
if (requesterUserId && requesterUserId !== authenticatedUserId) {
|
||||
return res.status(400).json({
|
||||
error: 'requesterUserId must match the authenticated user',
|
||||
errorCode: 'USER_ID_MISMATCH'
|
||||
});
|
||||
}
|
||||
|
||||
const server = await getServerById(serverId);
|
||||
@@ -322,7 +343,7 @@ router.post('/:id/invites', async (req, res) => {
|
||||
try {
|
||||
const invite = await createServerInvite(
|
||||
serverId,
|
||||
String(requesterUserId),
|
||||
authenticatedUserId,
|
||||
typeof requesterDisplayName === 'string' ? requesterDisplayName : undefined
|
||||
);
|
||||
|
||||
@@ -332,9 +353,10 @@ router.post('/:id/invites', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/:id/moderation/kick', async (req, res) => {
|
||||
router.post('/:id/moderation/kick', requireAuth, async (req, res) => {
|
||||
const { id: serverId } = req.params;
|
||||
const { actorUserId, targetUserId } = req.body;
|
||||
const authenticatedUserId = getAuthenticatedUserId(req);
|
||||
const server = await getServerById(serverId);
|
||||
|
||||
if (!server) {
|
||||
@@ -345,7 +367,14 @@ router.post('/:id/moderation/kick', async (req, res) => {
|
||||
return res.status(400).json({ error: 'Missing targetUserId', errorCode: 'MISSING_TARGET' });
|
||||
}
|
||||
|
||||
if (!canModerateServerMember(server, String(actorUserId || ''), String(targetUserId), 'kickMembers')) {
|
||||
if (actorUserId && actorUserId !== authenticatedUserId) {
|
||||
return res.status(400).json({
|
||||
error: 'actorUserId must match the authenticated user',
|
||||
errorCode: 'USER_ID_MISMATCH'
|
||||
});
|
||||
}
|
||||
|
||||
if (!canModerateServerMember(server, authenticatedUserId, String(targetUserId), 'kickMembers')) {
|
||||
return res.status(403).json({ error: 'Not authorized', errorCode: 'NOT_AUTHORIZED' });
|
||||
}
|
||||
|
||||
@@ -354,9 +383,10 @@ router.post('/:id/moderation/kick', async (req, res) => {
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
router.post('/:id/moderation/ban', async (req, res) => {
|
||||
router.post('/:id/moderation/ban', requireAuth, async (req, res) => {
|
||||
const { id: serverId } = req.params;
|
||||
const { actorUserId, targetUserId, banId, displayName, reason, expiresAt } = req.body;
|
||||
const authenticatedUserId = getAuthenticatedUserId(req);
|
||||
const server = await getServerById(serverId);
|
||||
|
||||
if (!server) {
|
||||
@@ -367,7 +397,14 @@ router.post('/:id/moderation/ban', async (req, res) => {
|
||||
return res.status(400).json({ error: 'Missing targetUserId', errorCode: 'MISSING_TARGET' });
|
||||
}
|
||||
|
||||
if (!canModerateServerMember(server, String(actorUserId || ''), String(targetUserId), 'banMembers')) {
|
||||
if (actorUserId && actorUserId !== authenticatedUserId) {
|
||||
return res.status(400).json({
|
||||
error: 'actorUserId must match the authenticated user',
|
||||
errorCode: 'USER_ID_MISMATCH'
|
||||
});
|
||||
}
|
||||
|
||||
if (!canModerateServerMember(server, authenticatedUserId, String(targetUserId), 'banMembers')) {
|
||||
return res.status(403).json({ error: 'Not authorized', errorCode: 'NOT_AUTHORIZED' });
|
||||
}
|
||||
|
||||
@@ -375,7 +412,7 @@ router.post('/:id/moderation/ban', async (req, res) => {
|
||||
serverId,
|
||||
userId: String(targetUserId),
|
||||
banId: typeof banId === 'string' ? banId : undefined,
|
||||
bannedBy: String(actorUserId || ''),
|
||||
bannedBy: authenticatedUserId,
|
||||
displayName: typeof displayName === 'string' ? displayName : undefined,
|
||||
reason: typeof reason === 'string' ? reason : undefined,
|
||||
expiresAt: typeof expiresAt === 'number' ? expiresAt : undefined
|
||||
@@ -384,16 +421,24 @@ router.post('/:id/moderation/ban', async (req, res) => {
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
router.post('/:id/moderation/unban', async (req, res) => {
|
||||
router.post('/:id/moderation/unban', requireAuth, async (req, res) => {
|
||||
const { id: serverId } = req.params;
|
||||
const { actorUserId, banId, targetUserId } = req.body;
|
||||
const authenticatedUserId = getAuthenticatedUserId(req);
|
||||
const server = await getServerById(serverId);
|
||||
|
||||
if (!server) {
|
||||
return res.status(404).json({ error: 'Server not found', errorCode: 'SERVER_NOT_FOUND' });
|
||||
}
|
||||
|
||||
if (!resolveServerPermission(server, String(actorUserId || ''), 'manageBans')) {
|
||||
if (actorUserId && actorUserId !== authenticatedUserId) {
|
||||
return res.status(400).json({
|
||||
error: 'actorUserId must match the authenticated user',
|
||||
errorCode: 'USER_ID_MISMATCH'
|
||||
});
|
||||
}
|
||||
|
||||
if (!resolveServerPermission(server, authenticatedUserId, 'manageBans')) {
|
||||
return res.status(403).json({ error: 'Not authorized', errorCode: 'NOT_AUTHORIZED' });
|
||||
}
|
||||
|
||||
@@ -406,25 +451,29 @@ router.post('/:id/moderation/unban', async (req, res) => {
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
router.post('/:id/leave', async (req, res) => {
|
||||
router.post('/:id/leave', requireAuth, async (req, res) => {
|
||||
const { id: serverId } = req.params;
|
||||
const { userId } = req.body;
|
||||
const authenticatedUserId = getAuthenticatedUserId(req);
|
||||
const server = await getServerById(serverId);
|
||||
|
||||
if (!server) {
|
||||
return res.status(404).json({ error: 'Server not found', errorCode: 'SERVER_NOT_FOUND' });
|
||||
}
|
||||
|
||||
if (!userId) {
|
||||
return res.status(400).json({ error: 'Missing userId', errorCode: 'MISSING_USER' });
|
||||
if (userId && userId !== authenticatedUserId) {
|
||||
return res.status(400).json({
|
||||
error: 'userId must match the authenticated user',
|
||||
errorCode: 'USER_ID_MISMATCH'
|
||||
});
|
||||
}
|
||||
|
||||
await leaveServerUser(serverId, String(userId));
|
||||
await leaveServerUser(serverId, authenticatedUserId);
|
||||
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
router.post('/:id/heartbeat', async (req, res) => {
|
||||
router.post('/:id/heartbeat', requireAuth, async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { currentUsers } = req.body;
|
||||
const existing = await getServerById(id);
|
||||
@@ -442,30 +491,38 @@ router.post('/:id/heartbeat', async (req, res) => {
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
router.delete('/:id', async (req, res) => {
|
||||
router.delete('/:id', requireAuth, async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { ownerId } = req.body;
|
||||
const authenticatedOwnerId = getAuthenticatedUserId(req);
|
||||
const existing = await getServerById(id);
|
||||
|
||||
if (!existing)
|
||||
return res.status(404).json({ error: 'Server not found' });
|
||||
|
||||
if (existing.ownerId !== ownerId)
|
||||
if (ownerId && ownerId !== authenticatedOwnerId) {
|
||||
return res.status(400).json({
|
||||
error: 'ownerId must match the authenticated user',
|
||||
errorCode: 'USER_ID_MISMATCH'
|
||||
});
|
||||
}
|
||||
|
||||
if (existing.ownerId !== authenticatedOwnerId)
|
||||
return res.status(403).json({ error: 'Not authorized' });
|
||||
|
||||
await deleteServer(id);
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
router.get('/:id/requests', async (req, res) => {
|
||||
router.get('/:id/requests', requireAuth, async (req, res) => {
|
||||
const { id: serverId } = req.params;
|
||||
const { ownerId } = req.query;
|
||||
const authenticatedUserId = getAuthenticatedUserId(req);
|
||||
const server = await getServerById(serverId);
|
||||
|
||||
if (!server)
|
||||
return res.status(404).json({ error: 'Server not found' });
|
||||
|
||||
if (server.ownerId !== ownerId)
|
||||
if (!resolveServerPermission(server, authenticatedUserId, 'manageServer'))
|
||||
return res.status(403).json({ error: 'Not authorized' });
|
||||
|
||||
const requests = await getPendingRequestsForServer(serverId);
|
||||
|
||||
@@ -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