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>
104 lines
3.2 KiB
TypeScript
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'
|
|
});
|