Admin User v1

This commit is contained in:
Eric Gullickson
2025-11-05 19:04:06 -06:00
parent e4e7e32a4f
commit 8174e0d5f9
48 changed files with 11289 additions and 1112 deletions

View File

@@ -0,0 +1,90 @@
/**
* @ai-summary Fastify admin authorization plugin
* @ai-context Checks if authenticated user is an admin and enforces access control
*/
import { FastifyPluginAsync, FastifyRequest, FastifyReply } 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(request: FastifyRequest, reply: FastifyReply) {
try {
// 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 auth0_sub, email, role, revoked_at
FROM admin_users
WHERE auth0_sub = $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'
});

View File

@@ -15,6 +15,12 @@ declare module 'fastify' {
interface FastifyRequest {
jwtVerify(): Promise<void>;
user?: any;
userContext?: {
userId: string;
email?: string;
isAdmin: boolean;
adminRecord?: any;
};
}
}
@@ -68,9 +74,17 @@ const authPlugin: FastifyPluginAsync = async (fastify) => {
fastify.decorate('authenticate', async function(request: FastifyRequest, reply: FastifyReply) {
try {
await request.jwtVerify();
// Hydrate userContext with basic auth info
const userId = request.user?.sub;
request.userContext = {
userId,
email: request.user?.email,
isAdmin: false, // Default to false; admin status checked by admin guard
};
logger.info('JWT authentication successful', {
userId: request.user?.sub?.substring(0, 8) + '...',
userId: userId?.substring(0, 8) + '...',
audience: auth0Config.audience
});
} catch (error) {
@@ -79,10 +93,10 @@ const authPlugin: FastifyPluginAsync = async (fastify) => {
method: request.method,
error: error instanceof Error ? error.message : 'Unknown error',
});
reply.code(401).send({
error: 'Unauthorized',
message: 'Invalid or missing JWT token'
reply.code(401).send({
error: 'Unauthorized',
message: 'Invalid or missing JWT token'
});
}
});