Admin settings fixed

This commit is contained in:
Eric Gullickson
2025-11-06 14:07:16 -06:00
parent 8174e0d5f9
commit 858cf31d38
3 changed files with 119 additions and 104 deletions

View File

@@ -35,11 +35,15 @@ export class AdminController {
const userId = request.userContext?.userId;
const userEmail = this.resolveUserEmail(request);
console.log('[DEBUG] Admin verify - userId:', userId);
console.log('[DEBUG] Admin verify - userEmail:', userEmail);
if (userEmail && request.userContext) {
request.userContext.email = userEmail.toLowerCase();
}
if (!userId && !userEmail) {
console.log('[DEBUG] Admin verify - No userId or userEmail, returning 401');
return reply.code(401).send({
error: 'Unauthorized',
message: 'User context missing'
@@ -50,15 +54,26 @@ export class AdminController {
? await this.adminService.getAdminByAuth0Sub(userId)
: null;
console.log('[DEBUG] Admin verify - adminRecord by auth0Sub:', adminRecord ? 'FOUND' : 'NOT FOUND');
// Fallback: attempt to resolve admin by email for legacy records
if (!adminRecord && userEmail) {
const emailMatch = await this.adminService.getAdminByEmail(userEmail.toLowerCase());
console.log('[DEBUG] Admin verify - emailMatch:', emailMatch ? 'FOUND' : 'NOT FOUND');
if (emailMatch) {
console.log('[DEBUG] Admin verify - emailMatch.auth0Sub:', emailMatch.auth0Sub);
console.log('[DEBUG] Admin verify - emailMatch.revokedAt:', emailMatch.revokedAt);
}
if (emailMatch && !emailMatch.revokedAt) {
// If the stored auth0Sub differs, link it to the authenticated user
if (userId && emailMatch.auth0Sub !== userId) {
console.log('[DEBUG] Admin verify - Calling linkAdminAuth0Sub to update auth0Sub');
adminRecord = await this.adminService.linkAdminAuth0Sub(userEmail, userId);
console.log('[DEBUG] Admin verify - adminRecord after link:', adminRecord ? 'SUCCESS' : 'FAILED');
} else {
console.log('[DEBUG] Admin verify - Using emailMatch as adminRecord');
adminRecord = emailMatch;
}
}
@@ -70,6 +85,7 @@ export class AdminController {
request.userContext.adminRecord = adminRecord;
}
console.log('[DEBUG] Admin verify - Returning isAdmin: true');
// User is an active admin
return reply.code(200).send({
isAdmin: true,
@@ -86,12 +102,14 @@ export class AdminController {
request.userContext.adminRecord = undefined;
}
console.log('[DEBUG] Admin verify - Returning isAdmin: false');
// User is not an admin
return reply.code(200).send({
isAdmin: false,
adminRecord: null
});
} catch (error) {
console.log('[DEBUG] Admin verify - Error caught:', error instanceof Error ? error.message : 'Unknown error');
logger.error('Error verifying admin access', {
error: error instanceof Error ? error.message : 'Unknown error',
userId: request.userContext?.userId?.substring(0, 8) + '...'
@@ -381,6 +399,9 @@ export class AdminController {
}
private resolveUserEmail(request: FastifyRequest): string | undefined {
console.log('[DEBUG] resolveUserEmail - request.userContext:', JSON.stringify(request.userContext, null, 2));
console.log('[DEBUG] resolveUserEmail - request.user:', JSON.stringify((request as any).user, null, 2));
const candidates: Array<string | undefined> = [
request.userContext?.email,
(request as any).user?.email,
@@ -389,11 +410,15 @@ export class AdminController {
(request as any).user?.preferred_username,
];
console.log('[DEBUG] resolveUserEmail - candidates:', candidates);
for (const value of candidates) {
if (typeof value === 'string' && value.includes('@')) {
console.log('[DEBUG] resolveUserEmail - found email:', value);
return value.trim();
}
}
console.log('[DEBUG] resolveUserEmail - no email found');
return undefined;
}
}

View File

@@ -1,123 +1,100 @@
/**
* @ai-summary Admin guard plugin unit tests
* @ai-context Tests authorization logic for admin-only routes
*/
import { FastifyRequest, FastifyReply } from 'fastify';
import Fastify, { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
import { Pool } from 'pg';
import { setAdminGuardPool } from '../../../../core/plugins/admin-guard.plugin';
import adminGuardPlugin, { setAdminGuardPool } from '../../../../core/plugins/admin-guard.plugin';
describe('Admin Guard', () => {
let mockPool: Pool;
let mockRequest: Partial<FastifyRequest>;
let mockReply: Partial<FastifyReply>;
const createReply = (): Partial<FastifyReply> & { payload?: unknown; statusCode?: number } => {
return {
sent: false,
code: jest.fn(function(this: any, status: number) {
this.statusCode = status;
return this;
}),
send: jest.fn(function(this: any, payload: unknown) {
this.payload = payload;
this.sent = true;
return this;
}),
};
};
beforeEach(() => {
// Mock database pool
mockPool = {
query: jest.fn(),
} as unknown as Pool;
describe('admin guard plugin', () => {
let fastify: FastifyInstance;
let authenticateMock: jest.Mock;
let mockPool: { query: jest.Mock };
// Mock reply methods
mockReply = {
code: jest.fn().mockReturnThis(),
send: jest.fn().mockReturnThis(),
};
});
describe('Authorization checks', () => {
it('should reject request without user context', async () => {
mockRequest = {
userContext: undefined,
};
const requireAdmin = jest.fn();
// Test would call requireAdmin and verify 401 response
});
it('should reject non-admin users', async () => {
mockRequest = {
userContext: {
userId: 'auth0|123456',
isAdmin: false,
},
};
// Test database query returns no admin record
(mockPool.query as jest.Mock).mockResolvedValue({ rows: [] });
// Test would call requireAdmin and verify 403 response
});
it('should accept active admin users', async () => {
mockRequest = {
userContext: {
userId: 'auth0|123456',
isAdmin: false,
},
};
const adminRecord = {
auth0_sub: 'auth0|123456',
beforeEach(async () => {
fastify = Fastify();
authenticateMock = jest.fn(async (request: FastifyRequest) => {
request.userContext = {
userId: 'auth0|admin',
email: 'admin@motovaultpro.com',
role: 'admin',
revoked_at: null,
isAdmin: false,
};
(mockPool.query as jest.Mock).mockResolvedValue({ rows: [adminRecord] });
// Test would call requireAdmin and verify isAdmin set to true
});
fastify.decorate('authenticate', authenticateMock);
it('should reject revoked admin users', async () => {
mockRequest = {
userContext: {
userId: 'auth0|123456',
isAdmin: false,
},
};
await fastify.register(adminGuardPlugin);
// Test database query returns no rows (admin is revoked)
(mockPool.query as jest.Mock).mockResolvedValue({ rows: [] });
mockPool = {
query: jest.fn().mockResolvedValue({
rows: [{
auth0_sub: 'auth0|admin',
email: 'admin@motovaultpro.com',
role: 'admin',
revoked_at: null,
}],
}),
};
// Test would call requireAdmin and verify 403 response
});
setAdminGuardPool(mockPool as unknown as Pool);
});
it('should handle database errors gracefully', async () => {
mockRequest = {
userContext: {
userId: 'auth0|123456',
isAdmin: false,
},
};
afterEach(async () => {
await fastify.close();
setAdminGuardPool(null as unknown as Pool);
jest.clearAllMocks();
});
const dbError = new Error('Database connection failed');
(mockPool.query as jest.Mock).mockRejectedValue(dbError);
it('authenticates the request before enforcing admin access', async () => {
const request = {} as FastifyRequest;
const reply = createReply();
// Test would call requireAdmin and verify 500 response
await fastify.requireAdmin(request, reply as FastifyReply);
expect(authenticateMock).toHaveBeenCalledTimes(1);
expect(mockPool.query).toHaveBeenCalledTimes(1);
expect(request.userContext?.isAdmin).toBe(true);
expect(reply.code).not.toHaveBeenCalled();
expect(reply.send).not.toHaveBeenCalled();
});
it('rejects non-admin users with 403', async () => {
mockPool.query.mockResolvedValue({ rows: [] });
const request = {} as FastifyRequest;
const reply = createReply();
await fastify.requireAdmin(request, reply as FastifyReply);
expect(authenticateMock).toHaveBeenCalledTimes(1);
expect(mockPool.query).toHaveBeenCalledTimes(1);
expect(reply.code).toHaveBeenCalledWith(403);
expect(reply.send).toHaveBeenCalledWith({
error: 'Forbidden',
message: 'Admin access required',
});
});
describe('Pool management', () => {
it('should set and use database pool for queries', () => {
const testPool = {} as Pool;
setAdminGuardPool(testPool);
it('responds with 500 when database pool is not initialized', async () => {
setAdminGuardPool(null as unknown as Pool);
const request = {} as FastifyRequest;
const reply = createReply();
// Pool should be available for guards to use
});
await fastify.requireAdmin(request, reply as FastifyReply);
it('should handle missing pool gracefully', async () => {
// Reset pool to null
setAdminGuardPool(null as any);
mockRequest = {
userContext: {
userId: 'auth0|123456',
isAdmin: false,
},
};
// Test would call requireAdmin and verify 500 response for missing pool
expect(reply.code).toHaveBeenCalledWith(500);
expect(reply.send).toHaveBeenCalledWith({
error: 'Internal server error',
message: 'Admin check unavailable',
});
});
});