Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 20 additions & 17 deletions srcs/auth/src/controllers/admin.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ export async function updateUserHandler(
});
}

if (role !== UserRole.USER && role !== UserRole.ADMIN) {
const validRoles = Object.values(UserRole);
if (!validRoles.includes(role)) {
return reply.code(HTTP_STATUS.BAD_REQUEST).send({
error: {
message: ERROR_MESSAGES.INVALID_ROLE,
Expand Down Expand Up @@ -170,15 +171,15 @@ export async function deleteUserHandler(
req: FastifyRequest,
reply: FastifyReply,
) {
// Récupérer les informations admin déjà validées par verifyAdminRole
const adminUserId = (req as any).adminUserId;
const adminUsername = (req as any).adminUsername;
// Récupérer les informations verifyAdminRole
const authUser = (req as any).authUser;
const targetUserId = Number((req.params as any).id);

logger.info({
event: 'admin_delete_user_attempt',
admin: adminUsername,
adminUserId,
actor: authUser.username,
actorId: authUser.id,
actorRole: authUser.role,
targetUserId,
});

Expand All @@ -192,7 +193,7 @@ export async function deleteUserHandler(
}

// Empêcher l'auto-suppression
if (targetUserId === adminUserId) {
if (targetUserId === authUser.id) {
return reply.code(HTTP_STATUS.BAD_REQUEST).send({
error: {
message: ERROR_MESSAGES.SELF_DELETION_FORBIDDEN,
Expand All @@ -218,7 +219,8 @@ export async function deleteUserHandler(

logger.info({
event: 'admin_delete_user_success',
admin: adminUsername,
actor: authUser.username,
actorRole: authUser.role,
targetUserId,
targetUsername,
});
Expand All @@ -229,7 +231,7 @@ export async function deleteUserHandler(
} catch (err: any) {
logger.error({
event: 'admin_delete_user_error',
admin: adminUsername,
actor: authUser.username,
targetUserId,
err: err?.message || err,
});
Expand All @@ -244,22 +246,22 @@ export async function deleteUserHandler(
}

/**
* ADMIN ONLY - Désactiver la 2FA d'un utilisateur
* MODERATOR or ADMIN - Désactiver la 2FA d'un utilisateur
*/
export async function adminDisable2FAHandler(
this: FastifyInstance,
req: FastifyRequest,
reply: FastifyReply,
) {
// Récupérer les informations admin déjà validées par verifyAdminRole
const adminUserId = (req as any).adminUserId;
const adminUsername = (req as any).adminUsername;
// Récupérer les informations verifyModeratorRole
const authUser = (req as any).authUser;
const targetUserId = Number((req.params as any).id);

logger.info({
event: 'admin_disable_2fa_attempt',
admin: adminUsername,
adminUserId,
actor: authUser.username,
actorId: authUser.id,
actorRole: authUser.role,
targetUserId,
});

Expand Down Expand Up @@ -299,7 +301,8 @@ export async function adminDisable2FAHandler(

logger.info({
event: 'admin_disable_2fa_success',
admin: adminUsername,
actor: authUser.username,
actorRole: authUser.role,
targetUserId,
targetUsername: targetUser.username,
});
Expand All @@ -310,7 +313,7 @@ export async function adminDisable2FAHandler(
} catch (err: any) {
logger.error({
event: 'admin_disable_2fa_error',
admin: adminUsername,
actor: authUser.username,
targetUserId,
err: err?.message || err,
});
Expand Down
3 changes: 2 additions & 1 deletion srcs/auth/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import fastifyCookie from '@fastify/cookie';
import fastifyJwt from '@fastify/jwt';
import fastifyRateLimit from '@fastify/rate-limit';
import { authRoutes } from './routes/auth.routes.js';
import { adminRoutes } from './routes/admin.routes.js';
import { adminRoutes, moderatorRoutes } from './routes/admin.routes.js';
import { initAdminUser, initInviteUser } from './utils/init-users.js';
import * as totpService from './services/totp.service.js';
import * as onlineService from './services/online.service.js';
Expand Down Expand Up @@ -141,6 +141,7 @@ app.register(fastifyRateLimit, {

app.register(authRoutes, { prefix: '/' });
app.register(adminRoutes, { prefix: '/admin' });
app.register(moderatorRoutes, { prefix: '/admin' });

(async () => {
try {
Expand Down
77 changes: 60 additions & 17 deletions srcs/auth/src/routes/admin.routes.ts
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pour éviter les as any, peut-on définir une interface

AuthenticatedRequest extends FastifyRequest {
   authUser: {
      id: number;
      username: string;
      role: string;
   };
}

que l'on utiliserait à la place de FastifyRequest pour les requêtes de authService nécessitant une authentification (hors login, register)

Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { UserRole, HTTP_STATUS, ERROR_MESSAGES, ERROR_RESPONSE_CODES } from '../

/**
* Pré-handler pour vérifier si l'utilisateur est administrateur
* Ajoute adminUserId et adminUsername a la requête
* Ajoute authUser (id, username, role) à la requête
*/
export async function verifyAdminRole(req: FastifyRequest, reply: FastifyReply) {
const idHeader = (req.headers as any)['x-user-id'];
Expand All @@ -31,8 +31,42 @@ export async function verifyAdminRole(req: FastifyRequest, reply: FastifyReply)
});
}

(req as any).adminUserId = userId;
(req as any).adminUsername = username;
(req as any).authUser = {
id: userId,
username,
role: authService.getUserRole(userId),
};
}

/**
* Pré-handler pour vérifier si l'utilisateur est modérateur ou administrateur
* Permet aux modérateurs de désactiver la 2FA et lister les utilisateurs
* Ajoute authUser (id, username, role) à la requête
*/
export async function verifyModeratorRole(req: FastifyRequest, reply: FastifyReply) {
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 || !authService.hasRole(userId, UserRole.MODERATOR)) {
req.log.warn({
event: 'moderator_access_forbidden',
user: username,
userId,
});
return reply.code(HTTP_STATUS.FORBIDDEN).send({
error: {
message: ERROR_MESSAGES.FORBIDDEN,
code: ERROR_RESPONSE_CODES.FORBIDDEN,
},
});
}

(req as any).authUser = {
id: userId,
username,
role: authService.getUserRole(userId),
};
}

/**
Expand All @@ -43,20 +77,6 @@ export async function adminRoutes(app: FastifyInstance) {
// Ajout du pré-handler pour toutes les routes admin
app.addHook('onRequest', verifyAdminRole);

// Liste tous les utilisateurs
app.get(
'/users',
{
config: {
rateLimit: {
max: 50,
timeWindow: '1 minute',
},
},
},
listAllUsers,
);

// Mettre à jour un utilisateur
app.put(
'/users/:id',
Expand Down Expand Up @@ -84,6 +104,29 @@ export async function adminRoutes(app: FastifyInstance) {
},
deleteUserHandler,
);
}

/**
* Routes de modération
* Ces routes nécessitent un rôle modérateur ou supérieur
*/
export async function moderatorRoutes(app: FastifyInstance) {
// Ajout du pré-handler pour toutes les routes modérateur
app.addHook('onRequest', verifyModeratorRole);

// Liste tous les utilisateurs
app.get(
'/users',
{
config: {
rateLimit: {
max: 50,
timeWindow: '1 minute',
},
},
},
listAllUsers,
);

// Désactiver la 2FA d'un utilisateur
app.post(
Expand Down
5 changes: 3 additions & 2 deletions srcs/auth/src/services/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,10 +212,11 @@ export function hasRole(userId: number, requiredRole: UserRole): boolean {
try {
const userRole = getUserRole(userId);

// Hiérarchie des rôles : user < admin
// Hiérarchie des rôles : user < moderator < admin
const roleHierarchy = {
[UserRole.USER]: 0,
[UserRole.ADMIN]: 1,
[UserRole.MODERATOR]: 1,
[UserRole.ADMIN]: 2,
};

const userRoleLevel = roleHierarchy[userRole as UserRole] ?? 0;
Expand Down
6 changes: 5 additions & 1 deletion srcs/auth/src/utils/constants.ts
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

auth service a acces au package core. Si il y a besoin de traduire les messages d'erreur, on pourrait reutiliser les errorCodes de core, pour que le frontend dispose des mémes clés

Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,13 @@ export const AUTH_CONFIG = {

/**
* Rôles utilisateur pour RBAC (Role-Based Access Control)
* USER: utilisateur standard
* MODERATOR: peut consulter la liste des utilisateurs et désactiver la 2FA
* ADMIN: contrôle total (view, update, delete users, disable 2FA)
*/
export enum UserRole {
USER = 'user',
MODERATOR = 'moderator',
ADMIN = 'admin',
}

Expand Down Expand Up @@ -135,7 +139,7 @@ export const ERROR_MESSAGES = {
INVALID_USER_ID: 'Invalid user ID',

// Role errors
INVALID_ROLE: 'Role must be either "user" or "admin"',
INVALID_ROLE: 'Role must be either "user" or "admin" or "moderator"',

// Field errors
MISSING_FIELDS: 'Username, email, and role are required',
Expand Down
Loading
Loading