2026-06-26 10:50:24 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* Users Routes – Admin user management
|
|
|
|
|
|
*/
|
2026-06-30 11:32:33 +02:00
|
|
|
|
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
|
2026-06-26 10:50:24 +02:00
|
|
|
|
import type { DatabaseInterface, DBUser } from '../database/DatabaseInterface.js';
|
|
|
|
|
|
import type { AuthService } from '../auth/AuthService.js';
|
|
|
|
|
|
|
2026-06-30 11:32:33 +02:00
|
|
|
|
function extractToken(request: FastifyRequest): string | null {
|
|
|
|
|
|
const auth = request.headers?.authorization;
|
|
|
|
|
|
if (auth && auth.startsWith('Bearer ')) {
|
|
|
|
|
|
return auth.slice(7);
|
|
|
|
|
|
}
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function requireAdmin(request: FastifyRequest, reply: FastifyReply, authService: AuthService): DBUser | null {
|
|
|
|
|
|
const token = extractToken(request);
|
|
|
|
|
|
if (!token) {
|
|
|
|
|
|
reply.code(401).send({ error: 'Authentication required' });
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
const user = authService.getUserFromSession(token);
|
|
|
|
|
|
if (!user) {
|
|
|
|
|
|
reply.code(401).send({ error: 'Invalid or expired session' });
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (user.role !== 'admin') {
|
|
|
|
|
|
reply.code(403).send({ error: 'Admin access required' });
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
return user;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-26 10:50:24 +02:00
|
|
|
|
export function registerUserRoutes(fastify: FastifyInstance, db: DatabaseInterface, authService: AuthService) {
|
2026-06-30 11:32:33 +02:00
|
|
|
|
// List all users (admin only)
|
|
|
|
|
|
fastify.get('/api/users', async (request, reply) => {
|
|
|
|
|
|
if (!requireAdmin(request, reply, authService)) return;
|
2026-06-26 10:50:24 +02:00
|
|
|
|
const users = db.listUsers();
|
|
|
|
|
|
return users.map(({ password_hash, ...safe }) => safe);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-06-30 11:32:33 +02:00
|
|
|
|
// Get single user (admin only)
|
2026-06-26 10:50:24 +02:00
|
|
|
|
fastify.get('/api/users/:id', async (request, reply) => {
|
2026-06-30 11:32:33 +02:00
|
|
|
|
if (!requireAdmin(request, reply, authService)) return;
|
2026-06-26 10:50:24 +02:00
|
|
|
|
const { id } = request.params as { id: string };
|
|
|
|
|
|
const user = db.getUser(id);
|
|
|
|
|
|
if (!user) return reply.code(404).send({ error: 'User not found' });
|
|
|
|
|
|
const { password_hash, ...safe } = user;
|
|
|
|
|
|
return safe;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-06-30 11:32:33 +02:00
|
|
|
|
// Update user (admin only)
|
2026-06-26 10:50:24 +02:00
|
|
|
|
fastify.patch('/api/users/:id', async (request, reply) => {
|
2026-06-30 11:32:33 +02:00
|
|
|
|
if (!requireAdmin(request, reply, authService)) return;
|
2026-06-26 10:50:24 +02:00
|
|
|
|
const { id } = request.params as { id: string };
|
|
|
|
|
|
const body = request.body as Partial<DBUser>;
|
|
|
|
|
|
// Prevent password update via this endpoint
|
|
|
|
|
|
delete body.password_hash;
|
|
|
|
|
|
const updated = db.updateUser(id, body);
|
|
|
|
|
|
if (!updated) return reply.code(404).send({ error: 'User not found' });
|
|
|
|
|
|
const { password_hash, ...safe } = updated;
|
|
|
|
|
|
return safe;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-06-30 11:32:33 +02:00
|
|
|
|
// Delete user (admin only)
|
2026-06-26 10:50:24 +02:00
|
|
|
|
fastify.delete('/api/users/:id', async (request, reply) => {
|
2026-06-30 11:32:33 +02:00
|
|
|
|
if (!requireAdmin(request, reply, authService)) return;
|
2026-06-26 10:50:24 +02:00
|
|
|
|
const { id } = request.params as { id: string };
|
|
|
|
|
|
const ok = db.deleteUser(id);
|
|
|
|
|
|
if (!ok) return reply.code(404).send({ error: 'User not found' });
|
|
|
|
|
|
return reply.code(204).send();
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|