From a0fff1a1942ddae61b6d10d4e9eb6a0106a679d3 Mon Sep 17 00:00:00 2001 From: rcaillie Date: Wed, 11 Feb 2026 13:51:35 +0100 Subject: [PATCH 1/2] =?UTF-8?q?feat(AUTH,=20USER):=20ajouter=20la=20foncti?= =?UTF-8?q?onnalit=C3=A9=20de=20suppression=20de=20compte=20utilisateur?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- srcs/auth/src/controllers/admin.controller.ts | 2 +- srcs/auth/src/controllers/auth.controller.ts | 68 +++++++++++++++++++ srcs/auth/src/routes/auth.routes.ts | 15 ++++ srcs/auth/src/services/auth.service.ts | 50 ++++++++++---- srcs/auth/src/services/external/um.service.ts | 49 +++++++++++++ srcs/auth/src/services/online.service.ts | 29 ++++++++ srcs/auth/src/utils/constants.ts | 4 ++ .../src/controllers/profiles.controller.ts | 9 +++ srcs/users/src/routes/profiles.routes.ts | 20 ++++++ srcs/users/src/services/profiles.service.ts | 14 ++++ 10 files changed, 247 insertions(+), 13 deletions(-) diff --git a/srcs/auth/src/controllers/admin.controller.ts b/srcs/auth/src/controllers/admin.controller.ts index 8570be56..c52d9b5c 100644 --- a/srcs/auth/src/controllers/admin.controller.ts +++ b/srcs/auth/src/controllers/admin.controller.ts @@ -214,7 +214,7 @@ export async function deleteUserHandler( } const targetUsername = targetUser.username; - authService.deleteUserAsAdmin(targetUserId); + await authService.deleteUser(targetUserId); logger.info({ event: 'admin_delete_user_success', diff --git a/srcs/auth/src/controllers/auth.controller.ts b/srcs/auth/src/controllers/auth.controller.ts index 743eaeb1..38f45446 100644 --- a/srcs/auth/src/controllers/auth.controller.ts +++ b/srcs/auth/src/controllers/auth.controller.ts @@ -938,3 +938,71 @@ export async function isUserOnlineHandler( }); } } + +/** + * Suppression du compte utilisateur + * Permet à un utilisateur de supprimer définitivement son compte + */ +export async function deleteUserHandler( + this: FastifyInstance, + req: FastifyRequest, + reply: FastifyReply, +) { + try { + // ID utilisateur headers + const idHeader = (req.headers as any)['x-user-id']; + const userId = idHeader ? Number(idHeader) : null; + const username = (req.headers as any)['x-user-name'] || null; + + if (!userId) { + this.log.warn({ + event: 'delete_user_unauthorized', + username, + }); + return reply.code(HTTP_STATUS.UNAUTHORIZED).send({ + error: { + message: ERROR_MESSAGES.UNAUTHORIZED, + code: ERROR_CODES.UNAUTHORIZED, + }, + }); + } + + this.log.info({ + event: 'delete_user_request', + userId, + username, + }); + + await authService.deleteUser(userId); + + this.log.info({ + event: 'delete_user_success', + userId, + username, + }); + + return reply.code(HTTP_STATUS.NO_CONTENT).send(); + } catch (error: unknown) { + this.log.error({ + event: 'delete_user_error', + error: (error as Error)?.message || error, + }); + + if (error instanceof AppError) { + const frontendError = mapToFrontendError(error); + return reply.code(frontendError.statusCode).send({ + error: { + message: frontendError.message, + code: frontendError.code, + }, + }); + } + + return reply.code(HTTP_STATUS.INTERNAL_SERVER_ERROR).send({ + error: { + message: 'Internal server error', + code: ERROR_CODES.INTERNAL_ERROR, + }, + }); + } +} diff --git a/srcs/auth/src/routes/auth.routes.ts b/srcs/auth/src/routes/auth.routes.ts index 8931413e..918f84d3 100644 --- a/srcs/auth/src/routes/auth.routes.ts +++ b/srcs/auth/src/routes/auth.routes.ts @@ -13,6 +13,7 @@ import { disable2FAHandler, heartbeatHandler, isUserOnlineHandler, + deleteUserHandler, } from '../controllers/auth.controller.js'; import { AUTH_CONFIG } from '../utils/constants.js'; @@ -122,6 +123,20 @@ export async function authRoutes(app: FastifyInstance) { app.post('/2fa/disable', disable2FAHandler); + // Suppression du compte utilisateur + app.delete( + '/user/delete', + { + config: { + rateLimit: { + max: AUTH_CONFIG.RATE_LIMIT.DELETE_USER.max, + timeWindow: AUTH_CONFIG.RATE_LIMIT.DELETE_USER.timeWindow, + }, + }, + }, + deleteUserHandler, + ); + app.get( '/is-online/:name', { diff --git a/srcs/auth/src/services/auth.service.ts b/srcs/auth/src/services/auth.service.ts index 7e1162bb..3bb11ae3 100644 --- a/srcs/auth/src/services/auth.service.ts +++ b/srcs/auth/src/services/auth.service.ts @@ -1,12 +1,13 @@ import bcrypt from 'bcrypt'; import * as db from './database.js'; -import { createUserProfile } from './external/um.service.js'; +import { createUserProfile, deleteUserProfile } from './external/um.service.js'; import { DataError, ServiceError } from '../types/errors.js'; import { APP_ERRORS } from '../utils/error-catalog.js'; import { EVENTS, REASONS, UserRole } from '../utils/constants.js'; import { authenv } from '../config/env.js'; import { logger } from '../index.js'; import { AppError, ERR_DEFS } from '@transcendence/core'; +import * as onlineService from './online.service.js'; const SALT_ROUNDS = 10; @@ -105,17 +106,6 @@ export function updateUserAsAdmin( } } -export function deleteUserAsAdmin(userId: number) { - try { - db.deleteUser(userId); - } catch (err: unknown) { - if (err instanceof DataError) { - throw new ServiceError(APP_ERRORS.LOGIN_USER_NOT_FOUND); - } - throw err; - } -} - export function adminDisable2FA(userId: number) { try { db.disable2FA(userId); @@ -228,3 +218,39 @@ export function hasRole(userId: number, requiredRole: UserRole): boolean { return false; } } + +/** + * Supprime un utilisateur et toutes ses données + * Suppression entre les services auth et users + */ +export async function deleteUser(userId: number): Promise { + logger.info({ event: 'delete_user_start', userId }); + + try { + // Supp user profile UM service + await deleteUserProfile(userId); + + // Supp user Redis online + await onlineService.removeUserFromRedis(userId); + + // Supp user from auth DB + db.deleteUser(userId); + + logger.info({ event: 'delete_user_completed', userId }); + } catch (error: unknown) { + logger.error({ event: 'delete_user_failed', userId, error: (error as Error)?.message }); + + if (error instanceof AppError) { + throw error; + } + + if (error instanceof DataError) { + throw new AppError(ERR_DEFS.LOGIN_USER_NOT_FOUND, { userId }); + } + + throw new AppError(ERR_DEFS.SERVICE_GENERIC, { + userId, + originalError: (error as Error)?.message, + }); + } +} diff --git a/srcs/auth/src/services/external/um.service.ts b/srcs/auth/src/services/external/um.service.ts index 6ff7db1f..76cd7634 100644 --- a/srcs/auth/src/services/external/um.service.ts +++ b/srcs/auth/src/services/external/um.service.ts @@ -55,3 +55,52 @@ export async function createUserProfile(payload: CreateProfileDTO): Promise { + try { + logger.info({ msg: `calling DELETE ${UM_SERVICE_URL}/users/${userId}` }); + + // Configuration de la requête avec l'agent mTLS + const init: MTLSRequestInit = { + method: 'DELETE', + headers: { + 'x-user-id': String(userId), + }, + dispatcher: mtlsAgent, + }; + + const response = await fetch(`${UM_SERVICE_URL}/users/${userId}`, init); + + if (!response.ok) { + const errorText = await response.text(); + + let parsedMessage = 'Failed to delete user profile'; + try { + const parsed = JSON.parse(errorText); + parsedMessage = parsed?.message || parsedMessage; + } catch { + // keep fallback + } + + throw new ServiceError( + { + code: ERROR_CODES.INTERNAL_ERROR, + event: EVENTS.DEPENDENCY.FAIL, + statusCode: response.status, + reason: REASONS.NETWORK.UPSTREAM_ERROR, + message: parsedMessage, + }, + { originalError: { status: response.status, body: errorText } }, + ); + } + + logger.info({ msg: `user profile deleted successfully`, userId }); + } catch (error) { + logger.error({ msg: `error DELETE ${UM_SERVICE_URL}/users/${userId}`, error: error }); + if (error instanceof ServiceError) throw error; + throw new ServiceError(APP_ERRORS.SERVICE_UNAVAILABLE, { originalError: error }); + } +} diff --git a/srcs/auth/src/services/online.service.ts b/srcs/auth/src/services/online.service.ts index 36de7550..d9fa8dab 100644 --- a/srcs/auth/src/services/online.service.ts +++ b/srcs/auth/src/services/online.service.ts @@ -244,3 +244,32 @@ export async function closeRedis(): Promise { logger.info({ event: 'redis_connection_closed' }); } } + +/** + * Supprime toutes les données Redis d'un utilisateur + */ +export async function removeUserFromRedis(userId: number): Promise { + try { + const client = getRedisClient(); + const userKey = `${ONLINE_KEY_PREFIX}${userId}`; + + // Supp user online key + await client.del(userKey); + + // Supp user du set des utilisateurs en ligne + await client.srem(ONLINE_USERS_SET, userId.toString()); + + logger.info({ + event: 'user_redis_cleanup', + userId, + message: 'User data removed from Redis', + }); + } catch (error) { + logger.error({ + event: 'user_redis_cleanup_error', + userId, + error: (error as Error)?.message, + }); + throw error; + } +} diff --git a/srcs/auth/src/utils/constants.ts b/srcs/auth/src/utils/constants.ts index 48aeeec5..af13359a 100644 --- a/srcs/auth/src/utils/constants.ts +++ b/srcs/auth/src/utils/constants.ts @@ -93,6 +93,10 @@ export const AUTH_CONFIG = { max: isTestOrDev ? 1000 : 2000, timeWindow: '1 minute', }, + DELETE_USER: { + max: isTestOrDev ? 100 : 5, + timeWindow: '1 minute', + }, }, } as const; diff --git a/srcs/users/src/controllers/profiles.controller.ts b/srcs/users/src/controllers/profiles.controller.ts index 712e5cfd..06054a21 100644 --- a/srcs/users/src/controllers/profiles.controller.ts +++ b/srcs/users/src/controllers/profiles.controller.ts @@ -62,6 +62,15 @@ export class ProfileController { const profileSimpleDTO = await profileService.deleteByUsername(username); return reply.status(200).send(profileSimpleDTO); } + + async deleteProfileById(req: FastifyRequest, reply: FastifyReply) { + const { userId } = req.params as { + userId: number; + }; + req.log.trace({ event: `${LOG_ACTIONS.DELETE}_${LOG_RESOURCES.PROFILE}`, param: userId }); + await profileService.deleteById(userId); + return reply.status(204).send(); + } } export const profileController = new ProfileController(); diff --git a/srcs/users/src/routes/profiles.routes.ts b/srcs/users/src/routes/profiles.routes.ts index b0d924c2..e306f956 100644 --- a/srcs/users/src/routes/profiles.routes.ts +++ b/srcs/users/src/routes/profiles.routes.ts @@ -73,6 +73,20 @@ const deleteProfileSchema = { }, } as const; +const deleteProfileByIdSchema = { + tags: ['users'], + summary: 'Delete a profile by user ID', + description: 'Delete a profile by user ID (internal service use)', + params: z.object({ + userId: z.coerce.number(), + }), + response: { + 204: z.void(), + 400: ValidationErrorSchema, + 404: DetailedErrorSchema, + }, +} as const; + export const umRoutes: FastifyPluginAsyncZod = async (app) => { app.get( '/health', @@ -104,4 +118,10 @@ export const umRoutes: FastifyPluginAsyncZod = async (app) => { { schema: deleteProfileSchema }, profileController.deleteProfile, ); + + app.delete( + '/users/:userId', + { schema: deleteProfileByIdSchema }, + profileController.deleteProfileById, + ); }; diff --git a/srcs/users/src/services/profiles.service.ts b/srcs/users/src/services/profiles.service.ts index 95182a55..c6d1656d 100644 --- a/srcs/users/src/services/profiles.service.ts +++ b/srcs/users/src/services/profiles.service.ts @@ -82,6 +82,20 @@ export class ProfileService { const deletedProfile = await profileRepository.deleteProfile(profile.authId); return deletedProfile; } + + @Trace + async deleteById(authId: number): Promise { + const profile = await profileRepository.findProfileById(authId); + if (!profile) { + throw new AppError(ERR_DEFS.RESOURCE_NOT_FOUND, { + details: { + resource: LOG_RESOURCES.PROFILE, + authId: authId, + }, + }); + } + await profileRepository.deleteProfile(authId); + } } export const profileService = new ProfileService(); From f13d16a1e09bb0b46cddcb71c2ada3d2f4fe8415 Mon Sep 17 00:00:00 2001 From: rom98759 Date: Thu, 12 Feb 2026 16:37:24 +0100 Subject: [PATCH 2/2] fix(): copilot review --- srcs/auth/src/services/auth.service.ts | 2 +- srcs/users/src/services/profiles.service.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/srcs/auth/src/services/auth.service.ts b/srcs/auth/src/services/auth.service.ts index 3bb11ae3..37078a89 100644 --- a/srcs/auth/src/services/auth.service.ts +++ b/srcs/auth/src/services/auth.service.ts @@ -245,7 +245,7 @@ export async function deleteUser(userId: number): Promise { } if (error instanceof DataError) { - throw new AppError(ERR_DEFS.LOGIN_USER_NOT_FOUND, { userId }); + throw new AppError(ERR_DEFS.RESOURCE_NOT_FOUND, { userId }); } throw new AppError(ERR_DEFS.SERVICE_GENERIC, { diff --git a/srcs/users/src/services/profiles.service.ts b/srcs/users/src/services/profiles.service.ts index c6d1656d..19738c14 100644 --- a/srcs/users/src/services/profiles.service.ts +++ b/srcs/users/src/services/profiles.service.ts @@ -90,7 +90,7 @@ export class ProfileService { throw new AppError(ERR_DEFS.RESOURCE_NOT_FOUND, { details: { resource: LOG_RESOURCES.PROFILE, - authId: authId, + id: authId, }, }); }