diff --git a/backend/src/features/audit-log/data/audit-log.repository.ts b/backend/src/features/audit-log/data/audit-log.repository.ts index 622e5b7..26c75c6 100644 --- a/backend/src/features/audit-log/data/audit-log.repository.ts +++ b/backend/src/features/audit-log/data/audit-log.repository.ts @@ -126,7 +126,7 @@ export class AuditLogRepository { al.resource_type, al.resource_id, al.details, al.created_at, up.email as user_email FROM audit_logs al - LEFT JOIN user_profiles up ON al.user_id = up.auth0_sub + LEFT JOIN user_profiles up ON al.user_id = up.id ${whereClause} ORDER BY al.created_at DESC LIMIT $${nextParamIndex} OFFSET $${nextParamIndex + 1} @@ -170,7 +170,7 @@ export class AuditLogRepository { al.resource_type, al.resource_id, al.details, al.created_at, up.email as user_email FROM audit_logs al - LEFT JOIN user_profiles up ON al.user_id = up.auth0_sub + LEFT JOIN user_profiles up ON al.user_id = up.id ${whereClause} ORDER BY al.created_at DESC LIMIT ${MAX_EXPORT_RECORDS} diff --git a/backend/src/features/backup/api/backup.controller.ts b/backend/src/features/backup/api/backup.controller.ts index 028e7ca..f84b899 100644 --- a/backend/src/features/backup/api/backup.controller.ts +++ b/backend/src/features/backup/api/backup.controller.ts @@ -45,12 +45,12 @@ export class BackupController { request: FastifyRequest<{ Body: CreateBackupBody }>, reply: FastifyReply ): Promise { - const adminSub = (request as any).userContext?.auth0Sub; + const adminUserId = request.userContext?.userId; const result = await this.backupService.createBackup({ name: request.body.name, backupType: 'manual', - createdBy: adminSub, + createdBy: adminUserId, includeDocuments: request.body.includeDocuments, }); @@ -58,7 +58,7 @@ export class BackupController { // Log backup creation to unified audit log await auditLogService.info( 'system', - adminSub || null, + adminUserId || null, `Backup created: ${request.body.name || 'Manual backup'}`, 'backup', result.backupId, @@ -74,7 +74,7 @@ export class BackupController { // Log backup failure await auditLogService.error( 'system', - adminSub || null, + adminUserId || null, `Backup failed: ${request.body.name || 'Manual backup'}`, 'backup', result.backupId, @@ -139,7 +139,7 @@ export class BackupController { request: FastifyRequest, reply: FastifyReply ): Promise { - const adminSub = (request as any).userContext?.auth0Sub; + const adminUserId = request.userContext?.userId; // Handle multipart file upload const data = await request.file(); @@ -173,7 +173,7 @@ export class BackupController { const backup = await this.backupService.importUploadedBackup( tempPath, filename, - adminSub + adminUserId ); reply.status(201).send({ @@ -217,7 +217,7 @@ export class BackupController { request: FastifyRequest<{ Params: BackupIdParam; Body: RestoreBody }>, reply: FastifyReply ): Promise { - const adminSub = (request as any).userContext?.auth0Sub; + const adminUserId = request.userContext?.userId; try { const result = await this.restoreService.executeRestore({ @@ -229,7 +229,7 @@ export class BackupController { // Log successful restore to unified audit log await auditLogService.info( 'system', - adminSub || null, + adminUserId || null, `Backup restored: ${request.params.id}`, 'backup', request.params.id, @@ -246,7 +246,7 @@ export class BackupController { // Log restore failure await auditLogService.error( 'system', - adminSub || null, + adminUserId || null, `Backup restore failed: ${request.params.id}`, 'backup', request.params.id, diff --git a/backend/src/features/ocr/api/ocr.controller.ts b/backend/src/features/ocr/api/ocr.controller.ts index 18860a6..fcc54cd 100644 --- a/backend/src/features/ocr/api/ocr.controller.ts +++ b/backend/src/features/ocr/api/ocr.controller.ts @@ -33,7 +33,7 @@ export class OcrController { request: FastifyRequest<{ Querystring: ExtractQuery }>, reply: FastifyReply ) { - const userId = (request as any).user?.sub as string; + const userId = request.userContext?.userId as string; const preprocess = request.query.preprocess !== false; logger.info('OCR extract requested', { @@ -140,7 +140,7 @@ export class OcrController { request: FastifyRequest, reply: FastifyReply ) { - const userId = (request as any).user?.sub as string; + const userId = request.userContext?.userId as string; logger.info('VIN extract requested', { operation: 'ocr.controller.extractVin', @@ -240,7 +240,7 @@ export class OcrController { request: FastifyRequest, reply: FastifyReply ) { - const userId = (request as any).user?.sub as string; + const userId = request.userContext?.userId as string; logger.info('Receipt extract requested', { operation: 'ocr.controller.extractReceipt', @@ -352,7 +352,7 @@ export class OcrController { request: FastifyRequest, reply: FastifyReply ) { - const userId = (request as any).user?.sub as string; + const userId = request.userContext?.userId as string; logger.info('Maintenance receipt extract requested', { operation: 'ocr.controller.extractMaintenanceReceipt', @@ -460,7 +460,7 @@ export class OcrController { request: FastifyRequest, reply: FastifyReply ) { - const userId = (request as any).user?.sub as string; + const userId = request.userContext?.userId as string; logger.info('Manual extract requested', { operation: 'ocr.controller.extractManual', @@ -584,7 +584,7 @@ export class OcrController { request: FastifyRequest<{ Body: JobSubmitBody }>, reply: FastifyReply ) { - const userId = (request as any).user?.sub as string; + const userId = request.userContext?.userId as string; logger.info('OCR job submit requested', { operation: 'ocr.controller.submitJob', @@ -691,7 +691,7 @@ export class OcrController { request: FastifyRequest<{ Params: JobIdParams }>, reply: FastifyReply ) { - const userId = (request as any).user?.sub as string; + const userId = request.userContext?.userId as string; const { jobId } = request.params; logger.debug('OCR job status requested', { diff --git a/backend/src/features/user-profile/api/user-profile.controller.ts b/backend/src/features/user-profile/api/user-profile.controller.ts index 68c8732..e157a6f 100644 --- a/backend/src/features/user-profile/api/user-profile.controller.ts +++ b/backend/src/features/user-profile/api/user-profile.controller.ts @@ -18,11 +18,12 @@ import { export class UserProfileController { private userProfileService: UserProfileService; + private userProfileRepository: UserProfileRepository; constructor() { - const repository = new UserProfileRepository(pool); + this.userProfileRepository = new UserProfileRepository(pool); const adminRepository = new AdminRepository(pool); - this.userProfileService = new UserProfileService(repository); + this.userProfileService = new UserProfileService(this.userProfileRepository); this.userProfileService.setAdminRepository(adminRepository); } @@ -31,27 +32,24 @@ export class UserProfileController { */ async getProfile(request: FastifyRequest, reply: FastifyReply) { try { - const auth0Sub = request.userContext?.userId; + const userId = request.userContext?.userId; - if (!auth0Sub) { + if (!userId) { return reply.code(401).send({ error: 'Unauthorized', message: 'User context missing', }); } - // Get user data from Auth0 token - const auth0User = { - sub: auth0Sub, - email: (request as any).user?.email || request.userContext?.email || '', - name: (request as any).user?.name, - }; + // Get profile by UUID (auth plugin ensures profile exists during authentication) + const profile = await this.userProfileRepository.getById(userId); - // Get or create profile - const profile = await this.userProfileService.getOrCreateProfile( - auth0Sub, - auth0User - ); + if (!profile) { + return reply.code(404).send({ + error: 'Not Found', + message: 'User profile not found', + }); + } return reply.code(200).send(profile); } catch (error: any) { @@ -75,9 +73,9 @@ export class UserProfileController { reply: FastifyReply ) { try { - const auth0Sub = request.userContext?.userId; + const userId = request.userContext?.userId; - if (!auth0Sub) { + if (!userId) { return reply.code(401).send({ error: 'Unauthorized', message: 'User context missing', @@ -96,9 +94,9 @@ export class UserProfileController { const updates = validation.data; - // Update profile + // Update profile by UUID const profile = await this.userProfileService.updateProfile( - auth0Sub, + userId, updates ); @@ -138,9 +136,9 @@ export class UserProfileController { reply: FastifyReply ) { try { - const auth0Sub = request.userContext?.userId; + const userId = request.userContext?.userId; - if (!auth0Sub) { + if (!userId) { return reply.code(401).send({ error: 'Unauthorized', message: 'User context missing', @@ -159,9 +157,9 @@ export class UserProfileController { const { confirmationText } = validation.data; - // Request deletion (user is already authenticated via JWT) + // Request deletion by UUID const profile = await this.userProfileService.requestDeletion( - auth0Sub, + userId, confirmationText ); @@ -210,17 +208,17 @@ export class UserProfileController { */ async cancelDeletion(request: FastifyRequest, reply: FastifyReply) { try { - const auth0Sub = request.userContext?.userId; + const userId = request.userContext?.userId; - if (!auth0Sub) { + if (!userId) { return reply.code(401).send({ error: 'Unauthorized', message: 'User context missing', }); } - // Cancel deletion - const profile = await this.userProfileService.cancelDeletion(auth0Sub); + // Cancel deletion by UUID + const profile = await this.userProfileService.cancelDeletion(userId); return reply.code(200).send({ message: 'Account deletion canceled successfully', @@ -258,27 +256,24 @@ export class UserProfileController { */ async getDeletionStatus(request: FastifyRequest, reply: FastifyReply) { try { - const auth0Sub = request.userContext?.userId; + const userId = request.userContext?.userId; - if (!auth0Sub) { + if (!userId) { return reply.code(401).send({ error: 'Unauthorized', message: 'User context missing', }); } - // Get user data from Auth0 token - const auth0User = { - sub: auth0Sub, - email: (request as any).user?.email || request.userContext?.email || '', - name: (request as any).user?.name, - }; + // Get profile by UUID (auth plugin ensures profile exists) + const profile = await this.userProfileRepository.getById(userId); - // Get or create profile - const profile = await this.userProfileService.getOrCreateProfile( - auth0Sub, - auth0User - ); + if (!profile) { + return reply.code(404).send({ + error: 'Not Found', + message: 'User profile not found', + }); + } const deletionStatus = this.userProfileService.getDeletionStatus(profile); diff --git a/backend/src/features/user-profile/data/user-profile.repository.ts b/backend/src/features/user-profile/data/user-profile.repository.ts index fcc78dd..f782395 100644 --- a/backend/src/features/user-profile/data/user-profile.repository.ts +++ b/backend/src/features/user-profile/data/user-profile.repository.ts @@ -194,7 +194,7 @@ export class UserProfileRepository { private mapRowToUserWithAdminStatus(row: any): UserWithAdminStatus { return { ...this.mapRowToUserProfile(row), - isAdmin: !!row.admin_auth0_sub, + isAdmin: !!row.admin_id, adminRole: row.admin_role || null, vehicleCount: parseInt(row.vehicle_count, 10) || 0, }; @@ -262,7 +262,7 @@ export class UserProfileRepository { up.id, up.auth0_sub, up.email, up.display_name, up.notification_email, up.subscription_tier, up.email_verified, up.onboarding_completed_at, up.deactivated_at, up.deactivated_by, up.created_at, up.updated_at, - au.auth0_sub as admin_auth0_sub, + au.id as admin_id, au.role as admin_role, (SELECT COUNT(*) FROM vehicles v WHERE v.user_id = up.id @@ -300,7 +300,7 @@ export class UserProfileRepository { up.id, up.auth0_sub, up.email, up.display_name, up.notification_email, up.subscription_tier, up.email_verified, up.onboarding_completed_at, up.deactivated_at, up.deactivated_by, up.created_at, up.updated_at, - au.auth0_sub as admin_auth0_sub, + au.id as admin_id, au.role as admin_role, (SELECT COUNT(*) FROM vehicles v WHERE v.user_id = up.id diff --git a/backend/src/features/user-profile/domain/user-profile.service.ts b/backend/src/features/user-profile/domain/user-profile.service.ts index 285a264..ac13942 100644 --- a/backend/src/features/user-profile/domain/user-profile.service.ts +++ b/backend/src/features/user-profile/domain/user-profile.service.ts @@ -60,7 +60,7 @@ export class UserProfileService { } /** - * Get user profile by Auth0 sub + * Get user profile by Auth0 sub (used during auth flow) */ async getProfile(auth0Sub: string): Promise { try { @@ -72,10 +72,10 @@ export class UserProfileService { } /** - * Update user profile + * Update user profile by UUID */ async updateProfile( - auth0Sub: string, + userId: string, updates: UpdateProfileRequest ): Promise { try { @@ -85,17 +85,17 @@ export class UserProfileService { } // Perform the update - const profile = await this.repository.update(auth0Sub, updates); + const profile = await this.repository.update(userId, updates); logger.info('User profile updated', { - auth0Sub, + userId, profileId: profile.id, updatedFields: Object.keys(updates), }); return profile; } catch (error) { - logger.error('Error updating user profile', { error, auth0Sub, updates }); + logger.error('Error updating user profile', { error, userId, updates }); throw error; } } @@ -117,29 +117,29 @@ export class UserProfileService { } /** - * Get user details with admin status (admin-only) + * Get user details with admin status by UUID (admin-only) */ - async getUserDetails(auth0Sub: string): Promise { + async getUserDetails(userId: string): Promise { try { - return await this.repository.getUserWithAdminStatus(auth0Sub); + return await this.repository.getUserWithAdminStatus(userId); } catch (error) { - logger.error('Error getting user details', { error, auth0Sub }); + logger.error('Error getting user details', { error, userId }); throw error; } } /** - * Update user subscription tier (admin-only) + * Update user subscription tier by UUID (admin-only) * Logs the change to admin audit logs */ async updateSubscriptionTier( - auth0Sub: string, + userId: string, tier: SubscriptionTier, - actorAuth0Sub: string + actorUserId: string ): Promise { try { // Get current user to log the change - const currentUser = await this.repository.getByAuth0Sub(auth0Sub); + const currentUser = await this.repository.getById(userId); if (!currentUser) { throw new Error('User not found'); } @@ -147,14 +147,14 @@ export class UserProfileService { const previousTier = currentUser.subscriptionTier; // Perform the update - const updatedProfile = await this.repository.updateSubscriptionTier(auth0Sub, tier); + const updatedProfile = await this.repository.updateSubscriptionTier(userId, tier); // Log to audit trail if (this.adminRepository) { await this.adminRepository.logAuditAction( - actorAuth0Sub, + actorUserId, 'UPDATE_TIER', - auth0Sub, + userId, 'user_profile', updatedProfile.id, { previousTier, newTier: tier } @@ -162,36 +162,36 @@ export class UserProfileService { } logger.info('User subscription tier updated', { - auth0Sub, + userId, previousTier, newTier: tier, - actorAuth0Sub, + actorUserId, }); return updatedProfile; } catch (error) { - logger.error('Error updating subscription tier', { error, auth0Sub, tier, actorAuth0Sub }); + logger.error('Error updating subscription tier', { error, userId, tier, actorUserId }); throw error; } } /** - * Deactivate user account (admin-only soft delete) + * Deactivate user account by UUID (admin-only soft delete) * Prevents self-deactivation */ async deactivateUser( - auth0Sub: string, - actorAuth0Sub: string, + userId: string, + actorUserId: string, reason?: string ): Promise { try { // Prevent self-deactivation - if (auth0Sub === actorAuth0Sub) { + if (userId === actorUserId) { throw new Error('Cannot deactivate your own account'); } // Verify user exists and is not already deactivated - const currentUser = await this.repository.getByAuth0Sub(auth0Sub); + const currentUser = await this.repository.getById(userId); if (!currentUser) { throw new Error('User not found'); } @@ -200,14 +200,14 @@ export class UserProfileService { } // Perform the deactivation - const deactivatedProfile = await this.repository.deactivateUser(auth0Sub, actorAuth0Sub); + const deactivatedProfile = await this.repository.deactivateUser(userId, actorUserId); // Log to audit trail if (this.adminRepository) { await this.adminRepository.logAuditAction( - actorAuth0Sub, + actorUserId, 'DEACTIVATE_USER', - auth0Sub, + userId, 'user_profile', deactivatedProfile.id, { reason: reason || 'No reason provided' } @@ -215,28 +215,28 @@ export class UserProfileService { } logger.info('User deactivated', { - auth0Sub, - actorAuth0Sub, + userId, + actorUserId, reason, }); return deactivatedProfile; } catch (error) { - logger.error('Error deactivating user', { error, auth0Sub, actorAuth0Sub }); + logger.error('Error deactivating user', { error, userId, actorUserId }); throw error; } } /** - * Reactivate a deactivated user account (admin-only) + * Reactivate a deactivated user account by UUID (admin-only) */ async reactivateUser( - auth0Sub: string, - actorAuth0Sub: string + userId: string, + actorUserId: string ): Promise { try { // Verify user exists and is deactivated - const currentUser = await this.repository.getByAuth0Sub(auth0Sub); + const currentUser = await this.repository.getById(userId); if (!currentUser) { throw new Error('User not found'); } @@ -245,14 +245,14 @@ export class UserProfileService { } // Perform the reactivation - const reactivatedProfile = await this.repository.reactivateUser(auth0Sub); + const reactivatedProfile = await this.repository.reactivateUser(userId); // Log to audit trail if (this.adminRepository) { await this.adminRepository.logAuditAction( - actorAuth0Sub, + actorUserId, 'REACTIVATE_USER', - auth0Sub, + userId, 'user_profile', reactivatedProfile.id, { previouslyDeactivatedBy: currentUser.deactivatedBy } @@ -260,29 +260,29 @@ export class UserProfileService { } logger.info('User reactivated', { - auth0Sub, - actorAuth0Sub, + userId, + actorUserId, }); return reactivatedProfile; } catch (error) { - logger.error('Error reactivating user', { error, auth0Sub, actorAuth0Sub }); + logger.error('Error reactivating user', { error, userId, actorUserId }); throw error; } } /** - * Admin update of user profile (email, displayName) + * Admin update of user profile by UUID (email, displayName) * Logs the change to admin audit logs */ async adminUpdateProfile( - auth0Sub: string, + userId: string, updates: { email?: string; displayName?: string }, - actorAuth0Sub: string + actorUserId: string ): Promise { try { // Get current user to log the change - const currentUser = await this.repository.getByAuth0Sub(auth0Sub); + const currentUser = await this.repository.getById(userId); if (!currentUser) { throw new Error('User not found'); } @@ -293,14 +293,14 @@ export class UserProfileService { }; // Perform the update - const updatedProfile = await this.repository.adminUpdateProfile(auth0Sub, updates); + const updatedProfile = await this.repository.adminUpdateProfile(userId, updates); // Log to audit trail if (this.adminRepository) { await this.adminRepository.logAuditAction( - actorAuth0Sub, + actorUserId, 'UPDATE_PROFILE', - auth0Sub, + userId, 'user_profile', updatedProfile.id, { @@ -311,14 +311,14 @@ export class UserProfileService { } logger.info('User profile updated by admin', { - auth0Sub, + userId, updatedFields: Object.keys(updates), - actorAuth0Sub, + actorUserId, }); return updatedProfile; } catch (error) { - logger.error('Error admin updating user profile', { error, auth0Sub, updates, actorAuth0Sub }); + logger.error('Error admin updating user profile', { error, userId, updates, actorUserId }); throw error; } } @@ -328,12 +328,12 @@ export class UserProfileService { // ============================================ /** - * Request account deletion + * Request account deletion by UUID * Sets 30-day grace period before permanent deletion * Note: User is already authenticated via JWT, confirmation text is sufficient */ async requestDeletion( - auth0Sub: string, + userId: string, confirmationText: string ): Promise { try { @@ -343,7 +343,7 @@ export class UserProfileService { } // Get user profile - const profile = await this.repository.getByAuth0Sub(auth0Sub); + const profile = await this.repository.getById(userId); if (!profile) { throw new Error('User not found'); } @@ -354,14 +354,14 @@ export class UserProfileService { } // Request deletion - const updatedProfile = await this.repository.requestDeletion(auth0Sub); + const updatedProfile = await this.repository.requestDeletion(userId); // Log to audit trail if (this.adminRepository) { await this.adminRepository.logAuditAction( - auth0Sub, + userId, 'REQUEST_DELETION', - auth0Sub, + userId, 'user_profile', updatedProfile.id, { @@ -371,42 +371,42 @@ export class UserProfileService { } logger.info('Account deletion requested', { - auth0Sub, + userId, deletionScheduledFor: updatedProfile.deletionScheduledFor, }); return updatedProfile; } catch (error) { - logger.error('Error requesting account deletion', { error, auth0Sub }); + logger.error('Error requesting account deletion', { error, userId }); throw error; } } /** - * Cancel pending deletion request + * Cancel pending deletion request by UUID */ - async cancelDeletion(auth0Sub: string): Promise { + async cancelDeletion(userId: string): Promise { try { // Cancel deletion - const updatedProfile = await this.repository.cancelDeletion(auth0Sub); + const updatedProfile = await this.repository.cancelDeletion(userId); // Log to audit trail if (this.adminRepository) { await this.adminRepository.logAuditAction( - auth0Sub, + userId, 'CANCEL_DELETION', - auth0Sub, + userId, 'user_profile', updatedProfile.id, {} ); } - logger.info('Account deletion canceled', { auth0Sub }); + logger.info('Account deletion canceled', { userId }); return updatedProfile; } catch (error) { - logger.error('Error canceling account deletion', { error, auth0Sub }); + logger.error('Error canceling account deletion', { error, userId }); throw error; } } @@ -438,22 +438,22 @@ export class UserProfileService { } /** - * Admin hard delete user (permanent deletion) + * Admin hard delete user by UUID (permanent deletion) * Prevents self-delete */ async adminHardDeleteUser( - auth0Sub: string, - actorAuth0Sub: string, + userId: string, + actorUserId: string, reason?: string ): Promise { try { // Prevent self-delete - if (auth0Sub === actorAuth0Sub) { + if (userId === actorUserId) { throw new Error('Cannot delete your own account'); } // Get user profile before deletion for audit log - const profile = await this.repository.getByAuth0Sub(auth0Sub); + const profile = await this.repository.getById(userId); if (!profile) { throw new Error('User not found'); } @@ -461,9 +461,9 @@ export class UserProfileService { // Log to audit trail before deletion if (this.adminRepository) { await this.adminRepository.logAuditAction( - actorAuth0Sub, + actorUserId, 'HARD_DELETE_USER', - auth0Sub, + userId, 'user_profile', profile.id, { @@ -475,18 +475,20 @@ export class UserProfileService { } // Hard delete from database - await this.repository.hardDeleteUser(auth0Sub); + await this.repository.hardDeleteUser(userId); - // Delete from Auth0 - await auth0ManagementClient.deleteUser(auth0Sub); + // Delete from Auth0 (using auth0Sub for Auth0 API) + if (profile.auth0Sub) { + await auth0ManagementClient.deleteUser(profile.auth0Sub); + } logger.info('User hard deleted by admin', { - auth0Sub, - actorAuth0Sub, + userId, + actorUserId, reason, }); } catch (error) { - logger.error('Error hard deleting user', { error, auth0Sub, actorAuth0Sub }); + logger.error('Error hard deleting user', { error, userId, actorUserId }); throw error; } }