Files
motovaultpro/backend/src/features/user-profile/api/user-profile.controller.ts
Eric Gullickson 3b1112a9fe 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>
2026-02-16 09:59:05 -06:00

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',
});
}
}
}