- 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>
294 lines
8.0 KiB
TypeScript
294 lines
8.0 KiB
TypeScript
/**
|
|
* @ai-summary Fastify route handlers for user profile API
|
|
* @ai-context HTTP request/response handling with user authentication
|
|
*/
|
|
|
|
import { FastifyRequest, FastifyReply } from 'fastify';
|
|
import { UserProfileService } from '../domain/user-profile.service';
|
|
import { UserProfileRepository } from '../data/user-profile.repository';
|
|
import { AdminRepository } from '../../admin/data/admin.repository';
|
|
import { pool } from '../../../core/config/database';
|
|
import { logger } from '../../../core/logging/logger';
|
|
import {
|
|
UpdateProfileInput,
|
|
updateProfileSchema,
|
|
RequestDeletionInput,
|
|
requestDeletionSchema,
|
|
} from './user-profile.validation';
|
|
|
|
export class UserProfileController {
|
|
private userProfileService: UserProfileService;
|
|
private userProfileRepository: UserProfileRepository;
|
|
|
|
constructor() {
|
|
this.userProfileRepository = new UserProfileRepository(pool);
|
|
const adminRepository = new AdminRepository(pool);
|
|
this.userProfileService = new UserProfileService(this.userProfileRepository);
|
|
this.userProfileService.setAdminRepository(adminRepository);
|
|
}
|
|
|
|
/**
|
|
* GET /api/user/profile - Get current user's profile
|
|
*/
|
|
async getProfile(request: FastifyRequest, reply: FastifyReply) {
|
|
try {
|
|
const userId = request.userContext?.userId;
|
|
|
|
if (!userId) {
|
|
return reply.code(401).send({
|
|
error: 'Unauthorized',
|
|
message: 'User context missing',
|
|
});
|
|
}
|
|
|
|
// Get profile by UUID (auth plugin ensures profile exists during authentication)
|
|
const profile = await this.userProfileRepository.getById(userId);
|
|
|
|
if (!profile) {
|
|
return reply.code(404).send({
|
|
error: 'Not Found',
|
|
message: 'User profile not found',
|
|
});
|
|
}
|
|
|
|
return reply.code(200).send(profile);
|
|
} catch (error: any) {
|
|
logger.error('Error getting user profile', {
|
|
error: error.message,
|
|
userId: request.userContext?.userId,
|
|
});
|
|
|
|
return reply.code(500).send({
|
|
error: 'Internal server error',
|
|
message: 'Failed to retrieve user profile',
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* PUT /api/user/profile - Update user profile
|
|
*/
|
|
async updateProfile(
|
|
request: FastifyRequest<{ Body: UpdateProfileInput }>,
|
|
reply: FastifyReply
|
|
) {
|
|
try {
|
|
const userId = request.userContext?.userId;
|
|
|
|
if (!userId) {
|
|
return reply.code(401).send({
|
|
error: 'Unauthorized',
|
|
message: 'User context missing',
|
|
});
|
|
}
|
|
|
|
// Validate request body
|
|
const validation = updateProfileSchema.safeParse(request.body);
|
|
if (!validation.success) {
|
|
return reply.code(400).send({
|
|
error: 'Bad Request',
|
|
message: 'Invalid request body',
|
|
details: validation.error.errors,
|
|
});
|
|
}
|
|
|
|
const updates = validation.data;
|
|
|
|
// Update profile by UUID
|
|
const profile = await this.userProfileService.updateProfile(
|
|
userId,
|
|
updates
|
|
);
|
|
|
|
return reply.code(200).send(profile);
|
|
} catch (error: any) {
|
|
logger.error('Error updating user profile', {
|
|
error: error.message,
|
|
userId: request.userContext?.userId,
|
|
});
|
|
|
|
if (error.message.includes('not found')) {
|
|
return reply.code(404).send({
|
|
error: 'Not Found',
|
|
message: 'User profile not found',
|
|
});
|
|
}
|
|
|
|
if (error.message.includes('At least one field')) {
|
|
return reply.code(400).send({
|
|
error: 'Bad Request',
|
|
message: error.message,
|
|
});
|
|
}
|
|
|
|
return reply.code(500).send({
|
|
error: 'Internal server error',
|
|
message: 'Failed to update user profile',
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* POST /api/user/delete - Request account deletion
|
|
*/
|
|
async requestDeletion(
|
|
request: FastifyRequest<{ Body: RequestDeletionInput }>,
|
|
reply: FastifyReply
|
|
) {
|
|
try {
|
|
const userId = request.userContext?.userId;
|
|
|
|
if (!userId) {
|
|
return reply.code(401).send({
|
|
error: 'Unauthorized',
|
|
message: 'User context missing',
|
|
});
|
|
}
|
|
|
|
// Validate request body
|
|
const validation = requestDeletionSchema.safeParse(request.body);
|
|
if (!validation.success) {
|
|
return reply.code(400).send({
|
|
error: 'Bad Request',
|
|
message: 'Invalid request body',
|
|
details: validation.error.errors,
|
|
});
|
|
}
|
|
|
|
const { confirmationText } = validation.data;
|
|
|
|
// Request deletion by UUID
|
|
const profile = await this.userProfileService.requestDeletion(
|
|
userId,
|
|
confirmationText
|
|
);
|
|
|
|
const deletionStatus = this.userProfileService.getDeletionStatus(profile);
|
|
|
|
return reply.code(200).send({
|
|
message: 'Account deletion requested successfully',
|
|
deletionStatus,
|
|
});
|
|
} catch (error: any) {
|
|
logger.error('Error requesting account deletion', {
|
|
error: error.message,
|
|
userId: request.userContext?.userId,
|
|
});
|
|
|
|
if (error.message.includes('Invalid confirmation')) {
|
|
return reply.code(400).send({
|
|
error: 'Bad Request',
|
|
message: 'Confirmation text must be exactly "DELETE"',
|
|
});
|
|
}
|
|
|
|
if (error.message.includes('already requested')) {
|
|
return reply.code(400).send({
|
|
error: 'Bad Request',
|
|
message: 'Account deletion already requested',
|
|
});
|
|
}
|
|
|
|
if (error.message.includes('not found')) {
|
|
return reply.code(404).send({
|
|
error: 'Not Found',
|
|
message: 'User profile not found',
|
|
});
|
|
}
|
|
|
|
return reply.code(500).send({
|
|
error: 'Internal server error',
|
|
message: 'Failed to request account deletion',
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* POST /api/user/cancel-deletion - Cancel account deletion
|
|
*/
|
|
async cancelDeletion(request: FastifyRequest, reply: FastifyReply) {
|
|
try {
|
|
const userId = request.userContext?.userId;
|
|
|
|
if (!userId) {
|
|
return reply.code(401).send({
|
|
error: 'Unauthorized',
|
|
message: 'User context missing',
|
|
});
|
|
}
|
|
|
|
// Cancel deletion by UUID
|
|
const profile = await this.userProfileService.cancelDeletion(userId);
|
|
|
|
return reply.code(200).send({
|
|
message: 'Account deletion canceled successfully',
|
|
profile,
|
|
});
|
|
} catch (error: any) {
|
|
logger.error('Error canceling account deletion', {
|
|
error: error.message,
|
|
userId: request.userContext?.userId,
|
|
});
|
|
|
|
if (error.message.includes('no deletion request')) {
|
|
return reply.code(400).send({
|
|
error: 'Bad Request',
|
|
message: 'No deletion request pending',
|
|
});
|
|
}
|
|
|
|
if (error.message.includes('not found')) {
|
|
return reply.code(404).send({
|
|
error: 'Not Found',
|
|
message: 'User profile not found',
|
|
});
|
|
}
|
|
|
|
return reply.code(500).send({
|
|
error: 'Internal server error',
|
|
message: 'Failed to cancel account deletion',
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /api/user/deletion-status - Get deletion status
|
|
*/
|
|
async getDeletionStatus(request: FastifyRequest, reply: FastifyReply) {
|
|
try {
|
|
const userId = request.userContext?.userId;
|
|
|
|
if (!userId) {
|
|
return reply.code(401).send({
|
|
error: 'Unauthorized',
|
|
message: 'User context missing',
|
|
});
|
|
}
|
|
|
|
// Get profile by UUID (auth plugin ensures profile exists)
|
|
const profile = await this.userProfileRepository.getById(userId);
|
|
|
|
if (!profile) {
|
|
return reply.code(404).send({
|
|
error: 'Not Found',
|
|
message: 'User profile not found',
|
|
});
|
|
}
|
|
|
|
const deletionStatus = this.userProfileService.getDeletionStatus(profile);
|
|
|
|
return reply.code(200).send(deletionStatus);
|
|
} catch (error: any) {
|
|
logger.error('Error getting deletion status', {
|
|
error: error.message,
|
|
userId: request.userContext?.userId,
|
|
});
|
|
|
|
return reply.code(500).send({
|
|
error: 'Internal server error',
|
|
message: 'Failed to get deletion status',
|
|
});
|
|
}
|
|
}
|
|
}
|