chore: update test fixtures and frontend for UUID identity (refs #217)
Some checks failed
Deploy to Staging / Build Images (pull_request) Successful in 6m41s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 52s
Deploy to Staging / Verify Staging (pull_request) Failing after 4m7s
Deploy to Staging / Notify Staging Ready (pull_request) Has been skipped
Deploy to Staging / Notify Staging Failure (pull_request) Successful in 9s

Backend test fixtures:
- Replace auth0|xxx format with UUID in all test userId values
- Update admin tests for new id/userProfileId schema
- Add missing deletionRequestedAt/deletionScheduledFor to auth test mocks
- Fix admin integration test supertest usage (app.server)

Frontend:
- AdminUser type: auth0Sub -> id + userProfileId
- admin.api.ts: all user management methods use userId (UUID) params
- useUsers/useAdmins hooks: auth0Sub -> userId/id in mutations
- AdminUsersPage + AdminUsersMobileScreen: user.auth0Sub -> user.id
- Remove encodeURIComponent (UUIDs don't need encoding)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Eric Gullickson
2026-02-16 10:21:18 -06:00
parent 3b1112a9fe
commit 754639c86d
20 changed files with 316 additions and 263 deletions

View File

@@ -17,7 +17,7 @@ const createRequest = (subscriptionTier?: string): Partial<FastifyRequest> => {
} }
return { return {
userContext: { userContext: {
userId: 'auth0|user123456789', userId: '550e8400-e29b-41d4-a716-446655440000',
email: 'user@example.com', email: 'user@example.com',
emailVerified: true, emailVerified: true,
onboardingCompleted: true, onboardingCompleted: true,

View File

@@ -26,7 +26,7 @@ describe('tier guard plugin', () => {
// Mock authenticate to set userContext // Mock authenticate to set userContext
authenticateMock = jest.fn(async (request: FastifyRequest) => { authenticateMock = jest.fn(async (request: FastifyRequest) => {
request.userContext = { request.userContext = {
userId: 'auth0|user123', userId: '550e8400-e29b-41d4-a716-446655440000',
email: 'user@example.com', email: 'user@example.com',
emailVerified: true, emailVerified: true,
onboardingCompleted: true, onboardingCompleted: true,
@@ -48,7 +48,7 @@ describe('tier guard plugin', () => {
it('allows access when user tier meets minimum', async () => { it('allows access when user tier meets minimum', async () => {
authenticateMock.mockImplementation(async (request: FastifyRequest) => { authenticateMock.mockImplementation(async (request: FastifyRequest) => {
request.userContext = { request.userContext = {
userId: 'auth0|user123', userId: '550e8400-e29b-41d4-a716-446655440000',
email: 'user@example.com', email: 'user@example.com',
emailVerified: true, emailVerified: true,
onboardingCompleted: true, onboardingCompleted: true,
@@ -71,7 +71,7 @@ describe('tier guard plugin', () => {
it('allows access when user tier exceeds minimum', async () => { it('allows access when user tier exceeds minimum', async () => {
authenticateMock.mockImplementation(async (request: FastifyRequest) => { authenticateMock.mockImplementation(async (request: FastifyRequest) => {
request.userContext = { request.userContext = {
userId: 'auth0|user123', userId: '550e8400-e29b-41d4-a716-446655440000',
email: 'user@example.com', email: 'user@example.com',
emailVerified: true, emailVerified: true,
onboardingCompleted: true, onboardingCompleted: true,
@@ -130,7 +130,7 @@ describe('tier guard plugin', () => {
it('allows pro tier access to pro feature', async () => { it('allows pro tier access to pro feature', async () => {
authenticateMock.mockImplementation(async (request: FastifyRequest) => { authenticateMock.mockImplementation(async (request: FastifyRequest) => {
request.userContext = { request.userContext = {
userId: 'auth0|user123', userId: '550e8400-e29b-41d4-a716-446655440000',
email: 'user@example.com', email: 'user@example.com',
emailVerified: true, emailVerified: true,
onboardingCompleted: true, onboardingCompleted: true,

View File

@@ -4,18 +4,19 @@
*/ */
import request from 'supertest'; import request from 'supertest';
import { app } from '../../../../app'; import { buildApp } from '../../../../app';
import pool from '../../../../core/config/database'; import pool from '../../../../core/config/database';
import { FastifyInstance } from 'fastify';
import { readFileSync } from 'fs'; import { readFileSync } from 'fs';
import { join } from 'path'; import { join } from 'path';
import fastifyPlugin from 'fastify-plugin'; import fastifyPlugin from 'fastify-plugin';
import { setAdminGuardPool } from '../../../../core/plugins/admin-guard.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'; const DEFAULT_ADMIN_EMAIL = 'test-admin@motovaultpro.com';
let currentUser = { let currentUser = {
sub: DEFAULT_ADMIN_SUB, sub: 'auth0|test-admin-123',
email: DEFAULT_ADMIN_EMAIL, email: DEFAULT_ADMIN_EMAIL,
}; };
@@ -25,11 +26,15 @@ jest.mock('../../../../core/plugins/auth.plugin', () => {
default: fastifyPlugin(async function(fastify) { default: fastifyPlugin(async function(fastify) {
fastify.decorate('authenticate', async function(request, _reply) { fastify.decorate('authenticate', async function(request, _reply) {
// Inject dynamic test user context // Inject dynamic test user context
// JWT sub is still auth0|xxx format
request.user = { sub: currentUser.sub }; request.user = { sub: currentUser.sub };
request.userContext = { request.userContext = {
userId: currentUser.sub, userId: DEFAULT_ADMIN_ID,
email: currentUser.email, email: currentUser.email,
emailVerified: true,
onboardingCompleted: true,
isAdmin: false, // Will be set by admin guard isAdmin: false, // Will be set by admin guard
subscriptionTier: 'free',
}; };
}); });
}, { name: 'auth-plugin' }) }, { name: 'auth-plugin' })
@@ -37,10 +42,14 @@ jest.mock('../../../../core/plugins/auth.plugin', () => {
}); });
describe('Admin Management Integration Tests', () => { describe('Admin Management Integration Tests', () => {
let testAdminAuth0Sub: string; let app: FastifyInstance;
let testNonAdminAuth0Sub: string; let testAdminId: string;
beforeAll(async () => { beforeAll(async () => {
// Build the app
app = await buildApp();
await app.ready();
// Run the admin migration directly using the migration file // Run the admin migration directly using the migration file
const migrationFile = join(__dirname, '../../migrations/001_create_admin_users.sql'); const migrationFile = join(__dirname, '../../migrations/001_create_admin_users.sql');
const migrationSQL = readFileSync(migrationFile, 'utf-8'); const migrationSQL = readFileSync(migrationFile, 'utf-8');
@@ -50,33 +59,31 @@ describe('Admin Management Integration Tests', () => {
setAdminGuardPool(pool); setAdminGuardPool(pool);
// Create test admin user // Create test admin user
testAdminAuth0Sub = DEFAULT_ADMIN_SUB; testAdminId = DEFAULT_ADMIN_ID;
await pool.query(` 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) VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (auth0_sub) DO NOTHING ON CONFLICT (user_profile_id) DO NOTHING
`, [testAdminAuth0Sub, DEFAULT_ADMIN_EMAIL, 'admin', 'system']); `, [testAdminId, testAdminId, DEFAULT_ADMIN_EMAIL, 'admin', 'system']);
// Create test non-admin auth0Sub for permission tests
testNonAdminAuth0Sub = 'test-non-admin-456';
}); });
afterAll(async () => { afterAll(async () => {
// Clean up test database // Clean up test database
await pool.query('DROP TABLE IF EXISTS admin_audit_logs CASCADE'); await pool.query('DROP TABLE IF EXISTS admin_audit_logs CASCADE');
await pool.query('DROP TABLE IF EXISTS admin_users CASCADE'); await pool.query('DROP TABLE IF EXISTS admin_users CASCADE');
await app.close();
await pool.end(); await pool.end();
}); });
beforeEach(async () => { beforeEach(async () => {
// Clean up test data before each test (except the test admin) // Clean up test data before each test (except the test admin)
await pool.query( await pool.query(
'DELETE FROM admin_users WHERE auth0_sub != $1 AND auth0_sub != $2', 'DELETE FROM admin_users WHERE user_profile_id != $1',
[testAdminAuth0Sub, 'system|bootstrap'] [testAdminId]
); );
await pool.query('DELETE FROM admin_audit_logs'); await pool.query('DELETE FROM admin_audit_logs');
currentUser = { currentUser = {
sub: DEFAULT_ADMIN_SUB, sub: 'auth0|test-admin-123',
email: DEFAULT_ADMIN_EMAIL, email: DEFAULT_ADMIN_EMAIL,
}; };
}); });
@@ -85,11 +92,11 @@ describe('Admin Management Integration Tests', () => {
it('should reject non-admin user trying to list admins', async () => { it('should reject non-admin user trying to list admins', async () => {
// Create mock for non-admin user // Create mock for non-admin user
currentUser = { currentUser = {
sub: testNonAdminAuth0Sub, sub: 'auth0|test-non-admin-456',
email: 'test-user@example.com', email: 'test-user@example.com',
}; };
const response = await request(app) const response = await request(app.server)
.get('/api/admin/admins') .get('/api/admin/admins')
.expect(403); .expect(403);
@@ -101,51 +108,51 @@ describe('Admin Management Integration Tests', () => {
describe('GET /api/admin/verify', () => { describe('GET /api/admin/verify', () => {
it('should confirm admin access for existing admin', async () => { it('should confirm admin access for existing admin', async () => {
currentUser = { currentUser = {
sub: testAdminAuth0Sub, sub: 'auth0|test-admin-123',
email: DEFAULT_ADMIN_EMAIL, email: DEFAULT_ADMIN_EMAIL,
}; };
const response = await request(app) const response = await request(app.server)
.get('/api/admin/verify') .get('/api/admin/verify')
.expect(200); .expect(200);
expect(response.body.isAdmin).toBe(true); expect(response.body.isAdmin).toBe(true);
expect(response.body.adminRecord).toMatchObject({ expect(response.body.adminRecord).toMatchObject({
auth0Sub: testAdminAuth0Sub, id: testAdminId,
email: DEFAULT_ADMIN_EMAIL, email: DEFAULT_ADMIN_EMAIL,
}); });
}); });
it('should link admin record by email when auth0_sub differs', async () => { it('should link admin record by email when user_profile_id differs', async () => {
const placeholderSub = 'auth0|placeholder-sub'; const placeholderId = '9b9a1234-1234-1234-1234-123456789abc';
const realSub = 'auth0|real-admin-sub'; const realId = 'a1b2c3d4-5678-90ab-cdef-123456789def';
const email = 'link-admin@example.com'; const email = 'link-admin@example.com';
await pool.query(` 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) VALUES ($1, $2, $3, $4, $5)
`, [placeholderSub, email, 'admin', testAdminAuth0Sub]); `, [placeholderId, placeholderId, email, 'admin', testAdminId]);
currentUser = { currentUser = {
sub: realSub, sub: 'auth0|real-admin-sub',
email, email,
}; };
const response = await request(app) const response = await request(app.server)
.get('/api/admin/verify') .get('/api/admin/verify')
.expect(200); .expect(200);
expect(response.body.isAdmin).toBe(true); expect(response.body.isAdmin).toBe(true);
expect(response.body.adminRecord).toMatchObject({ expect(response.body.adminRecord).toMatchObject({
auth0Sub: realSub, userProfileId: realId,
email, email,
}); });
const record = await pool.query( 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] [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 () => { it('should return non-admin response for unknown user', async () => {
@@ -154,7 +161,7 @@ describe('Admin Management Integration Tests', () => {
email: 'non-admin@example.com', email: 'non-admin@example.com',
}; };
const response = await request(app) const response = await request(app.server)
.get('/api/admin/verify') .get('/api/admin/verify')
.expect(200); .expect(200);
@@ -166,17 +173,19 @@ describe('Admin Management Integration Tests', () => {
describe('GET /api/admin/admins', () => { describe('GET /api/admin/admins', () => {
it('should list all admin users', async () => { it('should list all admin users', async () => {
// Create additional test admins // Create additional test admins
const admin1Id = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';
const admin2Id = '8f14e45f-ceea-367f-a27f-c9a6d0c67e0e';
await pool.query(` 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 VALUES
($1, $2, $3, $4), ($1, $2, $3, $4, $5),
($5, $6, $7, $8) ($6, $7, $8, $9, $10)
`, [ `, [
'auth0|admin1', 'admin1@example.com', 'admin', testAdminAuth0Sub, admin1Id, admin1Id, 'admin1@example.com', 'admin', testAdminId,
'auth0|admin2', 'admin2@example.com', 'super_admin', testAdminAuth0Sub admin2Id, admin2Id, 'admin2@example.com', 'super_admin', testAdminId
]); ]);
const response = await request(app) const response = await request(app.server)
.get('/api/admin/admins') .get('/api/admin/admins')
.expect(200); .expect(200);
@@ -184,7 +193,7 @@ describe('Admin Management Integration Tests', () => {
expect(response.body).toHaveProperty('admins'); expect(response.body).toHaveProperty('admins');
expect(response.body.admins.length).toBeGreaterThanOrEqual(3); // At least test admin + 2 created expect(response.body.admins.length).toBeGreaterThanOrEqual(3); // At least test admin + 2 created
expect(response.body.admins[0]).toMatchObject({ expect(response.body.admins[0]).toMatchObject({
auth0Sub: expect.any(String), id: expect.any(String),
email: expect.any(String), email: expect.any(String),
role: expect.stringMatching(/^(admin|super_admin)$/), role: expect.stringMatching(/^(admin|super_admin)$/),
createdAt: expect.any(String), createdAt: expect.any(String),
@@ -194,12 +203,13 @@ describe('Admin Management Integration Tests', () => {
it('should include revoked admins in the list', async () => { it('should include revoked admins in the list', async () => {
// Create and revoke an admin // Create and revoke an admin
const revokedId = 'f1e2d3c4-b5a6-9788-6543-210fedcba987';
await pool.query(` await pool.query(`
INSERT INTO admin_users (auth0_sub, email, role, created_by, revoked_at) INSERT INTO admin_users (id, user_profile_id, email, role, created_by, revoked_at)
VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP) VALUES ($1, $2, $3, $4, $5, CURRENT_TIMESTAMP)
`, ['auth0|revoked', 'revoked@example.com', 'admin', testAdminAuth0Sub]); `, [revokedId, revokedId, 'revoked@example.com', 'admin', testAdminId]);
const response = await request(app) const response = await request(app.server)
.get('/api/admin/admins') .get('/api/admin/admins')
.expect(200); .expect(200);
@@ -218,17 +228,17 @@ describe('Admin Management Integration Tests', () => {
role: 'admin' role: 'admin'
}; };
const response = await request(app) const response = await request(app.server)
.post('/api/admin/admins') .post('/api/admin/admins')
.send(newAdminData) .send(newAdminData)
.expect(201); .expect(201);
expect(response.body).toMatchObject({ expect(response.body).toMatchObject({
auth0Sub: expect.any(String), id: expect.any(String),
email: 'newadmin@example.com', email: 'newadmin@example.com',
role: 'admin', role: 'admin',
createdAt: expect.any(String), createdAt: expect.any(String),
createdBy: testAdminAuth0Sub, createdBy: testAdminId,
revokedAt: null revokedAt: null
}); });
@@ -238,7 +248,7 @@ describe('Admin Management Integration Tests', () => {
['CREATE', 'newadmin@example.com'] ['CREATE', 'newadmin@example.com']
); );
expect(auditResult.rows.length).toBe(1); 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 () => { it('should reject invalid email', async () => {
@@ -247,7 +257,7 @@ describe('Admin Management Integration Tests', () => {
role: 'admin' role: 'admin'
}; };
const response = await request(app) const response = await request(app.server)
.post('/api/admin/admins') .post('/api/admin/admins')
.send(invalidData) .send(invalidData)
.expect(400); .expect(400);
@@ -263,13 +273,13 @@ describe('Admin Management Integration Tests', () => {
}; };
// Create first admin // Create first admin
await request(app) await request(app.server)
.post('/api/admin/admins') .post('/api/admin/admins')
.send(adminData) .send(adminData)
.expect(201); .expect(201);
// Try to create duplicate // Try to create duplicate
const response = await request(app) const response = await request(app.server)
.post('/api/admin/admins') .post('/api/admin/admins')
.send(adminData) .send(adminData)
.expect(400); .expect(400);
@@ -284,7 +294,7 @@ describe('Admin Management Integration Tests', () => {
role: 'super_admin' role: 'super_admin'
}; };
const response = await request(app) const response = await request(app.server)
.post('/api/admin/admins') .post('/api/admin/admins')
.send(superAdminData) .send(superAdminData)
.expect(201); .expect(201);
@@ -297,7 +307,7 @@ describe('Admin Management Integration Tests', () => {
email: 'defaultrole@example.com' email: 'defaultrole@example.com'
}; };
const response = await request(app) const response = await request(app.server)
.post('/api/admin/admins') .post('/api/admin/admins')
.send(adminData) .send(adminData)
.expect(201); .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 () => { it('should revoke admin access', async () => {
// Create admin to revoke // Create admin to revoke
const toRevokeId = 'b1c2d3e4-f5a6-7890-1234-567890abcdef';
const createResult = await pool.query(` const createResult = 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) VALUES ($1, $2, $3, $4, $5)
RETURNING auth0_sub RETURNING id
`, ['auth0|to-revoke', 'torevoke@example.com', 'admin', testAdminAuth0Sub]); `, [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) const response = await request(app.server)
.patch(`/api/admin/admins/${auth0Sub}/revoke`) .patch(`/api/admin/admins/${adminId}/revoke`)
.expect(200); .expect(200);
expect(response.body).toMatchObject({ expect(response.body).toMatchObject({
auth0Sub, id: adminId,
email: 'torevoke@example.com', email: 'torevoke@example.com',
revokedAt: expect.any(String) revokedAt: expect.any(String)
}); });
@@ -330,7 +341,7 @@ describe('Admin Management Integration Tests', () => {
// Verify audit log // Verify audit log
const auditResult = await pool.query( const auditResult = await pool.query(
'SELECT * FROM admin_audit_logs WHERE action = $1 AND target_admin_id = $2', 'SELECT * FROM admin_audit_logs WHERE action = $1 AND target_admin_id = $2',
['REVOKE', auth0Sub] ['REVOKE', adminId]
); );
expect(auditResult.rows.length).toBe(1); expect(auditResult.rows.length).toBe(1);
}); });
@@ -338,12 +349,12 @@ describe('Admin Management Integration Tests', () => {
it('should prevent revoking last active admin', async () => { it('should prevent revoking last active admin', async () => {
// First, ensure only one active admin exists // First, ensure only one active admin exists
await pool.query( await pool.query(
'UPDATE admin_users SET revoked_at = CURRENT_TIMESTAMP WHERE auth0_sub != $1', 'UPDATE admin_users SET revoked_at = CURRENT_TIMESTAMP WHERE user_profile_id != $1',
[testAdminAuth0Sub] [testAdminId]
); );
const response = await request(app) const response = await request(app.server)
.patch(`/api/admin/admins/${testAdminAuth0Sub}/revoke`) .patch(`/api/admin/admins/${testAdminId}/revoke`)
.expect(400); .expect(400);
expect(response.body.error).toBe('Bad Request'); 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 () => { it('should return 404 for non-existent admin', async () => {
const response = await request(app) const response = await request(app.server)
.patch('/api/admin/admins/auth0|nonexistent/revoke') .patch('/api/admin/admins/00000000-0000-0000-0000-000000000000/revoke')
.expect(404); .expect(404);
expect(response.body.error).toBe('Not Found'); 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 () => { it('should reinstate revoked admin', async () => {
// Create revoked admin // Create revoked admin
const reinstateId = 'c2d3e4f5-a6b7-8901-2345-678901bcdef0';
const createResult = await pool.query(` const createResult = await pool.query(`
INSERT INTO admin_users (auth0_sub, email, role, created_by, revoked_at) INSERT INTO admin_users (id, user_profile_id, email, role, created_by, revoked_at)
VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP) VALUES ($1, $2, $3, $4, $5, CURRENT_TIMESTAMP)
RETURNING auth0_sub RETURNING id
`, ['auth0|to-reinstate', 'toreinstate@example.com', 'admin', testAdminAuth0Sub]); `, [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) const response = await request(app.server)
.patch(`/api/admin/admins/${auth0Sub}/reinstate`) .patch(`/api/admin/admins/${adminId}/reinstate`)
.expect(200); .expect(200);
expect(response.body).toMatchObject({ expect(response.body).toMatchObject({
auth0Sub, id: adminId,
email: 'toreinstate@example.com', email: 'toreinstate@example.com',
revokedAt: null revokedAt: null
}); });
@@ -384,14 +396,14 @@ describe('Admin Management Integration Tests', () => {
// Verify audit log // Verify audit log
const auditResult = await pool.query( const auditResult = await pool.query(
'SELECT * FROM admin_audit_logs WHERE action = $1 AND target_admin_id = $2', 'SELECT * FROM admin_audit_logs WHERE action = $1 AND target_admin_id = $2',
['REINSTATE', auth0Sub] ['REINSTATE', adminId]
); );
expect(auditResult.rows.length).toBe(1); expect(auditResult.rows.length).toBe(1);
}); });
it('should return 404 for non-existent admin', async () => { it('should return 404 for non-existent admin', async () => {
const response = await request(app) const response = await request(app.server)
.patch('/api/admin/admins/auth0|nonexistent/reinstate') .patch('/api/admin/admins/00000000-0000-0000-0000-000000000000/reinstate')
.expect(404); .expect(404);
expect(response.body.error).toBe('Not Found'); expect(response.body.error).toBe('Not Found');
@@ -400,16 +412,17 @@ describe('Admin Management Integration Tests', () => {
it('should handle reinstating already active admin', async () => { it('should handle reinstating already active admin', async () => {
// Create active admin // Create active admin
const activeId = 'd3e4f5a6-b7c8-9012-3456-789012cdef01';
const createResult = await pool.query(` const createResult = 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) VALUES ($1, $2, $3, $4, $5)
RETURNING auth0_sub RETURNING id
`, ['auth0|already-active', 'active@example.com', 'admin', testAdminAuth0Sub]); `, [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) const response = await request(app.server)
.patch(`/api/admin/admins/${auth0Sub}/reinstate`) .patch(`/api/admin/admins/${adminId}/reinstate`)
.expect(200); .expect(200);
expect(response.body.revokedAt).toBeNull(); expect(response.body.revokedAt).toBeNull();
@@ -426,12 +439,12 @@ describe('Admin Management Integration Tests', () => {
($5, $6, $7, $8), ($5, $6, $7, $8),
($9, $10, $11, $12) ($9, $10, $11, $12)
`, [ `, [
testAdminAuth0Sub, 'CREATE', 'admin_user', 'test1@example.com', testAdminId, 'CREATE', 'admin_user', 'test1@example.com',
testAdminAuth0Sub, 'REVOKE', 'admin_user', 'test2@example.com', testAdminId, 'REVOKE', 'admin_user', 'test2@example.com',
testAdminAuth0Sub, 'REINSTATE', 'admin_user', 'test3@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') .get('/api/admin/audit-logs')
.expect(200); .expect(200);
@@ -440,7 +453,7 @@ describe('Admin Management Integration Tests', () => {
expect(response.body.logs.length).toBeGreaterThanOrEqual(3); expect(response.body.logs.length).toBeGreaterThanOrEqual(3);
expect(response.body.logs[0]).toMatchObject({ expect(response.body.logs[0]).toMatchObject({
id: expect.any(String), id: expect.any(String),
actorAdminId: testAdminAuth0Sub, actorAdminId: testAdminId,
action: expect.any(String), action: expect.any(String),
resourceType: expect.any(String), resourceType: expect.any(String),
createdAt: expect.any(String) createdAt: expect.any(String)
@@ -453,10 +466,10 @@ describe('Admin Management Integration Tests', () => {
await pool.query(` await pool.query(`
INSERT INTO admin_audit_logs (actor_admin_id, action, resource_type, resource_id) INSERT INTO admin_audit_logs (actor_admin_id, action, resource_type, resource_id)
VALUES ($1, $2, $3, $4) 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') .get('/api/admin/audit-logs?limit=5&offset=0')
.expect(200); .expect(200);
@@ -473,12 +486,12 @@ describe('Admin Management Integration Tests', () => {
($3, $4, CURRENT_TIMESTAMP - INTERVAL '1 minute'), ($3, $4, CURRENT_TIMESTAMP - INTERVAL '1 minute'),
($5, $6, CURRENT_TIMESTAMP) ($5, $6, CURRENT_TIMESTAMP)
`, [ `, [
testAdminAuth0Sub, 'FIRST', testAdminId, 'FIRST',
testAdminAuth0Sub, 'SECOND', testAdminId, 'SECOND',
testAdminAuth0Sub, 'THIRD' testAdminId, 'THIRD'
]); ]);
const response = await request(app) const response = await request(app.server)
.get('/api/admin/audit-logs?limit=3') .get('/api/admin/audit-logs?limit=3')
.expect(200); .expect(200);
@@ -491,45 +504,45 @@ describe('Admin Management Integration Tests', () => {
describe('End-to-end workflow', () => { describe('End-to-end workflow', () => {
it('should create, revoke, and reinstate admin with full audit trail', async () => { it('should create, revoke, and reinstate admin with full audit trail', async () => {
// 1. Create new admin // 1. Create new admin
const createResponse = await request(app) const createResponse = await request(app.server)
.post('/api/admin/admins') .post('/api/admin/admins')
.send({ email: 'workflow@example.com', role: 'admin' }) .send({ email: 'workflow@example.com', role: 'admin' })
.expect(201); .expect(201);
const auth0Sub = createResponse.body.auth0Sub; const adminId = createResponse.body.id;
// 2. Verify admin appears in list // 2. Verify admin appears in list
const listResponse = await request(app) const listResponse = await request(app.server)
.get('/api/admin/admins') .get('/api/admin/admins')
.expect(200); .expect(200);
const createdAdmin = listResponse.body.admins.find( const createdAdmin = listResponse.body.admins.find(
(admin: any) => admin.auth0Sub === auth0Sub (admin: any) => admin.id === adminId
); );
expect(createdAdmin).toBeDefined(); expect(createdAdmin).toBeDefined();
expect(createdAdmin.revokedAt).toBeNull(); expect(createdAdmin.revokedAt).toBeNull();
// 3. Revoke admin // 3. Revoke admin
const revokeResponse = await request(app) const revokeResponse = await request(app.server)
.patch(`/api/admin/admins/${auth0Sub}/revoke`) .patch(`/api/admin/admins/${adminId}/revoke`)
.expect(200); .expect(200);
expect(revokeResponse.body.revokedAt).toBeTruthy(); expect(revokeResponse.body.revokedAt).toBeTruthy();
// 4. Reinstate admin // 4. Reinstate admin
const reinstateResponse = await request(app) const reinstateResponse = await request(app.server)
.patch(`/api/admin/admins/${auth0Sub}/reinstate`) .patch(`/api/admin/admins/${adminId}/reinstate`)
.expect(200); .expect(200);
expect(reinstateResponse.body.revokedAt).toBeNull(); expect(reinstateResponse.body.revokedAt).toBeNull();
// 5. Verify complete audit trail // 5. Verify complete audit trail
const auditResponse = await request(app) const auditResponse = await request(app.server)
.get('/api/admin/audit-logs') .get('/api/admin/audit-logs')
.expect(200); .expect(200);
const workflowLogs = auditResponse.body.logs.filter( 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); expect(workflowLogs.length).toBeGreaterThanOrEqual(3);

View File

@@ -26,7 +26,7 @@ describe('admin guard plugin', () => {
fastify = Fastify(); fastify = Fastify();
authenticateMock = jest.fn(async (request: FastifyRequest) => { authenticateMock = jest.fn(async (request: FastifyRequest) => {
request.userContext = { request.userContext = {
userId: 'auth0|admin', userId: '7c9e6679-7425-40de-944b-e07fc1f90ae7',
email: 'admin@motovaultpro.com', email: 'admin@motovaultpro.com',
emailVerified: true, emailVerified: true,
onboardingCompleted: true, onboardingCompleted: true,
@@ -41,7 +41,7 @@ describe('admin guard plugin', () => {
mockPool = { mockPool = {
query: jest.fn().mockResolvedValue({ query: jest.fn().mockResolvedValue({
rows: [{ rows: [{
auth0_sub: 'auth0|admin', user_profile_id: '7c9e6679-7425-40de-944b-e07fc1f90ae7',
email: 'admin@motovaultpro.com', email: 'admin@motovaultpro.com',
role: 'admin', role: 'admin',
revoked_at: null, revoked_at: null,

View File

@@ -6,13 +6,23 @@
import { AdminService } from '../../domain/admin.service'; import { AdminService } from '../../domain/admin.service';
import { AdminRepository } from '../../data/admin.repository'; 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', () => { describe('AdminService', () => {
let adminService: AdminService; let adminService: AdminService;
let mockRepository: jest.Mocked<AdminRepository>; let mockRepository: jest.Mocked<AdminRepository>;
beforeEach(() => { beforeEach(() => {
mockRepository = { mockRepository = {
getAdminByAuth0Sub: jest.fn(), getAdminById: jest.fn(),
getAdminByUserProfileId: jest.fn(),
getAdminByEmail: jest.fn(), getAdminByEmail: jest.fn(),
getAllAdmins: jest.fn(), getAllAdmins: jest.fn(),
getActiveAdmins: jest.fn(), getActiveAdmins: jest.fn(),
@@ -26,30 +36,31 @@ describe('AdminService', () => {
adminService = new AdminService(mockRepository); adminService = new AdminService(mockRepository);
}); });
describe('getAdminByAuth0Sub', () => { describe('getAdminById', () => {
it('should return admin when found', async () => { it('should return admin when found', async () => {
const mockAdmin = { const mockAdmin = {
auth0Sub: 'auth0|123456', id: '7c9e6679-7425-40de-944b-e07fc1f90ae7',
userProfileId: '7c9e6679-7425-40de-944b-e07fc1f90ae7',
email: 'admin@motovaultpro.com', email: 'admin@motovaultpro.com',
role: 'admin', role: 'admin' as const,
createdAt: new Date(), createdAt: new Date(),
createdBy: 'system', createdBy: '550e8400-e29b-41d4-a716-446655440000',
revokedAt: null, revokedAt: null,
updatedAt: new Date(), 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(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 () => { 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(); expect(result).toBeNull();
}); });
@@ -57,12 +68,15 @@ describe('AdminService', () => {
describe('createAdmin', () => { describe('createAdmin', () => {
it('should create new admin and log audit', async () => { 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 = { const mockAdmin = {
auth0Sub: 'auth0|newadmin', id: newAdminId,
userProfileId: newAdminId,
email: 'newadmin@motovaultpro.com', email: 'newadmin@motovaultpro.com',
role: 'admin', role: 'admin' as const,
createdAt: new Date(), createdAt: new Date(),
createdBy: 'auth0|existing', createdBy: creatorId,
revokedAt: null, revokedAt: null,
updatedAt: new Date(), updatedAt: new Date(),
}; };
@@ -74,16 +88,16 @@ describe('AdminService', () => {
const result = await adminService.createAdmin( const result = await adminService.createAdmin(
'newadmin@motovaultpro.com', 'newadmin@motovaultpro.com',
'admin', 'admin',
'auth0|newadmin', newAdminId,
'auth0|existing' creatorId
); );
expect(result).toEqual(mockAdmin); expect(result).toEqual(mockAdmin);
expect(mockRepository.createAdmin).toHaveBeenCalled(); expect(mockRepository.createAdmin).toHaveBeenCalled();
expect(mockRepository.logAuditAction).toHaveBeenCalledWith( expect(mockRepository.logAuditAction).toHaveBeenCalledWith(
'auth0|existing', creatorId,
'CREATE', 'CREATE',
mockAdmin.auth0Sub, mockAdmin.id,
'admin_user', 'admin_user',
mockAdmin.email, mockAdmin.email,
expect.any(Object) expect.any(Object)
@@ -91,12 +105,14 @@ describe('AdminService', () => {
}); });
it('should reject if admin already exists', async () => { it('should reject if admin already exists', async () => {
const existingId = '7c9e6679-7425-40de-944b-e07fc1f90ae7';
const existingAdmin = { const existingAdmin = {
auth0Sub: 'auth0|existing', id: existingId,
userProfileId: existingId,
email: 'admin@motovaultpro.com', email: 'admin@motovaultpro.com',
role: 'admin', role: 'admin' as const,
createdAt: new Date(), createdAt: new Date(),
createdBy: 'system', createdBy: '550e8400-e29b-41d4-a716-446655440000',
revokedAt: null, revokedAt: null,
updatedAt: new Date(), updatedAt: new Date(),
}; };
@@ -104,39 +120,46 @@ describe('AdminService', () => {
mockRepository.getAdminByEmail.mockResolvedValue(existingAdmin); mockRepository.getAdminByEmail.mockResolvedValue(existingAdmin);
await expect( 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'); ).rejects.toThrow('already exists');
}); });
}); });
describe('revokeAdmin', () => { describe('revokeAdmin', () => {
it('should revoke admin when multiple active admins exist', async () => { 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 = { const revokedAdmin = {
auth0Sub: 'auth0|toadmin', id: toRevokeId,
userProfileId: toRevokeId,
email: 'toadmin@motovaultpro.com', email: 'toadmin@motovaultpro.com',
role: 'admin', role: 'admin' as const,
createdAt: new Date(), createdAt: new Date(),
createdBy: 'system', createdBy: '550e8400-e29b-41d4-a716-446655440000',
revokedAt: new Date(), revokedAt: new Date(),
updatedAt: new Date(), updatedAt: new Date(),
}; };
const activeAdmins = [ const activeAdmins = [
{ {
auth0Sub: 'auth0|admin1', id: admin1Id,
userProfileId: admin1Id,
email: 'admin1@motovaultpro.com', email: 'admin1@motovaultpro.com',
role: 'admin', role: 'admin' as const,
createdAt: new Date(), createdAt: new Date(),
createdBy: 'system', createdBy: '550e8400-e29b-41d4-a716-446655440000',
revokedAt: null, revokedAt: null,
updatedAt: new Date(), updatedAt: new Date(),
}, },
{ {
auth0Sub: 'auth0|admin2', id: admin2Id,
userProfileId: admin2Id,
email: 'admin2@motovaultpro.com', email: 'admin2@motovaultpro.com',
role: 'admin', role: 'admin' as const,
createdAt: new Date(), createdAt: new Date(),
createdBy: 'system', createdBy: '550e8400-e29b-41d4-a716-446655440000',
revokedAt: null, revokedAt: null,
updatedAt: new Date(), updatedAt: new Date(),
}, },
@@ -146,20 +169,22 @@ describe('AdminService', () => {
mockRepository.revokeAdmin.mockResolvedValue(revokedAdmin); mockRepository.revokeAdmin.mockResolvedValue(revokedAdmin);
mockRepository.logAuditAction.mockResolvedValue({} as any); 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(result).toEqual(revokedAdmin);
expect(mockRepository.revokeAdmin).toHaveBeenCalledWith('auth0|toadmin'); expect(mockRepository.revokeAdmin).toHaveBeenCalledWith(toRevokeId);
expect(mockRepository.logAuditAction).toHaveBeenCalled(); expect(mockRepository.logAuditAction).toHaveBeenCalled();
}); });
it('should prevent revoking last active admin', async () => { it('should prevent revoking last active admin', async () => {
const lastAdminId = '7c9e6679-7425-40de-944b-e07fc1f90ae7';
const lastAdmin = { const lastAdmin = {
auth0Sub: 'auth0|lastadmin', id: lastAdminId,
userProfileId: lastAdminId,
email: 'last@motovaultpro.com', email: 'last@motovaultpro.com',
role: 'admin', role: 'admin' as const,
createdAt: new Date(), createdAt: new Date(),
createdBy: 'system', createdBy: '550e8400-e29b-41d4-a716-446655440000',
revokedAt: null, revokedAt: null,
updatedAt: new Date(), updatedAt: new Date(),
}; };
@@ -167,19 +192,22 @@ describe('AdminService', () => {
mockRepository.getActiveAdmins.mockResolvedValue([lastAdmin]); mockRepository.getActiveAdmins.mockResolvedValue([lastAdmin]);
await expect( await expect(
adminService.revokeAdmin('auth0|lastadmin', 'auth0|lastadmin') adminService.revokeAdmin(lastAdminId, lastAdminId)
).rejects.toThrow('Cannot revoke the last active admin'); ).rejects.toThrow('Cannot revoke the last active admin');
}); });
}); });
describe('reinstateAdmin', () => { describe('reinstateAdmin', () => {
it('should reinstate revoked admin and log audit', async () => { 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 = { const reinstatedAdmin = {
auth0Sub: 'auth0|reinstate', id: reinstateId,
userProfileId: reinstateId,
email: 'reinstate@motovaultpro.com', email: 'reinstate@motovaultpro.com',
role: 'admin', role: 'admin' as const,
createdAt: new Date(), createdAt: new Date(),
createdBy: 'system', createdBy: '550e8400-e29b-41d4-a716-446655440000',
revokedAt: null, revokedAt: null,
updatedAt: new Date(), updatedAt: new Date(),
}; };
@@ -187,14 +215,14 @@ describe('AdminService', () => {
mockRepository.reinstateAdmin.mockResolvedValue(reinstatedAdmin); mockRepository.reinstateAdmin.mockResolvedValue(reinstatedAdmin);
mockRepository.logAuditAction.mockResolvedValue({} as any); 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(result).toEqual(reinstatedAdmin);
expect(mockRepository.reinstateAdmin).toHaveBeenCalledWith('auth0|reinstate'); expect(mockRepository.reinstateAdmin).toHaveBeenCalledWith(reinstateId);
expect(mockRepository.logAuditAction).toHaveBeenCalledWith( expect(mockRepository.logAuditAction).toHaveBeenCalledWith(
'auth0|admin', adminActorId,
'REINSTATE', 'REINSTATE',
'auth0|reinstate', reinstateId,
'admin_user', 'admin_user',
reinstatedAdmin.email reinstatedAdmin.email
); );

View File

@@ -32,7 +32,7 @@ describe('AuditLog Feature Integration', () => {
describe('Vehicle logging integration', () => { describe('Vehicle logging integration', () => {
it('should create audit log with vehicle category and correct resource', async () => { 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 vehicleId = 'vehicle-uuid-123';
const entry = await service.info( const entry = await service.info(
'vehicle', 'vehicle',
@@ -56,7 +56,7 @@ describe('AuditLog Feature Integration', () => {
}); });
it('should log vehicle update with correct fields', async () => { 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 vehicleId = 'vehicle-uuid-456';
const entry = await service.info( const entry = await service.info(
'vehicle', 'vehicle',
@@ -75,7 +75,7 @@ describe('AuditLog Feature Integration', () => {
}); });
it('should log vehicle deletion with vehicle info', async () => { 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 vehicleId = 'vehicle-uuid-789';
const entry = await service.info( const entry = await service.info(
'vehicle', 'vehicle',
@@ -96,7 +96,7 @@ describe('AuditLog Feature Integration', () => {
describe('Auth logging integration', () => { describe('Auth logging integration', () => {
it('should create audit log with auth category for signup', async () => { 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( const entry = await service.info(
'auth', 'auth',
userId, userId,
@@ -116,7 +116,7 @@ describe('AuditLog Feature Integration', () => {
}); });
it('should create audit log for password reset request', async () => { 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( const entry = await service.info(
'auth', 'auth',
userId, userId,
@@ -134,14 +134,14 @@ describe('AuditLog Feature Integration', () => {
describe('Admin logging integration', () => { describe('Admin logging integration', () => {
it('should create audit log for admin user creation', async () => { it('should create audit log for admin user creation', async () => {
const adminId = 'admin-user-123'; const adminId = '7c9e6679-7425-40de-944b-e07fc1f90ae7';
const targetAdminSub = 'auth0|target-admin-456'; const targetAdminId = '8f14e45f-ceea-367f-a27f-c9a6d0c67e0e';
const entry = await service.info( const entry = await service.info(
'admin', 'admin',
adminId, adminId,
'Admin user created: newadmin@example.com', 'Admin user created: newadmin@example.com',
'admin_user', 'admin_user',
targetAdminSub, targetAdminId,
{ email: 'newadmin@example.com', role: 'admin' } { email: 'newadmin@example.com', role: 'admin' }
); );
@@ -156,14 +156,14 @@ describe('AuditLog Feature Integration', () => {
}); });
it('should create audit log for admin revocation', async () => { it('should create audit log for admin revocation', async () => {
const adminId = 'admin-user-123'; const adminId = '7c9e6679-7425-40de-944b-e07fc1f90ae7';
const targetAdminSub = 'auth0|target-admin-789'; const targetAdminId = 'a1b2c3d4-e5f6-7890-1234-567890abcdef';
const entry = await service.info( const entry = await service.info(
'admin', 'admin',
adminId, adminId,
'Admin user revoked: revoked@example.com', 'Admin user revoked: revoked@example.com',
'admin_user', 'admin_user',
targetAdminSub, targetAdminId,
{ email: 'revoked@example.com' } { email: 'revoked@example.com' }
); );
@@ -174,14 +174,14 @@ describe('AuditLog Feature Integration', () => {
}); });
it('should create audit log for admin reinstatement', async () => { it('should create audit log for admin reinstatement', async () => {
const adminId = 'admin-user-123'; const adminId = '7c9e6679-7425-40de-944b-e07fc1f90ae7';
const targetAdminSub = 'auth0|target-admin-reinstated'; const targetAdminId = 'b2c3d4e5-f6a7-8901-2345-678901bcdef0';
const entry = await service.info( const entry = await service.info(
'admin', 'admin',
adminId, adminId,
'Admin user reinstated: reinstated@example.com', 'Admin user reinstated: reinstated@example.com',
'admin_user', 'admin_user',
targetAdminSub, targetAdminId,
{ email: 'reinstated@example.com' } { email: 'reinstated@example.com' }
); );
@@ -194,7 +194,7 @@ describe('AuditLog Feature Integration', () => {
describe('Backup/System logging integration', () => { describe('Backup/System logging integration', () => {
it('should create audit log for backup creation', async () => { 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 backupId = 'backup-uuid-123';
const entry = await service.info( const entry = await service.info(
'system', 'system',
@@ -215,7 +215,7 @@ describe('AuditLog Feature Integration', () => {
}); });
it('should create audit log for backup restore', async () => { 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 backupId = 'backup-uuid-456';
const entry = await service.info( const entry = await service.info(
'system', 'system',
@@ -233,7 +233,7 @@ describe('AuditLog Feature Integration', () => {
}); });
it('should create error-level audit log for backup failure', async () => { 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 backupId = 'backup-uuid-789';
const entry = await service.error( const entry = await service.error(
'system', 'system',
@@ -253,7 +253,7 @@ describe('AuditLog Feature Integration', () => {
}); });
it('should create error-level audit log for restore failure', async () => { 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 backupId = 'backup-uuid-restore-fail';
const entry = await service.error( const entry = await service.error(
'system', 'system',

View File

@@ -19,6 +19,7 @@ jest.mock('../../../../core/plugins/auth.plugin', () => {
return { return {
default: fastifyPlugin(async function (fastify) { default: fastifyPlugin(async function (fastify) {
fastify.decorate('authenticate', async function (request, _reply) { fastify.decorate('authenticate', async function (request, _reply) {
// JWT sub is still auth0|xxx format
request.user = { sub: 'auth0|test-user-123' }; request.user = { sub: 'auth0|test-user-123' };
}); });
}, { name: 'auth-plugin' }), }, { name: 'auth-plugin' }),

View File

@@ -103,6 +103,8 @@ describe('AuthService', () => {
onboardingCompletedAt: null, onboardingCompletedAt: null,
deactivatedAt: null, deactivatedAt: null,
deactivatedBy: null, deactivatedBy: null,
deletionRequestedAt: null,
deletionScheduledFor: null,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date(),
}); });
@@ -116,6 +118,8 @@ describe('AuthService', () => {
onboardingCompletedAt: null, onboardingCompletedAt: null,
deactivatedAt: null, deactivatedAt: null,
deactivatedBy: null, deactivatedBy: null,
deletionRequestedAt: null,
deletionScheduledFor: null,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date(),
}); });
@@ -149,6 +153,8 @@ describe('AuthService', () => {
onboardingCompletedAt: null, onboardingCompletedAt: null,
deactivatedAt: null, deactivatedAt: null,
deactivatedBy: null, deactivatedBy: null,
deletionRequestedAt: null,
deletionScheduledFor: null,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date(),
}); });

View File

@@ -73,7 +73,7 @@
}, },
"responseWithEfficiency": { "responseWithEfficiency": {
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"userId": "auth0|user123", "userId": "550e8400-e29b-41d4-a716-446655440000",
"vehicleId": "550e8400-e29b-41d4-a716-446655440000", "vehicleId": "550e8400-e29b-41d4-a716-446655440000",
"dateTime": "2024-01-15T10:30:00Z", "dateTime": "2024-01-15T10:30:00Z",
"odometerReading": 52000, "odometerReading": 52000,

View File

@@ -99,7 +99,7 @@
}, },
"maintenanceScheduleResponse": { "maintenanceScheduleResponse": {
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"userId": "auth0|user123", "userId": "550e8400-e29b-41d4-a716-446655440000",
"vehicleId": "550e8400-e29b-41d4-a716-446655440000", "vehicleId": "550e8400-e29b-41d4-a716-446655440000",
"type": "oil_change", "type": "oil_change",
"category": "routine_maintenance", "category": "routine_maintenance",

View File

@@ -12,8 +12,8 @@ describe('Community Stations API Integration Tests', () => {
let app: FastifyInstance; let app: FastifyInstance;
let pool: Pool; let pool: Pool;
const testUserId = 'auth0|test-user-123'; const testUserId = '550e8400-e29b-41d4-a716-446655440000';
const testAdminId = 'auth0|test-admin-123'; const testAdminId = '7c9e6679-7425-40de-944b-e07fc1f90ae7';
const mockStationData = { const mockStationData = {
name: 'Test Gas Station', name: 'Test Gas Station',

View File

@@ -48,7 +48,8 @@ describe('AdminUsersPage', () => {
mockUseAdminAccess.mockReturnValue({ mockUseAdminAccess.mockReturnValue({
isAdmin: true, isAdmin: true,
adminRecord: { adminRecord: {
auth0Sub: 'auth0|123', id: 'admin-uuid-123',
userProfileId: 'user-uuid-123',
email: 'admin@example.com', email: 'admin@example.com',
role: 'admin', role: 'admin',
createdAt: '2024-01-01', createdAt: '2024-01-01',

View File

@@ -55,7 +55,8 @@ describe('useAdminAccess', () => {
mockAdminApi.verifyAccess.mockResolvedValue({ mockAdminApi.verifyAccess.mockResolvedValue({
isAdmin: true, isAdmin: true,
adminRecord: { adminRecord: {
auth0Sub: 'auth0|123', id: 'admin-uuid-123',
userProfileId: 'user-uuid-123',
email: 'admin@example.com', email: 'admin@example.com',
role: 'admin', role: 'admin',
createdAt: '2024-01-01', createdAt: '2024-01-01',

View File

@@ -42,7 +42,8 @@ describe('Admin user management hooks', () => {
it('should fetch admin users', async () => { it('should fetch admin users', async () => {
const mockAdmins = [ const mockAdmins = [
{ {
auth0Sub: 'auth0|123', id: 'admin-uuid-123',
userProfileId: 'user-uuid-123',
email: 'admin1@example.com', email: 'admin1@example.com',
role: 'admin', role: 'admin',
createdAt: '2024-01-01', createdAt: '2024-01-01',
@@ -68,11 +69,12 @@ describe('Admin user management hooks', () => {
describe('useCreateAdmin', () => { describe('useCreateAdmin', () => {
it('should create admin and show success toast', async () => { it('should create admin and show success toast', async () => {
const newAdmin = { const newAdmin = {
auth0Sub: 'auth0|456', id: 'admin-uuid-456',
userProfileId: 'user-uuid-456',
email: 'newadmin@example.com', email: 'newadmin@example.com',
role: 'admin', role: 'admin',
createdAt: '2024-01-01', createdAt: '2024-01-01',
createdBy: 'auth0|123', createdBy: 'admin-uuid-123',
revokedAt: null, revokedAt: null,
updatedAt: '2024-01-01', updatedAt: '2024-01-01',
}; };
@@ -131,11 +133,11 @@ describe('Admin user management hooks', () => {
wrapper: createWrapper(), wrapper: createWrapper(),
}); });
result.current.mutate('auth0|123'); result.current.mutate('admin-uuid-123');
await waitFor(() => expect(result.current.isSuccess).toBe(true)); 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'); expect(toast.success).toHaveBeenCalledWith('Admin revoked successfully');
}); });
}); });
@@ -148,11 +150,11 @@ describe('Admin user management hooks', () => {
wrapper: createWrapper(), wrapper: createWrapper(),
}); });
result.current.mutate('auth0|123'); result.current.mutate('admin-uuid-123');
await waitFor(() => expect(result.current.isSuccess).toBe(true)); 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'); expect(toast.success).toHaveBeenCalledWith('Admin reinstated successfully');
}); });
}); });

View File

@@ -101,12 +101,12 @@ export const adminApi = {
return response.data; return response.data;
}, },
revokeAdmin: async (auth0Sub: string): Promise<void> => { revokeAdmin: async (id: string): Promise<void> => {
await apiClient.patch(`/admin/admins/${auth0Sub}/revoke`); await apiClient.patch(`/admin/admins/${id}/revoke`);
}, },
reinstateAdmin: async (auth0Sub: string): Promise<void> => { reinstateAdmin: async (id: string): Promise<void> => {
await apiClient.patch(`/admin/admins/${auth0Sub}/reinstate`); await apiClient.patch(`/admin/admins/${id}/reinstate`);
}, },
// Audit logs // Audit logs
@@ -328,62 +328,62 @@ export const adminApi = {
return response.data; return response.data;
}, },
get: async (auth0Sub: string): Promise<ManagedUser> => { get: async (userId: string): Promise<ManagedUser> => {
const response = await apiClient.get<ManagedUser>( const response = await apiClient.get<ManagedUser>(
`/admin/users/${encodeURIComponent(auth0Sub)}` `/admin/users/${userId}`
); );
return response.data; return response.data;
}, },
getVehicles: async (auth0Sub: string): Promise<AdminUserVehiclesResponse> => { getVehicles: async (userId: string): Promise<AdminUserVehiclesResponse> => {
const response = await apiClient.get<AdminUserVehiclesResponse>( const response = await apiClient.get<AdminUserVehiclesResponse>(
`/admin/users/${encodeURIComponent(auth0Sub)}/vehicles` `/admin/users/${userId}/vehicles`
); );
return response.data; return response.data;
}, },
updateTier: async (auth0Sub: string, data: UpdateUserTierRequest): Promise<ManagedUser> => { updateTier: async (userId: string, data: UpdateUserTierRequest): Promise<ManagedUser> => {
const response = await apiClient.patch<ManagedUser>( const response = await apiClient.patch<ManagedUser>(
`/admin/users/${encodeURIComponent(auth0Sub)}/tier`, `/admin/users/${userId}/tier`,
data data
); );
return response.data; return response.data;
}, },
deactivate: async (auth0Sub: string, data?: DeactivateUserRequest): Promise<ManagedUser> => { deactivate: async (userId: string, data?: DeactivateUserRequest): Promise<ManagedUser> => {
const response = await apiClient.patch<ManagedUser>( const response = await apiClient.patch<ManagedUser>(
`/admin/users/${encodeURIComponent(auth0Sub)}/deactivate`, `/admin/users/${userId}/deactivate`,
data || {} data || {}
); );
return response.data; return response.data;
}, },
reactivate: async (auth0Sub: string): Promise<ManagedUser> => { reactivate: async (userId: string): Promise<ManagedUser> => {
const response = await apiClient.patch<ManagedUser>( const response = await apiClient.patch<ManagedUser>(
`/admin/users/${encodeURIComponent(auth0Sub)}/reactivate` `/admin/users/${userId}/reactivate`
); );
return response.data; return response.data;
}, },
updateProfile: async (auth0Sub: string, data: UpdateUserProfileRequest): Promise<ManagedUser> => { updateProfile: async (userId: string, data: UpdateUserProfileRequest): Promise<ManagedUser> => {
const response = await apiClient.patch<ManagedUser>( const response = await apiClient.patch<ManagedUser>(
`/admin/users/${encodeURIComponent(auth0Sub)}/profile`, `/admin/users/${userId}/profile`,
data data
); );
return response.data; return response.data;
}, },
promoteToAdmin: async (auth0Sub: string, data?: PromoteToAdminRequest): Promise<AdminUser> => { promoteToAdmin: async (userId: string, data?: PromoteToAdminRequest): Promise<AdminUser> => {
const response = await apiClient.patch<AdminUser>( const response = await apiClient.patch<AdminUser>(
`/admin/users/${encodeURIComponent(auth0Sub)}/promote`, `/admin/users/${userId}/promote`,
data || {} data || {}
); );
return response.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 }>( const response = await apiClient.delete<{ message: string }>(
`/admin/users/${encodeURIComponent(auth0Sub)}`, `/admin/users/${userId}`,
{ params: reason ? { reason } : undefined } { params: reason ? { reason } : undefined }
); );
return response.data; return response.data;

View File

@@ -51,7 +51,7 @@ export const useRevokeAdmin = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: (auth0Sub: string) => adminApi.revokeAdmin(auth0Sub), mutationFn: (id: string) => adminApi.revokeAdmin(id),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admins'] }); queryClient.invalidateQueries({ queryKey: ['admins'] });
toast.success('Admin revoked successfully'); toast.success('Admin revoked successfully');
@@ -66,7 +66,7 @@ export const useReinstateAdmin = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: (auth0Sub: string) => adminApi.reinstateAdmin(auth0Sub), mutationFn: (id: string) => adminApi.reinstateAdmin(id),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admins'] }); queryClient.invalidateQueries({ queryKey: ['admins'] });
toast.success('Admin reinstated successfully'); toast.success('Admin reinstated successfully');

View File

@@ -29,8 +29,8 @@ interface ApiError {
export const userQueryKeys = { export const userQueryKeys = {
all: ['admin-users'] as const, all: ['admin-users'] as const,
list: (params: ListUsersParams) => [...userQueryKeys.all, 'list', params] as const, list: (params: ListUsersParams) => [...userQueryKeys.all, 'list', params] as const,
detail: (auth0Sub: string) => [...userQueryKeys.all, 'detail', auth0Sub] as const, detail: (userId: string) => [...userQueryKeys.all, 'detail', userId] as const,
vehicles: (auth0Sub: string) => [...userQueryKeys.all, 'vehicles', auth0Sub] as const, vehicles: (userId: string) => [...userQueryKeys.all, 'vehicles', userId] as const,
}; };
// Query keys for admin stats // Query keys for admin stats
@@ -58,13 +58,13 @@ export const useUsers = (params: ListUsersParams = {}) => {
/** /**
* Hook to get a single user's details * Hook to get a single user's details
*/ */
export const useUser = (auth0Sub: string) => { export const useUser = (userId: string) => {
const { isAuthenticated, isLoading } = useAuth0(); const { isAuthenticated, isLoading } = useAuth0();
return useQuery({ return useQuery({
queryKey: userQueryKeys.detail(auth0Sub), queryKey: userQueryKeys.detail(userId),
queryFn: () => adminApi.users.get(auth0Sub), queryFn: () => adminApi.users.get(userId),
enabled: isAuthenticated && !isLoading && !!auth0Sub, enabled: isAuthenticated && !isLoading && !!userId,
staleTime: 2 * 60 * 1000, staleTime: 2 * 60 * 1000,
gcTime: 5 * 60 * 1000, gcTime: 5 * 60 * 1000,
retry: 1, retry: 1,
@@ -78,8 +78,8 @@ export const useUpdateUserTier = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: ({ auth0Sub, data }: { auth0Sub: string; data: UpdateUserTierRequest }) => mutationFn: ({ userId, data }: { userId: string; data: UpdateUserTierRequest }) =>
adminApi.users.updateTier(auth0Sub, data), adminApi.users.updateTier(userId, data),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: userQueryKeys.all }); queryClient.invalidateQueries({ queryKey: userQueryKeys.all });
toast.success('Subscription tier updated'); toast.success('Subscription tier updated');
@@ -101,8 +101,8 @@ export const useDeactivateUser = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: ({ auth0Sub, data }: { auth0Sub: string; data?: DeactivateUserRequest }) => mutationFn: ({ userId, data }: { userId: string; data?: DeactivateUserRequest }) =>
adminApi.users.deactivate(auth0Sub, data), adminApi.users.deactivate(userId, data),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: userQueryKeys.all }); queryClient.invalidateQueries({ queryKey: userQueryKeys.all });
toast.success('User deactivated'); toast.success('User deactivated');
@@ -124,7 +124,7 @@ export const useReactivateUser = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: (auth0Sub: string) => adminApi.users.reactivate(auth0Sub), mutationFn: (userId: string) => adminApi.users.reactivate(userId),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: userQueryKeys.all }); queryClient.invalidateQueries({ queryKey: userQueryKeys.all });
toast.success('User reactivated'); toast.success('User reactivated');
@@ -146,8 +146,8 @@ export const useUpdateUserProfile = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: ({ auth0Sub, data }: { auth0Sub: string; data: UpdateUserProfileRequest }) => mutationFn: ({ userId, data }: { userId: string; data: UpdateUserProfileRequest }) =>
adminApi.users.updateProfile(auth0Sub, data), adminApi.users.updateProfile(userId, data),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: userQueryKeys.all }); queryClient.invalidateQueries({ queryKey: userQueryKeys.all });
toast.success('User profile updated'); toast.success('User profile updated');
@@ -169,8 +169,8 @@ export const usePromoteToAdmin = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: ({ auth0Sub, data }: { auth0Sub: string; data?: PromoteToAdminRequest }) => mutationFn: ({ userId, data }: { userId: string; data?: PromoteToAdminRequest }) =>
adminApi.users.promoteToAdmin(auth0Sub, data), adminApi.users.promoteToAdmin(userId, data),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: userQueryKeys.all }); queryClient.invalidateQueries({ queryKey: userQueryKeys.all });
toast.success('User promoted to admin'); toast.success('User promoted to admin');
@@ -192,8 +192,8 @@ export const useHardDeleteUser = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: ({ auth0Sub, reason }: { auth0Sub: string; reason?: string }) => mutationFn: ({ userId, reason }: { userId: string; reason?: string }) =>
adminApi.users.hardDelete(auth0Sub, reason), adminApi.users.hardDelete(userId, reason),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: userQueryKeys.all }); queryClient.invalidateQueries({ queryKey: userQueryKeys.all });
toast.success('User permanently deleted'); 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) * 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(); const { isAuthenticated, isLoading } = useAuth0();
return useQuery({ return useQuery({
queryKey: userQueryKeys.vehicles(auth0Sub), queryKey: userQueryKeys.vehicles(userId),
queryFn: () => adminApi.users.getVehicles(auth0Sub), queryFn: () => adminApi.users.getVehicles(userId),
enabled: isAuthenticated && !isLoading && !!auth0Sub, enabled: isAuthenticated && !isLoading && !!userId,
staleTime: 2 * 60 * 1000, staleTime: 2 * 60 * 1000,
gcTime: 5 * 60 * 1000, gcTime: 5 * 60 * 1000,
retry: 1, retry: 1,

View File

@@ -104,8 +104,8 @@ const VehicleCountBadge: React.FC<{ count: number; onClick?: () => void }> = ({
); );
// Expandable vehicle list component // Expandable vehicle list component
const UserVehiclesList: React.FC<{ auth0Sub: string; isOpen: boolean }> = ({ auth0Sub, isOpen }) => { const UserVehiclesList: React.FC<{ userId: string; isOpen: boolean }> = ({ userId, isOpen }) => {
const { data, isLoading, error } = useUserVehicles(auth0Sub); const { data, isLoading, error } = useUserVehicles(userId);
if (!isOpen) return null; if (!isOpen) return null;
@@ -215,7 +215,7 @@ export const AdminUsersMobileScreen: React.FC = () => {
(newTier: SubscriptionTier) => { (newTier: SubscriptionTier) => {
if (selectedUser) { if (selectedUser) {
updateTierMutation.mutate( updateTierMutation.mutate(
{ auth0Sub: selectedUser.auth0Sub, data: { subscriptionTier: newTier } }, { userId: selectedUser.id, data: { subscriptionTier: newTier } },
{ {
onSuccess: () => { onSuccess: () => {
setShowTierPicker(false); setShowTierPicker(false);
@@ -232,7 +232,7 @@ export const AdminUsersMobileScreen: React.FC = () => {
const handleDeactivate = useCallback(() => { const handleDeactivate = useCallback(() => {
if (selectedUser) { if (selectedUser) {
deactivateMutation.mutate( deactivateMutation.mutate(
{ auth0Sub: selectedUser.auth0Sub, data: { reason: deactivateReason || undefined } }, { userId: selectedUser.id, data: { reason: deactivateReason || undefined } },
{ {
onSuccess: () => { onSuccess: () => {
setShowDeactivateConfirm(false); setShowDeactivateConfirm(false);
@@ -247,7 +247,7 @@ export const AdminUsersMobileScreen: React.FC = () => {
const handleReactivate = useCallback(() => { const handleReactivate = useCallback(() => {
if (selectedUser) { if (selectedUser) {
reactivateMutation.mutate(selectedUser.auth0Sub, { reactivateMutation.mutate(selectedUser.id, {
onSuccess: () => { onSuccess: () => {
setShowUserActions(false); setShowUserActions(false);
setSelectedUser(null); setSelectedUser(null);
@@ -276,7 +276,7 @@ export const AdminUsersMobileScreen: React.FC = () => {
} }
if (Object.keys(updates).length > 0) { if (Object.keys(updates).length > 0) {
updateProfileMutation.mutate( updateProfileMutation.mutate(
{ auth0Sub: selectedUser.auth0Sub, data: updates }, { userId: selectedUser.id, data: updates },
{ {
onSuccess: () => { onSuccess: () => {
setShowEditModal(false); setShowEditModal(false);
@@ -306,7 +306,7 @@ export const AdminUsersMobileScreen: React.FC = () => {
const handlePromoteConfirm = useCallback(() => { const handlePromoteConfirm = useCallback(() => {
if (selectedUser) { if (selectedUser) {
promoteToAdminMutation.mutate( promoteToAdminMutation.mutate(
{ auth0Sub: selectedUser.auth0Sub, data: { role: promoteRole } }, { userId: selectedUser.id, data: { role: promoteRole } },
{ {
onSuccess: () => { onSuccess: () => {
setShowPromoteModal(false); setShowPromoteModal(false);
@@ -332,7 +332,7 @@ export const AdminUsersMobileScreen: React.FC = () => {
const handleHardDeleteConfirm = useCallback(() => { const handleHardDeleteConfirm = useCallback(() => {
if (selectedUser && hardDeleteConfirmText === 'DELETE') { if (selectedUser && hardDeleteConfirmText === 'DELETE') {
hardDeleteMutation.mutate( hardDeleteMutation.mutate(
{ auth0Sub: selectedUser.auth0Sub, reason: hardDeleteReason || undefined }, { userId: selectedUser.id, reason: hardDeleteReason || undefined },
{ {
onSuccess: () => { onSuccess: () => {
setShowHardDeleteModal(false); setShowHardDeleteModal(false);
@@ -509,7 +509,7 @@ export const AdminUsersMobileScreen: React.FC = () => {
{users.length > 0 && ( {users.length > 0 && (
<div className="space-y-3"> <div className="space-y-3">
{users.map((user) => ( {users.map((user) => (
<GlassCard key={user.auth0Sub} padding="md"> <GlassCard key={user.id} padding="md">
<button <button
onClick={() => handleUserClick(user)} onClick={() => handleUserClick(user)}
className="w-full text-left min-h-[44px]" className="w-full text-left min-h-[44px]"
@@ -526,7 +526,7 @@ export const AdminUsersMobileScreen: React.FC = () => {
<VehicleCountBadge <VehicleCountBadge
count={user.vehicleCount} count={user.vehicleCount}
onClick={user.vehicleCount > 0 ? () => setExpandedUserId( onClick={user.vehicleCount > 0 ? () => setExpandedUserId(
expandedUserId === user.auth0Sub ? null : user.auth0Sub expandedUserId === user.id ? null : user.id
) : undefined} ) : undefined}
/> />
<StatusBadge active={!user.deactivatedAt} /> <StatusBadge active={!user.deactivatedAt} />
@@ -543,8 +543,8 @@ export const AdminUsersMobileScreen: React.FC = () => {
</div> </div>
</button> </button>
<UserVehiclesList <UserVehiclesList
auth0Sub={user.auth0Sub} userId={user.id}
isOpen={expandedUserId === user.auth0Sub} isOpen={expandedUserId === user.id}
/> />
</GlassCard> </GlassCard>
))} ))}

View File

@@ -5,7 +5,8 @@
// Admin user types // Admin user types
export interface AdminUser { export interface AdminUser {
auth0Sub: string; id: string;
userProfileId: string;
email: string; email: string;
role: string; role: string;
createdAt: string; createdAt: string;

View File

@@ -71,8 +71,8 @@ import { AdminSectionHeader } from '../../features/admin/components';
const PAGE_SIZE_OPTIONS = [10, 20, 50, 100]; const PAGE_SIZE_OPTIONS = [10, 20, 50, 100];
// Expandable vehicle row component // Expandable vehicle row component
const UserVehiclesRow: React.FC<{ auth0Sub: string; isOpen: boolean }> = ({ auth0Sub, isOpen }) => { const UserVehiclesRow: React.FC<{ userId: string; isOpen: boolean }> = ({ userId, isOpen }) => {
const { data, isLoading, error } = useUserVehicles(auth0Sub); const { data, isLoading, error } = useUserVehicles(userId);
if (!isOpen) return null; if (!isOpen) return null;
@@ -222,8 +222,8 @@ export const AdminUsersPage: React.FC = () => {
}, []); }, []);
const handleTierChange = useCallback( const handleTierChange = useCallback(
(auth0Sub: string, newTier: SubscriptionTier) => { (userId: string, newTier: SubscriptionTier) => {
updateTierMutation.mutate({ auth0Sub, data: { subscriptionTier: newTier } }); updateTierMutation.mutate({ userId, data: { subscriptionTier: newTier } });
}, },
[updateTierMutation] [updateTierMutation]
); );
@@ -246,7 +246,7 @@ export const AdminUsersPage: React.FC = () => {
const handleDeactivateConfirm = useCallback(() => { const handleDeactivateConfirm = useCallback(() => {
if (selectedUser) { if (selectedUser) {
deactivateMutation.mutate( deactivateMutation.mutate(
{ auth0Sub: selectedUser.auth0Sub, data: { reason: deactivateReason || undefined } }, { userId: selectedUser.id, data: { reason: deactivateReason || undefined } },
{ {
onSuccess: () => { onSuccess: () => {
setDeactivateDialogOpen(false); setDeactivateDialogOpen(false);
@@ -260,7 +260,7 @@ export const AdminUsersPage: React.FC = () => {
const handleReactivate = useCallback(() => { const handleReactivate = useCallback(() => {
if (selectedUser) { if (selectedUser) {
reactivateMutation.mutate(selectedUser.auth0Sub); reactivateMutation.mutate(selectedUser.id);
setAnchorEl(null); setAnchorEl(null);
setSelectedUser(null); setSelectedUser(null);
} }
@@ -286,7 +286,7 @@ export const AdminUsersPage: React.FC = () => {
} }
if (Object.keys(updates).length > 0) { if (Object.keys(updates).length > 0) {
updateProfileMutation.mutate( updateProfileMutation.mutate(
{ auth0Sub: selectedUser.auth0Sub, data: updates }, { userId: selectedUser.id, data: updates },
{ {
onSuccess: () => { onSuccess: () => {
setEditDialogOpen(false); setEditDialogOpen(false);
@@ -316,7 +316,7 @@ export const AdminUsersPage: React.FC = () => {
const handlePromoteConfirm = useCallback(() => { const handlePromoteConfirm = useCallback(() => {
if (selectedUser) { if (selectedUser) {
promoteToAdminMutation.mutate( promoteToAdminMutation.mutate(
{ auth0Sub: selectedUser.auth0Sub, data: { role: promoteRole } }, { userId: selectedUser.id, data: { role: promoteRole } },
{ {
onSuccess: () => { onSuccess: () => {
setPromoteDialogOpen(false); setPromoteDialogOpen(false);
@@ -342,7 +342,7 @@ export const AdminUsersPage: React.FC = () => {
const handleHardDeleteConfirm = useCallback(() => { const handleHardDeleteConfirm = useCallback(() => {
if (selectedUser && hardDeleteConfirmText === 'DELETE') { if (selectedUser && hardDeleteConfirmText === 'DELETE') {
hardDeleteMutation.mutate( hardDeleteMutation.mutate(
{ auth0Sub: selectedUser.auth0Sub, reason: hardDeleteReason || undefined }, { userId: selectedUser.id, reason: hardDeleteReason || undefined },
{ {
onSuccess: () => { onSuccess: () => {
setHardDeleteDialogOpen(false); setHardDeleteDialogOpen(false);
@@ -496,11 +496,11 @@ export const AdminUsersPage: React.FC = () => {
</TableHead> </TableHead>
<TableBody> <TableBody>
{users.map((user) => ( {users.map((user) => (
<React.Fragment key={user.auth0Sub}> <React.Fragment key={user.id}>
<TableRow <TableRow
sx={{ sx={{
opacity: user.deactivatedAt ? 0.6 : 1, opacity: user.deactivatedAt ? 0.6 : 1,
'& > *': { borderBottom: expandedRow === user.auth0Sub ? 'unset' : undefined }, '& > *': { borderBottom: expandedRow === user.id ? 'unset' : undefined },
}} }}
> >
<TableCell>{user.email}</TableCell> <TableCell>{user.email}</TableCell>
@@ -510,7 +510,7 @@ export const AdminUsersPage: React.FC = () => {
<Select <Select
value={user.subscriptionTier} value={user.subscriptionTier}
onChange={(e) => onChange={(e) =>
handleTierChange(user.auth0Sub, e.target.value as SubscriptionTier) handleTierChange(user.id, e.target.value as SubscriptionTier)
} }
disabled={!!user.deactivatedAt || updateTierMutation.isPending} disabled={!!user.deactivatedAt || updateTierMutation.isPending}
size="small" size="small"
@@ -527,12 +527,12 @@ export const AdminUsersPage: React.FC = () => {
<IconButton <IconButton
size="small" size="small"
onClick={() => setExpandedRow( onClick={() => setExpandedRow(
expandedRow === user.auth0Sub ? null : user.auth0Sub expandedRow === user.id ? null : user.id
)} )}
aria-label="show vehicles" aria-label="show vehicles"
sx={{ minWidth: 44, minHeight: 44 }} sx={{ minWidth: 44, minHeight: 44 }}
> >
{expandedRow === user.auth0Sub ? ( {expandedRow === user.id ? (
<KeyboardArrowUp fontSize="small" /> <KeyboardArrowUp fontSize="small" />
) : ( ) : (
<KeyboardArrowDown fontSize="small" /> <KeyboardArrowDown fontSize="small" />
@@ -569,8 +569,8 @@ export const AdminUsersPage: React.FC = () => {
</TableCell> </TableCell>
</TableRow> </TableRow>
<UserVehiclesRow <UserVehiclesRow
auth0Sub={user.auth0Sub} userId={user.id}
isOpen={expandedRow === user.auth0Sub} isOpen={expandedRow === user.id}
/> />
</React.Fragment> </React.Fragment>
))} ))}