Files
motovaultpro/backend/src/core/plugins/admin-guard.plugin.ts
Eric Gullickson 1321440cd0 chore: update auth plugin and admin guard for UUID (refs #212)
Auth plugin now uses profile.id (UUID) as userContext.userId instead
of raw JWT sub. Admin guard queries admin_users by user_profile_id.
Auth0 Management API calls continue using auth0Sub from JWT.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 09:36:32 -06:00

104 lines
3.2 KiB
TypeScript

/**
* @ai-summary Fastify admin authorization plugin
* @ai-context Checks if authenticated user is an admin and enforces access control
*/
import { FastifyPluginAsync, FastifyRequest, FastifyReply, FastifyInstance } from 'fastify';
import fp from 'fastify-plugin';
import { Pool } from 'pg';
import { logger } from '../logging/logger';
declare module 'fastify' {
interface FastifyInstance {
requireAdmin: (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
}
}
// Store pool reference for use in handler
let dbPool: Pool | null = null;
export function setAdminGuardPool(pool: Pool): void {
dbPool = pool;
}
const adminGuardPlugin: FastifyPluginAsync = async (fastify) => {
// Decorate with requireAdmin function that enforces admin authorization
fastify.decorate('requireAdmin', async function(this: FastifyInstance, request: FastifyRequest, reply: FastifyReply) {
try {
if (typeof this.authenticate !== 'function') {
logger.error('Admin guard: authenticate handler missing');
return reply.code(500).send({
error: 'Internal server error',
message: 'Authentication handler missing'
});
}
await this.authenticate(request, reply);
if (reply.sent) {
return;
}
// Ensure user is authenticated first
if (!request.userContext?.userId) {
logger.warn('Admin guard: user context missing');
return reply.code(401).send({
error: 'Unauthorized',
message: 'Authentication required'
});
}
// If pool not initialized, return 500
if (!dbPool) {
logger.error('Admin guard: database pool not initialized');
return reply.code(500).send({
error: 'Internal server error',
message: 'Admin check unavailable'
});
}
// Check if user is in admin_users table and not revoked
const query = `
SELECT id, user_profile_id, email, role, revoked_at
FROM admin_users
WHERE user_profile_id = $1 AND revoked_at IS NULL
LIMIT 1
`;
const result = await dbPool.query(query, [request.userContext.userId]);
if (result.rows.length === 0) {
logger.warn('Admin guard: user is not authorized as admin', {
userId: request.userContext.userId?.substring(0, 8) + '...'
});
return reply.code(403).send({
error: 'Forbidden',
message: 'Admin access required'
});
}
// Set admin flag in userContext
request.userContext.isAdmin = true;
request.userContext.adminRecord = result.rows[0];
logger.info('Admin guard: admin authorization successful', {
userId: request.userContext.userId?.substring(0, 8) + '...',
role: result.rows[0].role
});
} catch (error) {
logger.error('Admin guard: authorization check failed', {
error: error instanceof Error ? error.message : 'Unknown error',
userId: request.userContext?.userId?.substring(0, 8) + '...'
});
return reply.code(500).send({
error: 'Internal server error',
message: 'Admin check failed'
});
}
});
};
export default fp(adminGuardPlugin, {
name: 'admin-guard-plugin'
});