chore: update supporting code for UUID identity (refs #216)

- audit-log: JOIN on user_profiles.id instead of auth0_sub
- backup: use userContext.userId instead of auth0Sub
- ocr: use request.userContext.userId instead of request.user.sub
- user-profile controller: use getById() with UUID instead of getOrCreateProfile()
- user-profile service: accept UUID userId for all admin-focused methods
- user-profile repository: fix admin JOIN aliases from auth0_sub to id

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Eric Gullickson
2026-02-16 09:59:05 -06:00
parent fd9d1add24
commit 3b1112a9fe
6 changed files with 137 additions and 140 deletions

View File

@@ -126,7 +126,7 @@ export class AuditLogRepository {
al.resource_type, al.resource_id, al.details, al.created_at, al.resource_type, al.resource_id, al.details, al.created_at,
up.email as user_email up.email as user_email
FROM audit_logs al 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} ${whereClause}
ORDER BY al.created_at DESC ORDER BY al.created_at DESC
LIMIT $${nextParamIndex} OFFSET $${nextParamIndex + 1} LIMIT $${nextParamIndex} OFFSET $${nextParamIndex + 1}
@@ -170,7 +170,7 @@ export class AuditLogRepository {
al.resource_type, al.resource_id, al.details, al.created_at, al.resource_type, al.resource_id, al.details, al.created_at,
up.email as user_email up.email as user_email
FROM audit_logs al 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} ${whereClause}
ORDER BY al.created_at DESC ORDER BY al.created_at DESC
LIMIT ${MAX_EXPORT_RECORDS} LIMIT ${MAX_EXPORT_RECORDS}

View File

@@ -45,12 +45,12 @@ export class BackupController {
request: FastifyRequest<{ Body: CreateBackupBody }>, request: FastifyRequest<{ Body: CreateBackupBody }>,
reply: FastifyReply reply: FastifyReply
): Promise<void> { ): Promise<void> {
const adminSub = (request as any).userContext?.auth0Sub; const adminUserId = request.userContext?.userId;
const result = await this.backupService.createBackup({ const result = await this.backupService.createBackup({
name: request.body.name, name: request.body.name,
backupType: 'manual', backupType: 'manual',
createdBy: adminSub, createdBy: adminUserId,
includeDocuments: request.body.includeDocuments, includeDocuments: request.body.includeDocuments,
}); });
@@ -58,7 +58,7 @@ export class BackupController {
// Log backup creation to unified audit log // Log backup creation to unified audit log
await auditLogService.info( await auditLogService.info(
'system', 'system',
adminSub || null, adminUserId || null,
`Backup created: ${request.body.name || 'Manual backup'}`, `Backup created: ${request.body.name || 'Manual backup'}`,
'backup', 'backup',
result.backupId, result.backupId,
@@ -74,7 +74,7 @@ export class BackupController {
// Log backup failure // Log backup failure
await auditLogService.error( await auditLogService.error(
'system', 'system',
adminSub || null, adminUserId || null,
`Backup failed: ${request.body.name || 'Manual backup'}`, `Backup failed: ${request.body.name || 'Manual backup'}`,
'backup', 'backup',
result.backupId, result.backupId,
@@ -139,7 +139,7 @@ export class BackupController {
request: FastifyRequest, request: FastifyRequest,
reply: FastifyReply reply: FastifyReply
): Promise<void> { ): Promise<void> {
const adminSub = (request as any).userContext?.auth0Sub; const adminUserId = request.userContext?.userId;
// Handle multipart file upload // Handle multipart file upload
const data = await request.file(); const data = await request.file();
@@ -173,7 +173,7 @@ export class BackupController {
const backup = await this.backupService.importUploadedBackup( const backup = await this.backupService.importUploadedBackup(
tempPath, tempPath,
filename, filename,
adminSub adminUserId
); );
reply.status(201).send({ reply.status(201).send({
@@ -217,7 +217,7 @@ export class BackupController {
request: FastifyRequest<{ Params: BackupIdParam; Body: RestoreBody }>, request: FastifyRequest<{ Params: BackupIdParam; Body: RestoreBody }>,
reply: FastifyReply reply: FastifyReply
): Promise<void> { ): Promise<void> {
const adminSub = (request as any).userContext?.auth0Sub; const adminUserId = request.userContext?.userId;
try { try {
const result = await this.restoreService.executeRestore({ const result = await this.restoreService.executeRestore({
@@ -229,7 +229,7 @@ export class BackupController {
// Log successful restore to unified audit log // Log successful restore to unified audit log
await auditLogService.info( await auditLogService.info(
'system', 'system',
adminSub || null, adminUserId || null,
`Backup restored: ${request.params.id}`, `Backup restored: ${request.params.id}`,
'backup', 'backup',
request.params.id, request.params.id,
@@ -246,7 +246,7 @@ export class BackupController {
// Log restore failure // Log restore failure
await auditLogService.error( await auditLogService.error(
'system', 'system',
adminSub || null, adminUserId || null,
`Backup restore failed: ${request.params.id}`, `Backup restore failed: ${request.params.id}`,
'backup', 'backup',
request.params.id, request.params.id,

View File

@@ -33,7 +33,7 @@ export class OcrController {
request: FastifyRequest<{ Querystring: ExtractQuery }>, request: FastifyRequest<{ Querystring: ExtractQuery }>,
reply: FastifyReply reply: FastifyReply
) { ) {
const userId = (request as any).user?.sub as string; const userId = request.userContext?.userId as string;
const preprocess = request.query.preprocess !== false; const preprocess = request.query.preprocess !== false;
logger.info('OCR extract requested', { logger.info('OCR extract requested', {
@@ -140,7 +140,7 @@ export class OcrController {
request: FastifyRequest, request: FastifyRequest,
reply: FastifyReply reply: FastifyReply
) { ) {
const userId = (request as any).user?.sub as string; const userId = request.userContext?.userId as string;
logger.info('VIN extract requested', { logger.info('VIN extract requested', {
operation: 'ocr.controller.extractVin', operation: 'ocr.controller.extractVin',
@@ -240,7 +240,7 @@ export class OcrController {
request: FastifyRequest, request: FastifyRequest,
reply: FastifyReply reply: FastifyReply
) { ) {
const userId = (request as any).user?.sub as string; const userId = request.userContext?.userId as string;
logger.info('Receipt extract requested', { logger.info('Receipt extract requested', {
operation: 'ocr.controller.extractReceipt', operation: 'ocr.controller.extractReceipt',
@@ -352,7 +352,7 @@ export class OcrController {
request: FastifyRequest, request: FastifyRequest,
reply: FastifyReply reply: FastifyReply
) { ) {
const userId = (request as any).user?.sub as string; const userId = request.userContext?.userId as string;
logger.info('Maintenance receipt extract requested', { logger.info('Maintenance receipt extract requested', {
operation: 'ocr.controller.extractMaintenanceReceipt', operation: 'ocr.controller.extractMaintenanceReceipt',
@@ -460,7 +460,7 @@ export class OcrController {
request: FastifyRequest, request: FastifyRequest,
reply: FastifyReply reply: FastifyReply
) { ) {
const userId = (request as any).user?.sub as string; const userId = request.userContext?.userId as string;
logger.info('Manual extract requested', { logger.info('Manual extract requested', {
operation: 'ocr.controller.extractManual', operation: 'ocr.controller.extractManual',
@@ -584,7 +584,7 @@ export class OcrController {
request: FastifyRequest<{ Body: JobSubmitBody }>, request: FastifyRequest<{ Body: JobSubmitBody }>,
reply: FastifyReply reply: FastifyReply
) { ) {
const userId = (request as any).user?.sub as string; const userId = request.userContext?.userId as string;
logger.info('OCR job submit requested', { logger.info('OCR job submit requested', {
operation: 'ocr.controller.submitJob', operation: 'ocr.controller.submitJob',
@@ -691,7 +691,7 @@ export class OcrController {
request: FastifyRequest<{ Params: JobIdParams }>, request: FastifyRequest<{ Params: JobIdParams }>,
reply: FastifyReply reply: FastifyReply
) { ) {
const userId = (request as any).user?.sub as string; const userId = request.userContext?.userId as string;
const { jobId } = request.params; const { jobId } = request.params;
logger.debug('OCR job status requested', { logger.debug('OCR job status requested', {

View File

@@ -18,11 +18,12 @@ import {
export class UserProfileController { export class UserProfileController {
private userProfileService: UserProfileService; private userProfileService: UserProfileService;
private userProfileRepository: UserProfileRepository;
constructor() { constructor() {
const repository = new UserProfileRepository(pool); this.userProfileRepository = new UserProfileRepository(pool);
const adminRepository = new AdminRepository(pool); const adminRepository = new AdminRepository(pool);
this.userProfileService = new UserProfileService(repository); this.userProfileService = new UserProfileService(this.userProfileRepository);
this.userProfileService.setAdminRepository(adminRepository); this.userProfileService.setAdminRepository(adminRepository);
} }
@@ -31,27 +32,24 @@ export class UserProfileController {
*/ */
async getProfile(request: FastifyRequest, reply: FastifyReply) { async getProfile(request: FastifyRequest, reply: FastifyReply) {
try { try {
const auth0Sub = request.userContext?.userId; const userId = request.userContext?.userId;
if (!auth0Sub) { if (!userId) {
return reply.code(401).send({ return reply.code(401).send({
error: 'Unauthorized', error: 'Unauthorized',
message: 'User context missing', message: 'User context missing',
}); });
} }
// Get user data from Auth0 token // Get profile by UUID (auth plugin ensures profile exists during authentication)
const auth0User = { const profile = await this.userProfileRepository.getById(userId);
sub: auth0Sub,
email: (request as any).user?.email || request.userContext?.email || '',
name: (request as any).user?.name,
};
// Get or create profile if (!profile) {
const profile = await this.userProfileService.getOrCreateProfile( return reply.code(404).send({
auth0Sub, error: 'Not Found',
auth0User message: 'User profile not found',
); });
}
return reply.code(200).send(profile); return reply.code(200).send(profile);
} catch (error: any) { } catch (error: any) {
@@ -75,9 +73,9 @@ export class UserProfileController {
reply: FastifyReply reply: FastifyReply
) { ) {
try { try {
const auth0Sub = request.userContext?.userId; const userId = request.userContext?.userId;
if (!auth0Sub) { if (!userId) {
return reply.code(401).send({ return reply.code(401).send({
error: 'Unauthorized', error: 'Unauthorized',
message: 'User context missing', message: 'User context missing',
@@ -96,9 +94,9 @@ export class UserProfileController {
const updates = validation.data; const updates = validation.data;
// Update profile // Update profile by UUID
const profile = await this.userProfileService.updateProfile( const profile = await this.userProfileService.updateProfile(
auth0Sub, userId,
updates updates
); );
@@ -138,9 +136,9 @@ export class UserProfileController {
reply: FastifyReply reply: FastifyReply
) { ) {
try { try {
const auth0Sub = request.userContext?.userId; const userId = request.userContext?.userId;
if (!auth0Sub) { if (!userId) {
return reply.code(401).send({ return reply.code(401).send({
error: 'Unauthorized', error: 'Unauthorized',
message: 'User context missing', message: 'User context missing',
@@ -159,9 +157,9 @@ export class UserProfileController {
const { confirmationText } = validation.data; const { confirmationText } = validation.data;
// Request deletion (user is already authenticated via JWT) // Request deletion by UUID
const profile = await this.userProfileService.requestDeletion( const profile = await this.userProfileService.requestDeletion(
auth0Sub, userId,
confirmationText confirmationText
); );
@@ -210,17 +208,17 @@ export class UserProfileController {
*/ */
async cancelDeletion(request: FastifyRequest, reply: FastifyReply) { async cancelDeletion(request: FastifyRequest, reply: FastifyReply) {
try { try {
const auth0Sub = request.userContext?.userId; const userId = request.userContext?.userId;
if (!auth0Sub) { if (!userId) {
return reply.code(401).send({ return reply.code(401).send({
error: 'Unauthorized', error: 'Unauthorized',
message: 'User context missing', message: 'User context missing',
}); });
} }
// Cancel deletion // Cancel deletion by UUID
const profile = await this.userProfileService.cancelDeletion(auth0Sub); const profile = await this.userProfileService.cancelDeletion(userId);
return reply.code(200).send({ return reply.code(200).send({
message: 'Account deletion canceled successfully', message: 'Account deletion canceled successfully',
@@ -258,27 +256,24 @@ export class UserProfileController {
*/ */
async getDeletionStatus(request: FastifyRequest, reply: FastifyReply) { async getDeletionStatus(request: FastifyRequest, reply: FastifyReply) {
try { try {
const auth0Sub = request.userContext?.userId; const userId = request.userContext?.userId;
if (!auth0Sub) { if (!userId) {
return reply.code(401).send({ return reply.code(401).send({
error: 'Unauthorized', error: 'Unauthorized',
message: 'User context missing', message: 'User context missing',
}); });
} }
// Get user data from Auth0 token // Get profile by UUID (auth plugin ensures profile exists)
const auth0User = { const profile = await this.userProfileRepository.getById(userId);
sub: auth0Sub,
email: (request as any).user?.email || request.userContext?.email || '',
name: (request as any).user?.name,
};
// Get or create profile if (!profile) {
const profile = await this.userProfileService.getOrCreateProfile( return reply.code(404).send({
auth0Sub, error: 'Not Found',
auth0User message: 'User profile not found',
); });
}
const deletionStatus = this.userProfileService.getDeletionStatus(profile); const deletionStatus = this.userProfileService.getDeletionStatus(profile);

View File

@@ -194,7 +194,7 @@ export class UserProfileRepository {
private mapRowToUserWithAdminStatus(row: any): UserWithAdminStatus { private mapRowToUserWithAdminStatus(row: any): UserWithAdminStatus {
return { return {
...this.mapRowToUserProfile(row), ...this.mapRowToUserProfile(row),
isAdmin: !!row.admin_auth0_sub, isAdmin: !!row.admin_id,
adminRole: row.admin_role || null, adminRole: row.admin_role || null,
vehicleCount: parseInt(row.vehicle_count, 10) || 0, 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.id, up.auth0_sub, up.email, up.display_name, up.notification_email,
up.subscription_tier, up.email_verified, up.onboarding_completed_at, up.subscription_tier, up.email_verified, up.onboarding_completed_at,
up.deactivated_at, up.deactivated_by, up.created_at, up.updated_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, au.role as admin_role,
(SELECT COUNT(*) FROM vehicles v (SELECT COUNT(*) FROM vehicles v
WHERE v.user_id = up.id 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.id, up.auth0_sub, up.email, up.display_name, up.notification_email,
up.subscription_tier, up.email_verified, up.onboarding_completed_at, up.subscription_tier, up.email_verified, up.onboarding_completed_at,
up.deactivated_at, up.deactivated_by, up.created_at, up.updated_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, au.role as admin_role,
(SELECT COUNT(*) FROM vehicles v (SELECT COUNT(*) FROM vehicles v
WHERE v.user_id = up.id WHERE v.user_id = up.id

View File

@@ -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<UserProfile | null> { async getProfile(auth0Sub: string): Promise<UserProfile | null> {
try { try {
@@ -72,10 +72,10 @@ export class UserProfileService {
} }
/** /**
* Update user profile * Update user profile by UUID
*/ */
async updateProfile( async updateProfile(
auth0Sub: string, userId: string,
updates: UpdateProfileRequest updates: UpdateProfileRequest
): Promise<UserProfile> { ): Promise<UserProfile> {
try { try {
@@ -85,17 +85,17 @@ export class UserProfileService {
} }
// Perform the update // Perform the update
const profile = await this.repository.update(auth0Sub, updates); const profile = await this.repository.update(userId, updates);
logger.info('User profile updated', { logger.info('User profile updated', {
auth0Sub, userId,
profileId: profile.id, profileId: profile.id,
updatedFields: Object.keys(updates), updatedFields: Object.keys(updates),
}); });
return profile; return profile;
} catch (error) { } catch (error) {
logger.error('Error updating user profile', { error, auth0Sub, updates }); logger.error('Error updating user profile', { error, userId, updates });
throw error; 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<UserWithAdminStatus | null> { async getUserDetails(userId: string): Promise<UserWithAdminStatus | null> {
try { try {
return await this.repository.getUserWithAdminStatus(auth0Sub); return await this.repository.getUserWithAdminStatus(userId);
} catch (error) { } catch (error) {
logger.error('Error getting user details', { error, auth0Sub }); logger.error('Error getting user details', { error, userId });
throw error; throw error;
} }
} }
/** /**
* Update user subscription tier (admin-only) * Update user subscription tier by UUID (admin-only)
* Logs the change to admin audit logs * Logs the change to admin audit logs
*/ */
async updateSubscriptionTier( async updateSubscriptionTier(
auth0Sub: string, userId: string,
tier: SubscriptionTier, tier: SubscriptionTier,
actorAuth0Sub: string actorUserId: string
): Promise<UserProfile> { ): Promise<UserProfile> {
try { try {
// Get current user to log the change // Get current user to log the change
const currentUser = await this.repository.getByAuth0Sub(auth0Sub); const currentUser = await this.repository.getById(userId);
if (!currentUser) { if (!currentUser) {
throw new Error('User not found'); throw new Error('User not found');
} }
@@ -147,14 +147,14 @@ export class UserProfileService {
const previousTier = currentUser.subscriptionTier; const previousTier = currentUser.subscriptionTier;
// Perform the update // Perform the update
const updatedProfile = await this.repository.updateSubscriptionTier(auth0Sub, tier); const updatedProfile = await this.repository.updateSubscriptionTier(userId, tier);
// Log to audit trail // Log to audit trail
if (this.adminRepository) { if (this.adminRepository) {
await this.adminRepository.logAuditAction( await this.adminRepository.logAuditAction(
actorAuth0Sub, actorUserId,
'UPDATE_TIER', 'UPDATE_TIER',
auth0Sub, userId,
'user_profile', 'user_profile',
updatedProfile.id, updatedProfile.id,
{ previousTier, newTier: tier } { previousTier, newTier: tier }
@@ -162,36 +162,36 @@ export class UserProfileService {
} }
logger.info('User subscription tier updated', { logger.info('User subscription tier updated', {
auth0Sub, userId,
previousTier, previousTier,
newTier: tier, newTier: tier,
actorAuth0Sub, actorUserId,
}); });
return updatedProfile; return updatedProfile;
} catch (error) { } catch (error) {
logger.error('Error updating subscription tier', { error, auth0Sub, tier, actorAuth0Sub }); logger.error('Error updating subscription tier', { error, userId, tier, actorUserId });
throw error; throw error;
} }
} }
/** /**
* Deactivate user account (admin-only soft delete) * Deactivate user account by UUID (admin-only soft delete)
* Prevents self-deactivation * Prevents self-deactivation
*/ */
async deactivateUser( async deactivateUser(
auth0Sub: string, userId: string,
actorAuth0Sub: string, actorUserId: string,
reason?: string reason?: string
): Promise<UserProfile> { ): Promise<UserProfile> {
try { try {
// Prevent self-deactivation // Prevent self-deactivation
if (auth0Sub === actorAuth0Sub) { if (userId === actorUserId) {
throw new Error('Cannot deactivate your own account'); throw new Error('Cannot deactivate your own account');
} }
// Verify user exists and is not already deactivated // Verify user exists and is not already deactivated
const currentUser = await this.repository.getByAuth0Sub(auth0Sub); const currentUser = await this.repository.getById(userId);
if (!currentUser) { if (!currentUser) {
throw new Error('User not found'); throw new Error('User not found');
} }
@@ -200,14 +200,14 @@ export class UserProfileService {
} }
// Perform the deactivation // Perform the deactivation
const deactivatedProfile = await this.repository.deactivateUser(auth0Sub, actorAuth0Sub); const deactivatedProfile = await this.repository.deactivateUser(userId, actorUserId);
// Log to audit trail // Log to audit trail
if (this.adminRepository) { if (this.adminRepository) {
await this.adminRepository.logAuditAction( await this.adminRepository.logAuditAction(
actorAuth0Sub, actorUserId,
'DEACTIVATE_USER', 'DEACTIVATE_USER',
auth0Sub, userId,
'user_profile', 'user_profile',
deactivatedProfile.id, deactivatedProfile.id,
{ reason: reason || 'No reason provided' } { reason: reason || 'No reason provided' }
@@ -215,28 +215,28 @@ export class UserProfileService {
} }
logger.info('User deactivated', { logger.info('User deactivated', {
auth0Sub, userId,
actorAuth0Sub, actorUserId,
reason, reason,
}); });
return deactivatedProfile; return deactivatedProfile;
} catch (error) { } catch (error) {
logger.error('Error deactivating user', { error, auth0Sub, actorAuth0Sub }); logger.error('Error deactivating user', { error, userId, actorUserId });
throw error; throw error;
} }
} }
/** /**
* Reactivate a deactivated user account (admin-only) * Reactivate a deactivated user account by UUID (admin-only)
*/ */
async reactivateUser( async reactivateUser(
auth0Sub: string, userId: string,
actorAuth0Sub: string actorUserId: string
): Promise<UserProfile> { ): Promise<UserProfile> {
try { try {
// Verify user exists and is deactivated // Verify user exists and is deactivated
const currentUser = await this.repository.getByAuth0Sub(auth0Sub); const currentUser = await this.repository.getById(userId);
if (!currentUser) { if (!currentUser) {
throw new Error('User not found'); throw new Error('User not found');
} }
@@ -245,14 +245,14 @@ export class UserProfileService {
} }
// Perform the reactivation // Perform the reactivation
const reactivatedProfile = await this.repository.reactivateUser(auth0Sub); const reactivatedProfile = await this.repository.reactivateUser(userId);
// Log to audit trail // Log to audit trail
if (this.adminRepository) { if (this.adminRepository) {
await this.adminRepository.logAuditAction( await this.adminRepository.logAuditAction(
actorAuth0Sub, actorUserId,
'REACTIVATE_USER', 'REACTIVATE_USER',
auth0Sub, userId,
'user_profile', 'user_profile',
reactivatedProfile.id, reactivatedProfile.id,
{ previouslyDeactivatedBy: currentUser.deactivatedBy } { previouslyDeactivatedBy: currentUser.deactivatedBy }
@@ -260,29 +260,29 @@ export class UserProfileService {
} }
logger.info('User reactivated', { logger.info('User reactivated', {
auth0Sub, userId,
actorAuth0Sub, actorUserId,
}); });
return reactivatedProfile; return reactivatedProfile;
} catch (error) { } catch (error) {
logger.error('Error reactivating user', { error, auth0Sub, actorAuth0Sub }); logger.error('Error reactivating user', { error, userId, actorUserId });
throw error; 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 * Logs the change to admin audit logs
*/ */
async adminUpdateProfile( async adminUpdateProfile(
auth0Sub: string, userId: string,
updates: { email?: string; displayName?: string }, updates: { email?: string; displayName?: string },
actorAuth0Sub: string actorUserId: string
): Promise<UserProfile> { ): Promise<UserProfile> {
try { try {
// Get current user to log the change // Get current user to log the change
const currentUser = await this.repository.getByAuth0Sub(auth0Sub); const currentUser = await this.repository.getById(userId);
if (!currentUser) { if (!currentUser) {
throw new Error('User not found'); throw new Error('User not found');
} }
@@ -293,14 +293,14 @@ export class UserProfileService {
}; };
// Perform the update // Perform the update
const updatedProfile = await this.repository.adminUpdateProfile(auth0Sub, updates); const updatedProfile = await this.repository.adminUpdateProfile(userId, updates);
// Log to audit trail // Log to audit trail
if (this.adminRepository) { if (this.adminRepository) {
await this.adminRepository.logAuditAction( await this.adminRepository.logAuditAction(
actorAuth0Sub, actorUserId,
'UPDATE_PROFILE', 'UPDATE_PROFILE',
auth0Sub, userId,
'user_profile', 'user_profile',
updatedProfile.id, updatedProfile.id,
{ {
@@ -311,14 +311,14 @@ export class UserProfileService {
} }
logger.info('User profile updated by admin', { logger.info('User profile updated by admin', {
auth0Sub, userId,
updatedFields: Object.keys(updates), updatedFields: Object.keys(updates),
actorAuth0Sub, actorUserId,
}); });
return updatedProfile; return updatedProfile;
} catch (error) { } 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; 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 * Sets 30-day grace period before permanent deletion
* Note: User is already authenticated via JWT, confirmation text is sufficient * Note: User is already authenticated via JWT, confirmation text is sufficient
*/ */
async requestDeletion( async requestDeletion(
auth0Sub: string, userId: string,
confirmationText: string confirmationText: string
): Promise<UserProfile> { ): Promise<UserProfile> {
try { try {
@@ -343,7 +343,7 @@ export class UserProfileService {
} }
// Get user profile // Get user profile
const profile = await this.repository.getByAuth0Sub(auth0Sub); const profile = await this.repository.getById(userId);
if (!profile) { if (!profile) {
throw new Error('User not found'); throw new Error('User not found');
} }
@@ -354,14 +354,14 @@ export class UserProfileService {
} }
// Request deletion // Request deletion
const updatedProfile = await this.repository.requestDeletion(auth0Sub); const updatedProfile = await this.repository.requestDeletion(userId);
// Log to audit trail // Log to audit trail
if (this.adminRepository) { if (this.adminRepository) {
await this.adminRepository.logAuditAction( await this.adminRepository.logAuditAction(
auth0Sub, userId,
'REQUEST_DELETION', 'REQUEST_DELETION',
auth0Sub, userId,
'user_profile', 'user_profile',
updatedProfile.id, updatedProfile.id,
{ {
@@ -371,42 +371,42 @@ export class UserProfileService {
} }
logger.info('Account deletion requested', { logger.info('Account deletion requested', {
auth0Sub, userId,
deletionScheduledFor: updatedProfile.deletionScheduledFor, deletionScheduledFor: updatedProfile.deletionScheduledFor,
}); });
return updatedProfile; return updatedProfile;
} catch (error) { } catch (error) {
logger.error('Error requesting account deletion', { error, auth0Sub }); logger.error('Error requesting account deletion', { error, userId });
throw error; throw error;
} }
} }
/** /**
* Cancel pending deletion request * Cancel pending deletion request by UUID
*/ */
async cancelDeletion(auth0Sub: string): Promise<UserProfile> { async cancelDeletion(userId: string): Promise<UserProfile> {
try { try {
// Cancel deletion // Cancel deletion
const updatedProfile = await this.repository.cancelDeletion(auth0Sub); const updatedProfile = await this.repository.cancelDeletion(userId);
// Log to audit trail // Log to audit trail
if (this.adminRepository) { if (this.adminRepository) {
await this.adminRepository.logAuditAction( await this.adminRepository.logAuditAction(
auth0Sub, userId,
'CANCEL_DELETION', 'CANCEL_DELETION',
auth0Sub, userId,
'user_profile', 'user_profile',
updatedProfile.id, updatedProfile.id,
{} {}
); );
} }
logger.info('Account deletion canceled', { auth0Sub }); logger.info('Account deletion canceled', { userId });
return updatedProfile; return updatedProfile;
} catch (error) { } catch (error) {
logger.error('Error canceling account deletion', { error, auth0Sub }); logger.error('Error canceling account deletion', { error, userId });
throw error; 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 * Prevents self-delete
*/ */
async adminHardDeleteUser( async adminHardDeleteUser(
auth0Sub: string, userId: string,
actorAuth0Sub: string, actorUserId: string,
reason?: string reason?: string
): Promise<void> { ): Promise<void> {
try { try {
// Prevent self-delete // Prevent self-delete
if (auth0Sub === actorAuth0Sub) { if (userId === actorUserId) {
throw new Error('Cannot delete your own account'); throw new Error('Cannot delete your own account');
} }
// Get user profile before deletion for audit log // Get user profile before deletion for audit log
const profile = await this.repository.getByAuth0Sub(auth0Sub); const profile = await this.repository.getById(userId);
if (!profile) { if (!profile) {
throw new Error('User not found'); throw new Error('User not found');
} }
@@ -461,9 +461,9 @@ export class UserProfileService {
// Log to audit trail before deletion // Log to audit trail before deletion
if (this.adminRepository) { if (this.adminRepository) {
await this.adminRepository.logAuditAction( await this.adminRepository.logAuditAction(
actorAuth0Sub, actorUserId,
'HARD_DELETE_USER', 'HARD_DELETE_USER',
auth0Sub, userId,
'user_profile', 'user_profile',
profile.id, profile.id,
{ {
@@ -475,18 +475,20 @@ export class UserProfileService {
} }
// Hard delete from database // Hard delete from database
await this.repository.hardDeleteUser(auth0Sub); await this.repository.hardDeleteUser(userId);
// Delete from Auth0 // Delete from Auth0 (using auth0Sub for Auth0 API)
await auth0ManagementClient.deleteUser(auth0Sub); if (profile.auth0Sub) {
await auth0ManagementClient.deleteUser(profile.auth0Sub);
}
logger.info('User hard deleted by admin', { logger.info('User hard deleted by admin', {
auth0Sub, userId,
actorAuth0Sub, actorUserId,
reason, reason,
}); });
} catch (error) { } catch (error) {
logger.error('Error hard deleting user', { error, auth0Sub, actorAuth0Sub }); logger.error('Error hard deleting user', { error, userId, actorUserId });
throw error; throw error;
} }
} }