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:
@@ -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}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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', {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user