diff --git a/backend/src/core/middleware/require-tier.test.ts b/backend/src/core/middleware/require-tier.test.ts index 13fc80f..cdd32cf 100644 --- a/backend/src/core/middleware/require-tier.test.ts +++ b/backend/src/core/middleware/require-tier.test.ts @@ -17,7 +17,7 @@ const createRequest = (subscriptionTier?: string): Partial => { } return { userContext: { - userId: 'auth0|user123456789', + userId: '550e8400-e29b-41d4-a716-446655440000', email: 'user@example.com', emailVerified: true, onboardingCompleted: true, diff --git a/backend/src/core/plugins/tests/tier-guard.plugin.test.ts b/backend/src/core/plugins/tests/tier-guard.plugin.test.ts index de24c75..f7a754b 100644 --- a/backend/src/core/plugins/tests/tier-guard.plugin.test.ts +++ b/backend/src/core/plugins/tests/tier-guard.plugin.test.ts @@ -26,7 +26,7 @@ describe('tier guard plugin', () => { // Mock authenticate to set userContext authenticateMock = jest.fn(async (request: FastifyRequest) => { request.userContext = { - userId: 'auth0|user123', + userId: '550e8400-e29b-41d4-a716-446655440000', email: 'user@example.com', emailVerified: true, onboardingCompleted: true, @@ -48,7 +48,7 @@ describe('tier guard plugin', () => { it('allows access when user tier meets minimum', async () => { authenticateMock.mockImplementation(async (request: FastifyRequest) => { request.userContext = { - userId: 'auth0|user123', + userId: '550e8400-e29b-41d4-a716-446655440000', email: 'user@example.com', emailVerified: true, onboardingCompleted: true, @@ -71,7 +71,7 @@ describe('tier guard plugin', () => { it('allows access when user tier exceeds minimum', async () => { authenticateMock.mockImplementation(async (request: FastifyRequest) => { request.userContext = { - userId: 'auth0|user123', + userId: '550e8400-e29b-41d4-a716-446655440000', email: 'user@example.com', emailVerified: true, onboardingCompleted: true, @@ -130,7 +130,7 @@ describe('tier guard plugin', () => { it('allows pro tier access to pro feature', async () => { authenticateMock.mockImplementation(async (request: FastifyRequest) => { request.userContext = { - userId: 'auth0|user123', + userId: '550e8400-e29b-41d4-a716-446655440000', email: 'user@example.com', emailVerified: true, onboardingCompleted: true, diff --git a/backend/src/features/admin/tests/integration/admin.integration.test.ts b/backend/src/features/admin/tests/integration/admin.integration.test.ts index 377619d..a7e03bf 100644 --- a/backend/src/features/admin/tests/integration/admin.integration.test.ts +++ b/backend/src/features/admin/tests/integration/admin.integration.test.ts @@ -4,18 +4,19 @@ */ import request from 'supertest'; -import { app } from '../../../../app'; +import { buildApp } from '../../../../app'; import pool from '../../../../core/config/database'; +import { FastifyInstance } from 'fastify'; import { readFileSync } from 'fs'; import { join } from 'path'; import fastifyPlugin from 'fastify-plugin'; import { setAdminGuardPool } from '../../../../core/plugins/admin-guard.plugin'; -const DEFAULT_ADMIN_SUB = 'test-admin-123'; +const DEFAULT_ADMIN_ID = '7c9e6679-7425-40de-944b-e07fc1f90ae7'; const DEFAULT_ADMIN_EMAIL = 'test-admin@motovaultpro.com'; let currentUser = { - sub: DEFAULT_ADMIN_SUB, + sub: 'auth0|test-admin-123', email: DEFAULT_ADMIN_EMAIL, }; @@ -25,11 +26,15 @@ jest.mock('../../../../core/plugins/auth.plugin', () => { default: fastifyPlugin(async function(fastify) { fastify.decorate('authenticate', async function(request, _reply) { // Inject dynamic test user context + // JWT sub is still auth0|xxx format request.user = { sub: currentUser.sub }; request.userContext = { - userId: currentUser.sub, + userId: DEFAULT_ADMIN_ID, email: currentUser.email, + emailVerified: true, + onboardingCompleted: true, isAdmin: false, // Will be set by admin guard + subscriptionTier: 'free', }; }); }, { name: 'auth-plugin' }) @@ -37,10 +42,14 @@ jest.mock('../../../../core/plugins/auth.plugin', () => { }); describe('Admin Management Integration Tests', () => { - let testAdminAuth0Sub: string; - let testNonAdminAuth0Sub: string; + let app: FastifyInstance; + let testAdminId: string; beforeAll(async () => { + // Build the app + app = await buildApp(); + await app.ready(); + // Run the admin migration directly using the migration file const migrationFile = join(__dirname, '../../migrations/001_create_admin_users.sql'); const migrationSQL = readFileSync(migrationFile, 'utf-8'); @@ -50,33 +59,31 @@ describe('Admin Management Integration Tests', () => { setAdminGuardPool(pool); // Create test admin user - testAdminAuth0Sub = DEFAULT_ADMIN_SUB; + testAdminId = DEFAULT_ADMIN_ID; await pool.query(` - INSERT INTO admin_users (auth0_sub, email, role, created_by) - VALUES ($1, $2, $3, $4) - ON CONFLICT (auth0_sub) DO NOTHING - `, [testAdminAuth0Sub, DEFAULT_ADMIN_EMAIL, 'admin', 'system']); - - // Create test non-admin auth0Sub for permission tests - testNonAdminAuth0Sub = 'test-non-admin-456'; + INSERT INTO admin_users (id, user_profile_id, email, role, created_by) + VALUES ($1, $2, $3, $4, $5) + ON CONFLICT (user_profile_id) DO NOTHING + `, [testAdminId, testAdminId, DEFAULT_ADMIN_EMAIL, 'admin', 'system']); }); afterAll(async () => { // Clean up test database await pool.query('DROP TABLE IF EXISTS admin_audit_logs CASCADE'); await pool.query('DROP TABLE IF EXISTS admin_users CASCADE'); + await app.close(); await pool.end(); }); beforeEach(async () => { // Clean up test data before each test (except the test admin) await pool.query( - 'DELETE FROM admin_users WHERE auth0_sub != $1 AND auth0_sub != $2', - [testAdminAuth0Sub, 'system|bootstrap'] + 'DELETE FROM admin_users WHERE user_profile_id != $1', + [testAdminId] ); await pool.query('DELETE FROM admin_audit_logs'); currentUser = { - sub: DEFAULT_ADMIN_SUB, + sub: 'auth0|test-admin-123', email: DEFAULT_ADMIN_EMAIL, }; }); @@ -85,11 +92,11 @@ describe('Admin Management Integration Tests', () => { it('should reject non-admin user trying to list admins', async () => { // Create mock for non-admin user currentUser = { - sub: testNonAdminAuth0Sub, + sub: 'auth0|test-non-admin-456', email: 'test-user@example.com', }; - const response = await request(app) + const response = await request(app.server) .get('/api/admin/admins') .expect(403); @@ -101,51 +108,51 @@ describe('Admin Management Integration Tests', () => { describe('GET /api/admin/verify', () => { it('should confirm admin access for existing admin', async () => { currentUser = { - sub: testAdminAuth0Sub, + sub: 'auth0|test-admin-123', email: DEFAULT_ADMIN_EMAIL, }; - const response = await request(app) + const response = await request(app.server) .get('/api/admin/verify') .expect(200); expect(response.body.isAdmin).toBe(true); expect(response.body.adminRecord).toMatchObject({ - auth0Sub: testAdminAuth0Sub, + id: testAdminId, email: DEFAULT_ADMIN_EMAIL, }); }); - it('should link admin record by email when auth0_sub differs', async () => { - const placeholderSub = 'auth0|placeholder-sub'; - const realSub = 'auth0|real-admin-sub'; + it('should link admin record by email when user_profile_id differs', async () => { + const placeholderId = '9b9a1234-1234-1234-1234-123456789abc'; + const realId = 'a1b2c3d4-5678-90ab-cdef-123456789def'; const email = 'link-admin@example.com'; await pool.query(` - INSERT INTO admin_users (auth0_sub, email, role, created_by) - VALUES ($1, $2, $3, $4) - `, [placeholderSub, email, 'admin', testAdminAuth0Sub]); + INSERT INTO admin_users (id, user_profile_id, email, role, created_by) + VALUES ($1, $2, $3, $4, $5) + `, [placeholderId, placeholderId, email, 'admin', testAdminId]); currentUser = { - sub: realSub, + sub: 'auth0|real-admin-sub', email, }; - const response = await request(app) + const response = await request(app.server) .get('/api/admin/verify') .expect(200); expect(response.body.isAdmin).toBe(true); expect(response.body.adminRecord).toMatchObject({ - auth0Sub: realSub, + userProfileId: realId, email, }); const record = await pool.query( - 'SELECT auth0_sub FROM admin_users WHERE email = $1', + 'SELECT user_profile_id FROM admin_users WHERE email = $1', [email] ); - expect(record.rows[0].auth0_sub).toBe(realSub); + expect(record.rows[0].user_profile_id).toBe(realId); }); it('should return non-admin response for unknown user', async () => { @@ -154,7 +161,7 @@ describe('Admin Management Integration Tests', () => { email: 'non-admin@example.com', }; - const response = await request(app) + const response = await request(app.server) .get('/api/admin/verify') .expect(200); @@ -166,17 +173,19 @@ describe('Admin Management Integration Tests', () => { describe('GET /api/admin/admins', () => { it('should list all admin users', async () => { // Create additional test admins + const admin1Id = '6ba7b810-9dad-11d1-80b4-00c04fd430c8'; + const admin2Id = '8f14e45f-ceea-367f-a27f-c9a6d0c67e0e'; await pool.query(` - INSERT INTO admin_users (auth0_sub, email, role, created_by) + INSERT INTO admin_users (id, user_profile_id, email, role, created_by) VALUES - ($1, $2, $3, $4), - ($5, $6, $7, $8) + ($1, $2, $3, $4, $5), + ($6, $7, $8, $9, $10) `, [ - 'auth0|admin1', 'admin1@example.com', 'admin', testAdminAuth0Sub, - 'auth0|admin2', 'admin2@example.com', 'super_admin', testAdminAuth0Sub + admin1Id, admin1Id, 'admin1@example.com', 'admin', testAdminId, + admin2Id, admin2Id, 'admin2@example.com', 'super_admin', testAdminId ]); - const response = await request(app) + const response = await request(app.server) .get('/api/admin/admins') .expect(200); @@ -184,7 +193,7 @@ describe('Admin Management Integration Tests', () => { expect(response.body).toHaveProperty('admins'); expect(response.body.admins.length).toBeGreaterThanOrEqual(3); // At least test admin + 2 created expect(response.body.admins[0]).toMatchObject({ - auth0Sub: expect.any(String), + id: expect.any(String), email: expect.any(String), role: expect.stringMatching(/^(admin|super_admin)$/), createdAt: expect.any(String), @@ -194,12 +203,13 @@ describe('Admin Management Integration Tests', () => { it('should include revoked admins in the list', async () => { // Create and revoke an admin + const revokedId = 'f1e2d3c4-b5a6-9788-6543-210fedcba987'; await pool.query(` - INSERT INTO admin_users (auth0_sub, email, role, created_by, revoked_at) - VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP) - `, ['auth0|revoked', 'revoked@example.com', 'admin', testAdminAuth0Sub]); + INSERT INTO admin_users (id, user_profile_id, email, role, created_by, revoked_at) + VALUES ($1, $2, $3, $4, $5, CURRENT_TIMESTAMP) + `, [revokedId, revokedId, 'revoked@example.com', 'admin', testAdminId]); - const response = await request(app) + const response = await request(app.server) .get('/api/admin/admins') .expect(200); @@ -218,17 +228,17 @@ describe('Admin Management Integration Tests', () => { role: 'admin' }; - const response = await request(app) + const response = await request(app.server) .post('/api/admin/admins') .send(newAdminData) .expect(201); expect(response.body).toMatchObject({ - auth0Sub: expect.any(String), + id: expect.any(String), email: 'newadmin@example.com', role: 'admin', createdAt: expect.any(String), - createdBy: testAdminAuth0Sub, + createdBy: testAdminId, revokedAt: null }); @@ -238,7 +248,7 @@ describe('Admin Management Integration Tests', () => { ['CREATE', 'newadmin@example.com'] ); expect(auditResult.rows.length).toBe(1); - expect(auditResult.rows[0].actor_admin_id).toBe(testAdminAuth0Sub); + expect(auditResult.rows[0].actor_admin_id).toBe(testAdminId); }); it('should reject invalid email', async () => { @@ -247,7 +257,7 @@ describe('Admin Management Integration Tests', () => { role: 'admin' }; - const response = await request(app) + const response = await request(app.server) .post('/api/admin/admins') .send(invalidData) .expect(400); @@ -263,13 +273,13 @@ describe('Admin Management Integration Tests', () => { }; // Create first admin - await request(app) + await request(app.server) .post('/api/admin/admins') .send(adminData) .expect(201); // Try to create duplicate - const response = await request(app) + const response = await request(app.server) .post('/api/admin/admins') .send(adminData) .expect(400); @@ -284,7 +294,7 @@ describe('Admin Management Integration Tests', () => { role: 'super_admin' }; - const response = await request(app) + const response = await request(app.server) .post('/api/admin/admins') .send(superAdminData) .expect(201); @@ -297,7 +307,7 @@ describe('Admin Management Integration Tests', () => { email: 'defaultrole@example.com' }; - const response = await request(app) + const response = await request(app.server) .post('/api/admin/admins') .send(adminData) .expect(201); @@ -306,23 +316,24 @@ describe('Admin Management Integration Tests', () => { }); }); - describe('PATCH /api/admin/admins/:auth0Sub/revoke', () => { + describe('PATCH /api/admin/admins/:id/revoke', () => { it('should revoke admin access', async () => { // Create admin to revoke + const toRevokeId = 'b1c2d3e4-f5a6-7890-1234-567890abcdef'; const createResult = await pool.query(` - INSERT INTO admin_users (auth0_sub, email, role, created_by) - VALUES ($1, $2, $3, $4) - RETURNING auth0_sub - `, ['auth0|to-revoke', 'torevoke@example.com', 'admin', testAdminAuth0Sub]); + INSERT INTO admin_users (id, user_profile_id, email, role, created_by) + VALUES ($1, $2, $3, $4, $5) + RETURNING id + `, [toRevokeId, toRevokeId, 'torevoke@example.com', 'admin', testAdminId]); - const auth0Sub = createResult.rows[0].auth0_sub; + const adminId = createResult.rows[0].id; - const response = await request(app) - .patch(`/api/admin/admins/${auth0Sub}/revoke`) + const response = await request(app.server) + .patch(`/api/admin/admins/${adminId}/revoke`) .expect(200); expect(response.body).toMatchObject({ - auth0Sub, + id: adminId, email: 'torevoke@example.com', revokedAt: expect.any(String) }); @@ -330,7 +341,7 @@ describe('Admin Management Integration Tests', () => { // Verify audit log const auditResult = await pool.query( 'SELECT * FROM admin_audit_logs WHERE action = $1 AND target_admin_id = $2', - ['REVOKE', auth0Sub] + ['REVOKE', adminId] ); expect(auditResult.rows.length).toBe(1); }); @@ -338,12 +349,12 @@ describe('Admin Management Integration Tests', () => { it('should prevent revoking last active admin', async () => { // First, ensure only one active admin exists await pool.query( - 'UPDATE admin_users SET revoked_at = CURRENT_TIMESTAMP WHERE auth0_sub != $1', - [testAdminAuth0Sub] + 'UPDATE admin_users SET revoked_at = CURRENT_TIMESTAMP WHERE user_profile_id != $1', + [testAdminId] ); - const response = await request(app) - .patch(`/api/admin/admins/${testAdminAuth0Sub}/revoke`) + const response = await request(app.server) + .patch(`/api/admin/admins/${testAdminId}/revoke`) .expect(400); expect(response.body.error).toBe('Bad Request'); @@ -351,8 +362,8 @@ describe('Admin Management Integration Tests', () => { }); it('should return 404 for non-existent admin', async () => { - const response = await request(app) - .patch('/api/admin/admins/auth0|nonexistent/revoke') + const response = await request(app.server) + .patch('/api/admin/admins/00000000-0000-0000-0000-000000000000/revoke') .expect(404); expect(response.body.error).toBe('Not Found'); @@ -360,23 +371,24 @@ describe('Admin Management Integration Tests', () => { }); }); - describe('PATCH /api/admin/admins/:auth0Sub/reinstate', () => { + describe('PATCH /api/admin/admins/:id/reinstate', () => { it('should reinstate revoked admin', async () => { // Create revoked admin + const reinstateId = 'c2d3e4f5-a6b7-8901-2345-678901bcdef0'; const createResult = await pool.query(` - INSERT INTO admin_users (auth0_sub, email, role, created_by, revoked_at) - VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP) - RETURNING auth0_sub - `, ['auth0|to-reinstate', 'toreinstate@example.com', 'admin', testAdminAuth0Sub]); + INSERT INTO admin_users (id, user_profile_id, email, role, created_by, revoked_at) + VALUES ($1, $2, $3, $4, $5, CURRENT_TIMESTAMP) + RETURNING id + `, [reinstateId, reinstateId, 'toreinstate@example.com', 'admin', testAdminId]); - const auth0Sub = createResult.rows[0].auth0_sub; + const adminId = createResult.rows[0].id; - const response = await request(app) - .patch(`/api/admin/admins/${auth0Sub}/reinstate`) + const response = await request(app.server) + .patch(`/api/admin/admins/${adminId}/reinstate`) .expect(200); expect(response.body).toMatchObject({ - auth0Sub, + id: adminId, email: 'toreinstate@example.com', revokedAt: null }); @@ -384,14 +396,14 @@ describe('Admin Management Integration Tests', () => { // Verify audit log const auditResult = await pool.query( 'SELECT * FROM admin_audit_logs WHERE action = $1 AND target_admin_id = $2', - ['REINSTATE', auth0Sub] + ['REINSTATE', adminId] ); expect(auditResult.rows.length).toBe(1); }); it('should return 404 for non-existent admin', async () => { - const response = await request(app) - .patch('/api/admin/admins/auth0|nonexistent/reinstate') + const response = await request(app.server) + .patch('/api/admin/admins/00000000-0000-0000-0000-000000000000/reinstate') .expect(404); expect(response.body.error).toBe('Not Found'); @@ -400,16 +412,17 @@ describe('Admin Management Integration Tests', () => { it('should handle reinstating already active admin', async () => { // Create active admin + const activeId = 'd3e4f5a6-b7c8-9012-3456-789012cdef01'; const createResult = await pool.query(` - INSERT INTO admin_users (auth0_sub, email, role, created_by) - VALUES ($1, $2, $3, $4) - RETURNING auth0_sub - `, ['auth0|already-active', 'active@example.com', 'admin', testAdminAuth0Sub]); + INSERT INTO admin_users (id, user_profile_id, email, role, created_by) + VALUES ($1, $2, $3, $4, $5) + RETURNING id + `, [activeId, activeId, 'active@example.com', 'admin', testAdminId]); - const auth0Sub = createResult.rows[0].auth0_sub; + const adminId = createResult.rows[0].id; - const response = await request(app) - .patch(`/api/admin/admins/${auth0Sub}/reinstate`) + const response = await request(app.server) + .patch(`/api/admin/admins/${adminId}/reinstate`) .expect(200); expect(response.body.revokedAt).toBeNull(); @@ -426,12 +439,12 @@ describe('Admin Management Integration Tests', () => { ($5, $6, $7, $8), ($9, $10, $11, $12) `, [ - testAdminAuth0Sub, 'CREATE', 'admin_user', 'test1@example.com', - testAdminAuth0Sub, 'REVOKE', 'admin_user', 'test2@example.com', - testAdminAuth0Sub, 'REINSTATE', 'admin_user', 'test3@example.com' + testAdminId, 'CREATE', 'admin_user', 'test1@example.com', + testAdminId, 'REVOKE', 'admin_user', 'test2@example.com', + testAdminId, 'REINSTATE', 'admin_user', 'test3@example.com' ]); - const response = await request(app) + const response = await request(app.server) .get('/api/admin/audit-logs') .expect(200); @@ -440,7 +453,7 @@ describe('Admin Management Integration Tests', () => { expect(response.body.logs.length).toBeGreaterThanOrEqual(3); expect(response.body.logs[0]).toMatchObject({ id: expect.any(String), - actorAdminId: testAdminAuth0Sub, + actorAdminId: testAdminId, action: expect.any(String), resourceType: expect.any(String), createdAt: expect.any(String) @@ -453,10 +466,10 @@ describe('Admin Management Integration Tests', () => { await pool.query(` INSERT INTO admin_audit_logs (actor_admin_id, action, resource_type, resource_id) VALUES ($1, $2, $3, $4) - `, [testAdminAuth0Sub, 'CREATE', 'admin_user', `test${i}@example.com`]); + `, [testAdminId, 'CREATE', 'admin_user', `test${i}@example.com`]); } - const response = await request(app) + const response = await request(app.server) .get('/api/admin/audit-logs?limit=5&offset=0') .expect(200); @@ -473,12 +486,12 @@ describe('Admin Management Integration Tests', () => { ($3, $4, CURRENT_TIMESTAMP - INTERVAL '1 minute'), ($5, $6, CURRENT_TIMESTAMP) `, [ - testAdminAuth0Sub, 'FIRST', - testAdminAuth0Sub, 'SECOND', - testAdminAuth0Sub, 'THIRD' + testAdminId, 'FIRST', + testAdminId, 'SECOND', + testAdminId, 'THIRD' ]); - const response = await request(app) + const response = await request(app.server) .get('/api/admin/audit-logs?limit=3') .expect(200); @@ -491,45 +504,45 @@ describe('Admin Management Integration Tests', () => { describe('End-to-end workflow', () => { it('should create, revoke, and reinstate admin with full audit trail', async () => { // 1. Create new admin - const createResponse = await request(app) + const createResponse = await request(app.server) .post('/api/admin/admins') .send({ email: 'workflow@example.com', role: 'admin' }) .expect(201); - const auth0Sub = createResponse.body.auth0Sub; + const adminId = createResponse.body.id; // 2. Verify admin appears in list - const listResponse = await request(app) + const listResponse = await request(app.server) .get('/api/admin/admins') .expect(200); const createdAdmin = listResponse.body.admins.find( - (admin: any) => admin.auth0Sub === auth0Sub + (admin: any) => admin.id === adminId ); expect(createdAdmin).toBeDefined(); expect(createdAdmin.revokedAt).toBeNull(); // 3. Revoke admin - const revokeResponse = await request(app) - .patch(`/api/admin/admins/${auth0Sub}/revoke`) + const revokeResponse = await request(app.server) + .patch(`/api/admin/admins/${adminId}/revoke`) .expect(200); expect(revokeResponse.body.revokedAt).toBeTruthy(); // 4. Reinstate admin - const reinstateResponse = await request(app) - .patch(`/api/admin/admins/${auth0Sub}/reinstate`) + const reinstateResponse = await request(app.server) + .patch(`/api/admin/admins/${adminId}/reinstate`) .expect(200); expect(reinstateResponse.body.revokedAt).toBeNull(); // 5. Verify complete audit trail - const auditResponse = await request(app) + const auditResponse = await request(app.server) .get('/api/admin/audit-logs') .expect(200); const workflowLogs = auditResponse.body.logs.filter( - (log: any) => log.targetAdminId === auth0Sub || log.resourceId === 'workflow@example.com' + (log: any) => log.targetAdminId === adminId || log.resourceId === 'workflow@example.com' ); expect(workflowLogs.length).toBeGreaterThanOrEqual(3); diff --git a/backend/src/features/admin/tests/unit/admin.guard.test.ts b/backend/src/features/admin/tests/unit/admin.guard.test.ts index e2a281e..6cf3887 100644 --- a/backend/src/features/admin/tests/unit/admin.guard.test.ts +++ b/backend/src/features/admin/tests/unit/admin.guard.test.ts @@ -26,7 +26,7 @@ describe('admin guard plugin', () => { fastify = Fastify(); authenticateMock = jest.fn(async (request: FastifyRequest) => { request.userContext = { - userId: 'auth0|admin', + userId: '7c9e6679-7425-40de-944b-e07fc1f90ae7', email: 'admin@motovaultpro.com', emailVerified: true, onboardingCompleted: true, @@ -41,7 +41,7 @@ describe('admin guard plugin', () => { mockPool = { query: jest.fn().mockResolvedValue({ rows: [{ - auth0_sub: 'auth0|admin', + user_profile_id: '7c9e6679-7425-40de-944b-e07fc1f90ae7', email: 'admin@motovaultpro.com', role: 'admin', revoked_at: null, diff --git a/backend/src/features/admin/tests/unit/admin.service.test.ts b/backend/src/features/admin/tests/unit/admin.service.test.ts index b1d7aaa..6dfda6a 100644 --- a/backend/src/features/admin/tests/unit/admin.service.test.ts +++ b/backend/src/features/admin/tests/unit/admin.service.test.ts @@ -6,13 +6,23 @@ import { AdminService } from '../../domain/admin.service'; import { AdminRepository } from '../../data/admin.repository'; +// Mock the audit log service +jest.mock('../../../audit-log', () => ({ + auditLogService: { + info: jest.fn().mockResolvedValue(undefined), + warn: jest.fn().mockResolvedValue(undefined), + error: jest.fn().mockResolvedValue(undefined), + }, +})); + describe('AdminService', () => { let adminService: AdminService; let mockRepository: jest.Mocked; beforeEach(() => { mockRepository = { - getAdminByAuth0Sub: jest.fn(), + getAdminById: jest.fn(), + getAdminByUserProfileId: jest.fn(), getAdminByEmail: jest.fn(), getAllAdmins: jest.fn(), getActiveAdmins: jest.fn(), @@ -26,30 +36,31 @@ describe('AdminService', () => { adminService = new AdminService(mockRepository); }); - describe('getAdminByAuth0Sub', () => { + describe('getAdminById', () => { it('should return admin when found', async () => { const mockAdmin = { - auth0Sub: 'auth0|123456', + id: '7c9e6679-7425-40de-944b-e07fc1f90ae7', + userProfileId: '7c9e6679-7425-40de-944b-e07fc1f90ae7', email: 'admin@motovaultpro.com', - role: 'admin', + role: 'admin' as const, createdAt: new Date(), - createdBy: 'system', + createdBy: '550e8400-e29b-41d4-a716-446655440000', revokedAt: null, updatedAt: new Date(), }; - mockRepository.getAdminByAuth0Sub.mockResolvedValue(mockAdmin); + mockRepository.getAdminById.mockResolvedValue(mockAdmin); - const result = await adminService.getAdminByAuth0Sub('auth0|123456'); + const result = await adminService.getAdminById('7c9e6679-7425-40de-944b-e07fc1f90ae7'); expect(result).toEqual(mockAdmin); - expect(mockRepository.getAdminByAuth0Sub).toHaveBeenCalledWith('auth0|123456'); + expect(mockRepository.getAdminById).toHaveBeenCalledWith('7c9e6679-7425-40de-944b-e07fc1f90ae7'); }); it('should return null when admin not found', async () => { - mockRepository.getAdminByAuth0Sub.mockResolvedValue(null); + mockRepository.getAdminById.mockResolvedValue(null); - const result = await adminService.getAdminByAuth0Sub('auth0|unknown'); + const result = await adminService.getAdminById('00000000-0000-0000-0000-000000000000'); expect(result).toBeNull(); }); @@ -57,12 +68,15 @@ describe('AdminService', () => { describe('createAdmin', () => { it('should create new admin and log audit', async () => { + const newAdminId = '8f14e45f-ceea-367f-a27f-c9a6d0c67e0e'; + const creatorId = '7c9e6679-7425-40de-944b-e07fc1f90ae7'; const mockAdmin = { - auth0Sub: 'auth0|newadmin', + id: newAdminId, + userProfileId: newAdminId, email: 'newadmin@motovaultpro.com', - role: 'admin', + role: 'admin' as const, createdAt: new Date(), - createdBy: 'auth0|existing', + createdBy: creatorId, revokedAt: null, updatedAt: new Date(), }; @@ -74,16 +88,16 @@ describe('AdminService', () => { const result = await adminService.createAdmin( 'newadmin@motovaultpro.com', 'admin', - 'auth0|newadmin', - 'auth0|existing' + newAdminId, + creatorId ); expect(result).toEqual(mockAdmin); expect(mockRepository.createAdmin).toHaveBeenCalled(); expect(mockRepository.logAuditAction).toHaveBeenCalledWith( - 'auth0|existing', + creatorId, 'CREATE', - mockAdmin.auth0Sub, + mockAdmin.id, 'admin_user', mockAdmin.email, expect.any(Object) @@ -91,12 +105,14 @@ describe('AdminService', () => { }); it('should reject if admin already exists', async () => { + const existingId = '7c9e6679-7425-40de-944b-e07fc1f90ae7'; const existingAdmin = { - auth0Sub: 'auth0|existing', + id: existingId, + userProfileId: existingId, email: 'admin@motovaultpro.com', - role: 'admin', + role: 'admin' as const, createdAt: new Date(), - createdBy: 'system', + createdBy: '550e8400-e29b-41d4-a716-446655440000', revokedAt: null, updatedAt: new Date(), }; @@ -104,39 +120,46 @@ describe('AdminService', () => { mockRepository.getAdminByEmail.mockResolvedValue(existingAdmin); await expect( - adminService.createAdmin('admin@motovaultpro.com', 'admin', 'auth0|new', 'auth0|existing') + adminService.createAdmin('admin@motovaultpro.com', 'admin', '8f14e45f-ceea-367f-a27f-c9a6d0c67e0e', existingId) ).rejects.toThrow('already exists'); }); }); describe('revokeAdmin', () => { it('should revoke admin when multiple active admins exist', async () => { + const toRevokeId = 'a1b2c3d4-e5f6-7890-1234-567890abcdef'; + const admin1Id = '6ba7b810-9dad-11d1-80b4-00c04fd430c8'; + const admin2Id = '8f14e45f-ceea-367f-a27f-c9a6d0c67e0e'; + const revokedAdmin = { - auth0Sub: 'auth0|toadmin', + id: toRevokeId, + userProfileId: toRevokeId, email: 'toadmin@motovaultpro.com', - role: 'admin', + role: 'admin' as const, createdAt: new Date(), - createdBy: 'system', + createdBy: '550e8400-e29b-41d4-a716-446655440000', revokedAt: new Date(), updatedAt: new Date(), }; const activeAdmins = [ { - auth0Sub: 'auth0|admin1', + id: admin1Id, + userProfileId: admin1Id, email: 'admin1@motovaultpro.com', - role: 'admin', + role: 'admin' as const, createdAt: new Date(), - createdBy: 'system', + createdBy: '550e8400-e29b-41d4-a716-446655440000', revokedAt: null, updatedAt: new Date(), }, { - auth0Sub: 'auth0|admin2', + id: admin2Id, + userProfileId: admin2Id, email: 'admin2@motovaultpro.com', - role: 'admin', + role: 'admin' as const, createdAt: new Date(), - createdBy: 'system', + createdBy: '550e8400-e29b-41d4-a716-446655440000', revokedAt: null, updatedAt: new Date(), }, @@ -146,20 +169,22 @@ describe('AdminService', () => { mockRepository.revokeAdmin.mockResolvedValue(revokedAdmin); mockRepository.logAuditAction.mockResolvedValue({} as any); - const result = await adminService.revokeAdmin('auth0|toadmin', 'auth0|admin1'); + const result = await adminService.revokeAdmin(toRevokeId, admin1Id); expect(result).toEqual(revokedAdmin); - expect(mockRepository.revokeAdmin).toHaveBeenCalledWith('auth0|toadmin'); + expect(mockRepository.revokeAdmin).toHaveBeenCalledWith(toRevokeId); expect(mockRepository.logAuditAction).toHaveBeenCalled(); }); it('should prevent revoking last active admin', async () => { + const lastAdminId = '7c9e6679-7425-40de-944b-e07fc1f90ae7'; const lastAdmin = { - auth0Sub: 'auth0|lastadmin', + id: lastAdminId, + userProfileId: lastAdminId, email: 'last@motovaultpro.com', - role: 'admin', + role: 'admin' as const, createdAt: new Date(), - createdBy: 'system', + createdBy: '550e8400-e29b-41d4-a716-446655440000', revokedAt: null, updatedAt: new Date(), }; @@ -167,19 +192,22 @@ describe('AdminService', () => { mockRepository.getActiveAdmins.mockResolvedValue([lastAdmin]); await expect( - adminService.revokeAdmin('auth0|lastadmin', 'auth0|lastadmin') + adminService.revokeAdmin(lastAdminId, lastAdminId) ).rejects.toThrow('Cannot revoke the last active admin'); }); }); describe('reinstateAdmin', () => { it('should reinstate revoked admin and log audit', async () => { + const reinstateId = 'b2c3d4e5-f6a7-8901-2345-678901bcdef0'; + const adminActorId = '7c9e6679-7425-40de-944b-e07fc1f90ae7'; const reinstatedAdmin = { - auth0Sub: 'auth0|reinstate', + id: reinstateId, + userProfileId: reinstateId, email: 'reinstate@motovaultpro.com', - role: 'admin', + role: 'admin' as const, createdAt: new Date(), - createdBy: 'system', + createdBy: '550e8400-e29b-41d4-a716-446655440000', revokedAt: null, updatedAt: new Date(), }; @@ -187,14 +215,14 @@ describe('AdminService', () => { mockRepository.reinstateAdmin.mockResolvedValue(reinstatedAdmin); mockRepository.logAuditAction.mockResolvedValue({} as any); - const result = await adminService.reinstateAdmin('auth0|reinstate', 'auth0|admin'); + const result = await adminService.reinstateAdmin(reinstateId, adminActorId); expect(result).toEqual(reinstatedAdmin); - expect(mockRepository.reinstateAdmin).toHaveBeenCalledWith('auth0|reinstate'); + expect(mockRepository.reinstateAdmin).toHaveBeenCalledWith(reinstateId); expect(mockRepository.logAuditAction).toHaveBeenCalledWith( - 'auth0|admin', + adminActorId, 'REINSTATE', - 'auth0|reinstate', + reinstateId, 'admin_user', reinstatedAdmin.email ); diff --git a/backend/src/features/audit-log/__tests__/audit-log.integration.test.ts b/backend/src/features/audit-log/__tests__/audit-log.integration.test.ts index 73b9a23..7b376aa 100644 --- a/backend/src/features/audit-log/__tests__/audit-log.integration.test.ts +++ b/backend/src/features/audit-log/__tests__/audit-log.integration.test.ts @@ -32,7 +32,7 @@ describe('AuditLog Feature Integration', () => { describe('Vehicle logging integration', () => { it('should create audit log with vehicle category and correct resource', async () => { - const userId = 'test-user-vehicle-123'; + const userId = '550e8400-e29b-41d4-a716-446655440000'; const vehicleId = 'vehicle-uuid-123'; const entry = await service.info( 'vehicle', @@ -56,7 +56,7 @@ describe('AuditLog Feature Integration', () => { }); it('should log vehicle update with correct fields', async () => { - const userId = 'test-user-vehicle-456'; + const userId = '6ba7b810-9dad-11d1-80b4-00c04fd430c8'; const vehicleId = 'vehicle-uuid-456'; const entry = await service.info( 'vehicle', @@ -75,7 +75,7 @@ describe('AuditLog Feature Integration', () => { }); it('should log vehicle deletion with vehicle info', async () => { - const userId = 'test-user-vehicle-789'; + const userId = '7c9e6679-7425-40de-944b-e07fc1f90ae7'; const vehicleId = 'vehicle-uuid-789'; const entry = await service.info( 'vehicle', @@ -96,7 +96,7 @@ describe('AuditLog Feature Integration', () => { describe('Auth logging integration', () => { it('should create audit log with auth category for signup', async () => { - const userId = 'test-user-auth-123'; + const userId = '550e8400-e29b-41d4-a716-446655440000'; const entry = await service.info( 'auth', userId, @@ -116,7 +116,7 @@ describe('AuditLog Feature Integration', () => { }); it('should create audit log for password reset request', async () => { - const userId = 'test-user-auth-456'; + const userId = '6ba7b810-9dad-11d1-80b4-00c04fd430c8'; const entry = await service.info( 'auth', userId, @@ -134,14 +134,14 @@ describe('AuditLog Feature Integration', () => { describe('Admin logging integration', () => { it('should create audit log for admin user creation', async () => { - const adminId = 'admin-user-123'; - const targetAdminSub = 'auth0|target-admin-456'; + const adminId = '7c9e6679-7425-40de-944b-e07fc1f90ae7'; + const targetAdminId = '8f14e45f-ceea-367f-a27f-c9a6d0c67e0e'; const entry = await service.info( 'admin', adminId, 'Admin user created: newadmin@example.com', 'admin_user', - targetAdminSub, + targetAdminId, { email: 'newadmin@example.com', role: 'admin' } ); @@ -156,14 +156,14 @@ describe('AuditLog Feature Integration', () => { }); it('should create audit log for admin revocation', async () => { - const adminId = 'admin-user-123'; - const targetAdminSub = 'auth0|target-admin-789'; + const adminId = '7c9e6679-7425-40de-944b-e07fc1f90ae7'; + const targetAdminId = 'a1b2c3d4-e5f6-7890-1234-567890abcdef'; const entry = await service.info( 'admin', adminId, 'Admin user revoked: revoked@example.com', 'admin_user', - targetAdminSub, + targetAdminId, { email: 'revoked@example.com' } ); @@ -174,14 +174,14 @@ describe('AuditLog Feature Integration', () => { }); it('should create audit log for admin reinstatement', async () => { - const adminId = 'admin-user-123'; - const targetAdminSub = 'auth0|target-admin-reinstated'; + const adminId = '7c9e6679-7425-40de-944b-e07fc1f90ae7'; + const targetAdminId = 'b2c3d4e5-f6a7-8901-2345-678901bcdef0'; const entry = await service.info( 'admin', adminId, 'Admin user reinstated: reinstated@example.com', 'admin_user', - targetAdminSub, + targetAdminId, { email: 'reinstated@example.com' } ); @@ -194,7 +194,7 @@ describe('AuditLog Feature Integration', () => { describe('Backup/System logging integration', () => { it('should create audit log for backup creation', async () => { - const adminId = 'admin-user-backup-123'; + const adminId = '7c9e6679-7425-40de-944b-e07fc1f90ae7'; const backupId = 'backup-uuid-123'; const entry = await service.info( 'system', @@ -215,7 +215,7 @@ describe('AuditLog Feature Integration', () => { }); it('should create audit log for backup restore', async () => { - const adminId = 'admin-user-backup-456'; + const adminId = '7c9e6679-7425-40de-944b-e07fc1f90ae7'; const backupId = 'backup-uuid-456'; const entry = await service.info( 'system', @@ -233,7 +233,7 @@ describe('AuditLog Feature Integration', () => { }); it('should create error-level audit log for backup failure', async () => { - const adminId = 'admin-user-backup-789'; + const adminId = '7c9e6679-7425-40de-944b-e07fc1f90ae7'; const backupId = 'backup-uuid-789'; const entry = await service.error( 'system', @@ -253,7 +253,7 @@ describe('AuditLog Feature Integration', () => { }); it('should create error-level audit log for restore failure', async () => { - const adminId = 'admin-user-restore-fail'; + const adminId = '7c9e6679-7425-40de-944b-e07fc1f90ae7'; const backupId = 'backup-uuid-restore-fail'; const entry = await service.error( 'system', diff --git a/backend/src/features/auth/tests/integration/auth.integration.test.ts b/backend/src/features/auth/tests/integration/auth.integration.test.ts index e783ed2..b7a8345 100644 --- a/backend/src/features/auth/tests/integration/auth.integration.test.ts +++ b/backend/src/features/auth/tests/integration/auth.integration.test.ts @@ -19,6 +19,7 @@ jest.mock('../../../../core/plugins/auth.plugin', () => { return { default: fastifyPlugin(async function (fastify) { fastify.decorate('authenticate', async function (request, _reply) { + // JWT sub is still auth0|xxx format request.user = { sub: 'auth0|test-user-123' }; }); }, { name: 'auth-plugin' }), diff --git a/backend/src/features/auth/tests/unit/auth.service.test.ts b/backend/src/features/auth/tests/unit/auth.service.test.ts index c698f75..84f0d18 100644 --- a/backend/src/features/auth/tests/unit/auth.service.test.ts +++ b/backend/src/features/auth/tests/unit/auth.service.test.ts @@ -103,6 +103,8 @@ describe('AuthService', () => { onboardingCompletedAt: null, deactivatedAt: null, deactivatedBy: null, + deletionRequestedAt: null, + deletionScheduledFor: null, createdAt: new Date(), updatedAt: new Date(), }); @@ -116,6 +118,8 @@ describe('AuthService', () => { onboardingCompletedAt: null, deactivatedAt: null, deactivatedBy: null, + deletionRequestedAt: null, + deletionScheduledFor: null, createdAt: new Date(), updatedAt: new Date(), }); @@ -149,6 +153,8 @@ describe('AuthService', () => { onboardingCompletedAt: null, deactivatedAt: null, deactivatedBy: null, + deletionRequestedAt: null, + deletionScheduledFor: null, createdAt: new Date(), updatedAt: new Date(), }); diff --git a/backend/src/features/fuel-logs/tests/fixtures/fuel-logs.fixtures.json b/backend/src/features/fuel-logs/tests/fixtures/fuel-logs.fixtures.json index 2bd28f1..b6aeed8 100644 --- a/backend/src/features/fuel-logs/tests/fixtures/fuel-logs.fixtures.json +++ b/backend/src/features/fuel-logs/tests/fixtures/fuel-logs.fixtures.json @@ -73,7 +73,7 @@ }, "responseWithEfficiency": { "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", - "userId": "auth0|user123", + "userId": "550e8400-e29b-41d4-a716-446655440000", "vehicleId": "550e8400-e29b-41d4-a716-446655440000", "dateTime": "2024-01-15T10:30:00Z", "odometerReading": 52000, diff --git a/backend/src/features/maintenance/tests/fixtures/maintenance.fixtures.json b/backend/src/features/maintenance/tests/fixtures/maintenance.fixtures.json index f551606..8154492 100644 --- a/backend/src/features/maintenance/tests/fixtures/maintenance.fixtures.json +++ b/backend/src/features/maintenance/tests/fixtures/maintenance.fixtures.json @@ -99,7 +99,7 @@ }, "maintenanceScheduleResponse": { "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", - "userId": "auth0|user123", + "userId": "550e8400-e29b-41d4-a716-446655440000", "vehicleId": "550e8400-e29b-41d4-a716-446655440000", "type": "oil_change", "category": "routine_maintenance", diff --git a/backend/src/features/stations/tests/integration/community-stations.api.test.ts b/backend/src/features/stations/tests/integration/community-stations.api.test.ts index 5ee1bda..71e45bf 100644 --- a/backend/src/features/stations/tests/integration/community-stations.api.test.ts +++ b/backend/src/features/stations/tests/integration/community-stations.api.test.ts @@ -12,8 +12,8 @@ describe('Community Stations API Integration Tests', () => { let app: FastifyInstance; let pool: Pool; - const testUserId = 'auth0|test-user-123'; - const testAdminId = 'auth0|test-admin-123'; + const testUserId = '550e8400-e29b-41d4-a716-446655440000'; + const testAdminId = '7c9e6679-7425-40de-944b-e07fc1f90ae7'; const mockStationData = { name: 'Test Gas Station', diff --git a/frontend/src/features/admin/__tests__/AdminUsersPage.test.tsx b/frontend/src/features/admin/__tests__/AdminUsersPage.test.tsx index 191b1bb..4594683 100644 --- a/frontend/src/features/admin/__tests__/AdminUsersPage.test.tsx +++ b/frontend/src/features/admin/__tests__/AdminUsersPage.test.tsx @@ -48,7 +48,8 @@ describe('AdminUsersPage', () => { mockUseAdminAccess.mockReturnValue({ isAdmin: true, adminRecord: { - auth0Sub: 'auth0|123', + id: 'admin-uuid-123', + userProfileId: 'user-uuid-123', email: 'admin@example.com', role: 'admin', createdAt: '2024-01-01', diff --git a/frontend/src/features/admin/__tests__/useAdminAccess.test.tsx b/frontend/src/features/admin/__tests__/useAdminAccess.test.tsx index 4574380..e8a8007 100644 --- a/frontend/src/features/admin/__tests__/useAdminAccess.test.tsx +++ b/frontend/src/features/admin/__tests__/useAdminAccess.test.tsx @@ -55,7 +55,8 @@ describe('useAdminAccess', () => { mockAdminApi.verifyAccess.mockResolvedValue({ isAdmin: true, adminRecord: { - auth0Sub: 'auth0|123', + id: 'admin-uuid-123', + userProfileId: 'user-uuid-123', email: 'admin@example.com', role: 'admin', createdAt: '2024-01-01', diff --git a/frontend/src/features/admin/__tests__/useAdmins.test.tsx b/frontend/src/features/admin/__tests__/useAdmins.test.tsx index 2b1cbb6..9f886f5 100644 --- a/frontend/src/features/admin/__tests__/useAdmins.test.tsx +++ b/frontend/src/features/admin/__tests__/useAdmins.test.tsx @@ -42,7 +42,8 @@ describe('Admin user management hooks', () => { it('should fetch admin users', async () => { const mockAdmins = [ { - auth0Sub: 'auth0|123', + id: 'admin-uuid-123', + userProfileId: 'user-uuid-123', email: 'admin1@example.com', role: 'admin', createdAt: '2024-01-01', @@ -68,11 +69,12 @@ describe('Admin user management hooks', () => { describe('useCreateAdmin', () => { it('should create admin and show success toast', async () => { const newAdmin = { - auth0Sub: 'auth0|456', + id: 'admin-uuid-456', + userProfileId: 'user-uuid-456', email: 'newadmin@example.com', role: 'admin', createdAt: '2024-01-01', - createdBy: 'auth0|123', + createdBy: 'admin-uuid-123', revokedAt: null, updatedAt: '2024-01-01', }; @@ -131,11 +133,11 @@ describe('Admin user management hooks', () => { wrapper: createWrapper(), }); - result.current.mutate('auth0|123'); + result.current.mutate('admin-uuid-123'); await waitFor(() => expect(result.current.isSuccess).toBe(true)); - expect(mockAdminApi.revokeAdmin).toHaveBeenCalledWith('auth0|123'); + expect(mockAdminApi.revokeAdmin).toHaveBeenCalledWith('admin-uuid-123'); expect(toast.success).toHaveBeenCalledWith('Admin revoked successfully'); }); }); @@ -148,11 +150,11 @@ describe('Admin user management hooks', () => { wrapper: createWrapper(), }); - result.current.mutate('auth0|123'); + result.current.mutate('admin-uuid-123'); await waitFor(() => expect(result.current.isSuccess).toBe(true)); - expect(mockAdminApi.reinstateAdmin).toHaveBeenCalledWith('auth0|123'); + expect(mockAdminApi.reinstateAdmin).toHaveBeenCalledWith('admin-uuid-123'); expect(toast.success).toHaveBeenCalledWith('Admin reinstated successfully'); }); }); diff --git a/frontend/src/features/admin/api/admin.api.ts b/frontend/src/features/admin/api/admin.api.ts index 8382ea3..eac73cd 100644 --- a/frontend/src/features/admin/api/admin.api.ts +++ b/frontend/src/features/admin/api/admin.api.ts @@ -101,12 +101,12 @@ export const adminApi = { return response.data; }, - revokeAdmin: async (auth0Sub: string): Promise => { - await apiClient.patch(`/admin/admins/${auth0Sub}/revoke`); + revokeAdmin: async (id: string): Promise => { + await apiClient.patch(`/admin/admins/${id}/revoke`); }, - reinstateAdmin: async (auth0Sub: string): Promise => { - await apiClient.patch(`/admin/admins/${auth0Sub}/reinstate`); + reinstateAdmin: async (id: string): Promise => { + await apiClient.patch(`/admin/admins/${id}/reinstate`); }, // Audit logs @@ -328,62 +328,62 @@ export const adminApi = { return response.data; }, - get: async (auth0Sub: string): Promise => { + get: async (userId: string): Promise => { const response = await apiClient.get( - `/admin/users/${encodeURIComponent(auth0Sub)}` + `/admin/users/${userId}` ); return response.data; }, - getVehicles: async (auth0Sub: string): Promise => { + getVehicles: async (userId: string): Promise => { const response = await apiClient.get( - `/admin/users/${encodeURIComponent(auth0Sub)}/vehicles` + `/admin/users/${userId}/vehicles` ); return response.data; }, - updateTier: async (auth0Sub: string, data: UpdateUserTierRequest): Promise => { + updateTier: async (userId: string, data: UpdateUserTierRequest): Promise => { const response = await apiClient.patch( - `/admin/users/${encodeURIComponent(auth0Sub)}/tier`, + `/admin/users/${userId}/tier`, data ); return response.data; }, - deactivate: async (auth0Sub: string, data?: DeactivateUserRequest): Promise => { + deactivate: async (userId: string, data?: DeactivateUserRequest): Promise => { const response = await apiClient.patch( - `/admin/users/${encodeURIComponent(auth0Sub)}/deactivate`, + `/admin/users/${userId}/deactivate`, data || {} ); return response.data; }, - reactivate: async (auth0Sub: string): Promise => { + reactivate: async (userId: string): Promise => { const response = await apiClient.patch( - `/admin/users/${encodeURIComponent(auth0Sub)}/reactivate` + `/admin/users/${userId}/reactivate` ); return response.data; }, - updateProfile: async (auth0Sub: string, data: UpdateUserProfileRequest): Promise => { + updateProfile: async (userId: string, data: UpdateUserProfileRequest): Promise => { const response = await apiClient.patch( - `/admin/users/${encodeURIComponent(auth0Sub)}/profile`, + `/admin/users/${userId}/profile`, data ); return response.data; }, - promoteToAdmin: async (auth0Sub: string, data?: PromoteToAdminRequest): Promise => { + promoteToAdmin: async (userId: string, data?: PromoteToAdminRequest): Promise => { const response = await apiClient.patch( - `/admin/users/${encodeURIComponent(auth0Sub)}/promote`, + `/admin/users/${userId}/promote`, data || {} ); return response.data; }, - hardDelete: async (auth0Sub: string, reason?: string): Promise<{ message: string }> => { + hardDelete: async (userId: string, reason?: string): Promise<{ message: string }> => { const response = await apiClient.delete<{ message: string }>( - `/admin/users/${encodeURIComponent(auth0Sub)}`, + `/admin/users/${userId}`, { params: reason ? { reason } : undefined } ); return response.data; diff --git a/frontend/src/features/admin/hooks/useAdmins.ts b/frontend/src/features/admin/hooks/useAdmins.ts index 4170ec8..3df42f6 100644 --- a/frontend/src/features/admin/hooks/useAdmins.ts +++ b/frontend/src/features/admin/hooks/useAdmins.ts @@ -51,7 +51,7 @@ export const useRevokeAdmin = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: (auth0Sub: string) => adminApi.revokeAdmin(auth0Sub), + mutationFn: (id: string) => adminApi.revokeAdmin(id), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['admins'] }); toast.success('Admin revoked successfully'); @@ -66,7 +66,7 @@ export const useReinstateAdmin = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: (auth0Sub: string) => adminApi.reinstateAdmin(auth0Sub), + mutationFn: (id: string) => adminApi.reinstateAdmin(id), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['admins'] }); toast.success('Admin reinstated successfully'); diff --git a/frontend/src/features/admin/hooks/useUsers.ts b/frontend/src/features/admin/hooks/useUsers.ts index d76079a..3d6d9cc 100644 --- a/frontend/src/features/admin/hooks/useUsers.ts +++ b/frontend/src/features/admin/hooks/useUsers.ts @@ -29,8 +29,8 @@ interface ApiError { export const userQueryKeys = { all: ['admin-users'] as const, list: (params: ListUsersParams) => [...userQueryKeys.all, 'list', params] as const, - detail: (auth0Sub: string) => [...userQueryKeys.all, 'detail', auth0Sub] as const, - vehicles: (auth0Sub: string) => [...userQueryKeys.all, 'vehicles', auth0Sub] as const, + detail: (userId: string) => [...userQueryKeys.all, 'detail', userId] as const, + vehicles: (userId: string) => [...userQueryKeys.all, 'vehicles', userId] as const, }; // Query keys for admin stats @@ -58,13 +58,13 @@ export const useUsers = (params: ListUsersParams = {}) => { /** * Hook to get a single user's details */ -export const useUser = (auth0Sub: string) => { +export const useUser = (userId: string) => { const { isAuthenticated, isLoading } = useAuth0(); return useQuery({ - queryKey: userQueryKeys.detail(auth0Sub), - queryFn: () => adminApi.users.get(auth0Sub), - enabled: isAuthenticated && !isLoading && !!auth0Sub, + queryKey: userQueryKeys.detail(userId), + queryFn: () => adminApi.users.get(userId), + enabled: isAuthenticated && !isLoading && !!userId, staleTime: 2 * 60 * 1000, gcTime: 5 * 60 * 1000, retry: 1, @@ -78,8 +78,8 @@ export const useUpdateUserTier = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: ({ auth0Sub, data }: { auth0Sub: string; data: UpdateUserTierRequest }) => - adminApi.users.updateTier(auth0Sub, data), + mutationFn: ({ userId, data }: { userId: string; data: UpdateUserTierRequest }) => + adminApi.users.updateTier(userId, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: userQueryKeys.all }); toast.success('Subscription tier updated'); @@ -101,8 +101,8 @@ export const useDeactivateUser = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: ({ auth0Sub, data }: { auth0Sub: string; data?: DeactivateUserRequest }) => - adminApi.users.deactivate(auth0Sub, data), + mutationFn: ({ userId, data }: { userId: string; data?: DeactivateUserRequest }) => + adminApi.users.deactivate(userId, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: userQueryKeys.all }); toast.success('User deactivated'); @@ -124,7 +124,7 @@ export const useReactivateUser = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: (auth0Sub: string) => adminApi.users.reactivate(auth0Sub), + mutationFn: (userId: string) => adminApi.users.reactivate(userId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: userQueryKeys.all }); toast.success('User reactivated'); @@ -146,8 +146,8 @@ export const useUpdateUserProfile = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: ({ auth0Sub, data }: { auth0Sub: string; data: UpdateUserProfileRequest }) => - adminApi.users.updateProfile(auth0Sub, data), + mutationFn: ({ userId, data }: { userId: string; data: UpdateUserProfileRequest }) => + adminApi.users.updateProfile(userId, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: userQueryKeys.all }); toast.success('User profile updated'); @@ -169,8 +169,8 @@ export const usePromoteToAdmin = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: ({ auth0Sub, data }: { auth0Sub: string; data?: PromoteToAdminRequest }) => - adminApi.users.promoteToAdmin(auth0Sub, data), + mutationFn: ({ userId, data }: { userId: string; data?: PromoteToAdminRequest }) => + adminApi.users.promoteToAdmin(userId, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: userQueryKeys.all }); toast.success('User promoted to admin'); @@ -192,8 +192,8 @@ export const useHardDeleteUser = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: ({ auth0Sub, reason }: { auth0Sub: string; reason?: string }) => - adminApi.users.hardDelete(auth0Sub, reason), + mutationFn: ({ userId, reason }: { userId: string; reason?: string }) => + adminApi.users.hardDelete(userId, reason), onSuccess: () => { queryClient.invalidateQueries({ queryKey: userQueryKeys.all }); toast.success('User permanently deleted'); @@ -228,13 +228,13 @@ export const useAdminStats = () => { /** * Hook to get a user's vehicles (admin view - year, make, model only) */ -export const useUserVehicles = (auth0Sub: string) => { +export const useUserVehicles = (userId: string) => { const { isAuthenticated, isLoading } = useAuth0(); return useQuery({ - queryKey: userQueryKeys.vehicles(auth0Sub), - queryFn: () => adminApi.users.getVehicles(auth0Sub), - enabled: isAuthenticated && !isLoading && !!auth0Sub, + queryKey: userQueryKeys.vehicles(userId), + queryFn: () => adminApi.users.getVehicles(userId), + enabled: isAuthenticated && !isLoading && !!userId, staleTime: 2 * 60 * 1000, gcTime: 5 * 60 * 1000, retry: 1, diff --git a/frontend/src/features/admin/mobile/AdminUsersMobileScreen.tsx b/frontend/src/features/admin/mobile/AdminUsersMobileScreen.tsx index 4cfc876..d9b5011 100644 --- a/frontend/src/features/admin/mobile/AdminUsersMobileScreen.tsx +++ b/frontend/src/features/admin/mobile/AdminUsersMobileScreen.tsx @@ -104,8 +104,8 @@ const VehicleCountBadge: React.FC<{ count: number; onClick?: () => void }> = ({ ); // Expandable vehicle list component -const UserVehiclesList: React.FC<{ auth0Sub: string; isOpen: boolean }> = ({ auth0Sub, isOpen }) => { - const { data, isLoading, error } = useUserVehicles(auth0Sub); +const UserVehiclesList: React.FC<{ userId: string; isOpen: boolean }> = ({ userId, isOpen }) => { + const { data, isLoading, error } = useUserVehicles(userId); if (!isOpen) return null; @@ -215,7 +215,7 @@ export const AdminUsersMobileScreen: React.FC = () => { (newTier: SubscriptionTier) => { if (selectedUser) { updateTierMutation.mutate( - { auth0Sub: selectedUser.auth0Sub, data: { subscriptionTier: newTier } }, + { userId: selectedUser.id, data: { subscriptionTier: newTier } }, { onSuccess: () => { setShowTierPicker(false); @@ -232,7 +232,7 @@ export const AdminUsersMobileScreen: React.FC = () => { const handleDeactivate = useCallback(() => { if (selectedUser) { deactivateMutation.mutate( - { auth0Sub: selectedUser.auth0Sub, data: { reason: deactivateReason || undefined } }, + { userId: selectedUser.id, data: { reason: deactivateReason || undefined } }, { onSuccess: () => { setShowDeactivateConfirm(false); @@ -247,7 +247,7 @@ export const AdminUsersMobileScreen: React.FC = () => { const handleReactivate = useCallback(() => { if (selectedUser) { - reactivateMutation.mutate(selectedUser.auth0Sub, { + reactivateMutation.mutate(selectedUser.id, { onSuccess: () => { setShowUserActions(false); setSelectedUser(null); @@ -276,7 +276,7 @@ export const AdminUsersMobileScreen: React.FC = () => { } if (Object.keys(updates).length > 0) { updateProfileMutation.mutate( - { auth0Sub: selectedUser.auth0Sub, data: updates }, + { userId: selectedUser.id, data: updates }, { onSuccess: () => { setShowEditModal(false); @@ -306,7 +306,7 @@ export const AdminUsersMobileScreen: React.FC = () => { const handlePromoteConfirm = useCallback(() => { if (selectedUser) { promoteToAdminMutation.mutate( - { auth0Sub: selectedUser.auth0Sub, data: { role: promoteRole } }, + { userId: selectedUser.id, data: { role: promoteRole } }, { onSuccess: () => { setShowPromoteModal(false); @@ -332,7 +332,7 @@ export const AdminUsersMobileScreen: React.FC = () => { const handleHardDeleteConfirm = useCallback(() => { if (selectedUser && hardDeleteConfirmText === 'DELETE') { hardDeleteMutation.mutate( - { auth0Sub: selectedUser.auth0Sub, reason: hardDeleteReason || undefined }, + { userId: selectedUser.id, reason: hardDeleteReason || undefined }, { onSuccess: () => { setShowHardDeleteModal(false); @@ -509,7 +509,7 @@ export const AdminUsersMobileScreen: React.FC = () => { {users.length > 0 && (
{users.map((user) => ( - +
))} diff --git a/frontend/src/features/admin/types/admin.types.ts b/frontend/src/features/admin/types/admin.types.ts index 6ae46bf..4b9ba60 100644 --- a/frontend/src/features/admin/types/admin.types.ts +++ b/frontend/src/features/admin/types/admin.types.ts @@ -5,7 +5,8 @@ // Admin user types export interface AdminUser { - auth0Sub: string; + id: string; + userProfileId: string; email: string; role: string; createdAt: string; diff --git a/frontend/src/pages/admin/AdminUsersPage.tsx b/frontend/src/pages/admin/AdminUsersPage.tsx index d4e5f16..79e60c1 100644 --- a/frontend/src/pages/admin/AdminUsersPage.tsx +++ b/frontend/src/pages/admin/AdminUsersPage.tsx @@ -71,8 +71,8 @@ import { AdminSectionHeader } from '../../features/admin/components'; const PAGE_SIZE_OPTIONS = [10, 20, 50, 100]; // Expandable vehicle row component -const UserVehiclesRow: React.FC<{ auth0Sub: string; isOpen: boolean }> = ({ auth0Sub, isOpen }) => { - const { data, isLoading, error } = useUserVehicles(auth0Sub); +const UserVehiclesRow: React.FC<{ userId: string; isOpen: boolean }> = ({ userId, isOpen }) => { + const { data, isLoading, error } = useUserVehicles(userId); if (!isOpen) return null; @@ -222,8 +222,8 @@ export const AdminUsersPage: React.FC = () => { }, []); const handleTierChange = useCallback( - (auth0Sub: string, newTier: SubscriptionTier) => { - updateTierMutation.mutate({ auth0Sub, data: { subscriptionTier: newTier } }); + (userId: string, newTier: SubscriptionTier) => { + updateTierMutation.mutate({ userId, data: { subscriptionTier: newTier } }); }, [updateTierMutation] ); @@ -246,7 +246,7 @@ export const AdminUsersPage: React.FC = () => { const handleDeactivateConfirm = useCallback(() => { if (selectedUser) { deactivateMutation.mutate( - { auth0Sub: selectedUser.auth0Sub, data: { reason: deactivateReason || undefined } }, + { userId: selectedUser.id, data: { reason: deactivateReason || undefined } }, { onSuccess: () => { setDeactivateDialogOpen(false); @@ -260,7 +260,7 @@ export const AdminUsersPage: React.FC = () => { const handleReactivate = useCallback(() => { if (selectedUser) { - reactivateMutation.mutate(selectedUser.auth0Sub); + reactivateMutation.mutate(selectedUser.id); setAnchorEl(null); setSelectedUser(null); } @@ -286,7 +286,7 @@ export const AdminUsersPage: React.FC = () => { } if (Object.keys(updates).length > 0) { updateProfileMutation.mutate( - { auth0Sub: selectedUser.auth0Sub, data: updates }, + { userId: selectedUser.id, data: updates }, { onSuccess: () => { setEditDialogOpen(false); @@ -316,7 +316,7 @@ export const AdminUsersPage: React.FC = () => { const handlePromoteConfirm = useCallback(() => { if (selectedUser) { promoteToAdminMutation.mutate( - { auth0Sub: selectedUser.auth0Sub, data: { role: promoteRole } }, + { userId: selectedUser.id, data: { role: promoteRole } }, { onSuccess: () => { setPromoteDialogOpen(false); @@ -342,7 +342,7 @@ export const AdminUsersPage: React.FC = () => { const handleHardDeleteConfirm = useCallback(() => { if (selectedUser && hardDeleteConfirmText === 'DELETE') { hardDeleteMutation.mutate( - { auth0Sub: selectedUser.auth0Sub, reason: hardDeleteReason || undefined }, + { userId: selectedUser.id, reason: hardDeleteReason || undefined }, { onSuccess: () => { setHardDeleteDialogOpen(false); @@ -496,11 +496,11 @@ export const AdminUsersPage: React.FC = () => { {users.map((user) => ( - + *': { borderBottom: expandedRow === user.auth0Sub ? 'unset' : undefined }, + '& > *': { borderBottom: expandedRow === user.id ? 'unset' : undefined }, }} > {user.email} @@ -510,7 +510,7 @@ export const AdminUsersPage: React.FC = () => {