From 881a15b2a48e2de831c5f722704da8b7f7633ced Mon Sep 17 00:00:00 2001 From: Sosthene Date: Sun, 3 Aug 2025 19:02:34 +0200 Subject: [PATCH] [fix] Use db permissions instead of jwt --- src/app/api/idnot/UserController.ts | 56 ++------------ src/app/middlewares/RulesHandler.ts | 74 +++++++++++++++++-- .../middlewares/SubscriptionRulesHandler.ts | 63 ++++++++++++++++ src/common/databases/seeders/seeder.ts | 8 +- .../common/AuthService/AuthService.ts | 20 +---- 5 files changed, 145 insertions(+), 76 deletions(-) create mode 100644 src/app/middlewares/SubscriptionRulesHandler.ts diff --git a/src/app/api/idnot/UserController.ts b/src/app/api/idnot/UserController.ts index 0eb744c5..82a6dbb2 100644 --- a/src/app/api/idnot/UserController.ts +++ b/src/app/api/idnot/UserController.ts @@ -5,10 +5,9 @@ import { Service } from "typedi"; import AuthService, { IUserJwtPayload, PROVIDER_OPENID } from "@Services/common/AuthService/AuthService"; import IdNotService from "@Services/common/IdNotService/IdNotService"; -import User, { RulesGroup } from "le-coffre-resources/dist/Admin"; +import User from "le-coffre-resources/dist/Admin"; import UsersService from "@Services/super-admin/UsersService/UsersService"; import SubscriptionsService from "@Services/admin/SubscriptionsService/SubscriptionsService.ts"; -import RulesGroupsService from "@Services/admin/RulesGroupsService/RulesGroupsService"; @Controller() @Service() @@ -18,7 +17,6 @@ export default class UserController extends ApiController { private idNotService: IdNotService, private userService: UsersService, private subscriptionsService: SubscriptionsService, - private rulesGroupsService: RulesGroupsService, ) { super(); } @@ -75,47 +73,18 @@ export default class UserController extends ApiController { this.httpUnauthorized(response, "Email not found"); return; } - let isSubscribed = await this.subscriptionsService.isUserSubscribed(user.uid, userHydrated.office_membership?.uid!); - - //Check if user is whitelisted - // const isWhitelisted = await this.whitelistService.getByEmail(userHydrated.contact!.email); - - //When we'll switch to idNotId whitelisting - // const isWhitelisted = await this.userWhitelistService.getByIdNotId(user.idNot); - - //If not whitelisted, return 409 Not whitelisted - // if (!isWhitelisted || isWhitelisted.length === 0) { - // this.httpNotWhitelisted(response); - // return; - // } - await this.idNotService.updateOffice(user.office_uid); const payload = await this.authService.getUserJwtPayload(user.idNot); console.log("payload", payload); - if (!payload) { console.error("No payload"); return; } - if (!isSubscribed && (userHydrated.role?.name === "admin" || userHydrated.role?.name === "super-admin")) { - - const manageSubscriptionRulesEntity = await this.rulesGroupsService.get({ - where: { uid: "94343601-04c8-44ef-afb9-3047597528a9" }, - include: { rules: true }, - }); - - const manageSubscriptionRules = RulesGroup.hydrateArray(manageSubscriptionRulesEntity, { - strategy: "excludeAll", - }); - if (!manageSubscriptionRules[0]) return; - - payload.rules = manageSubscriptionRules[0].rules!.map((rule) => rule.name) || []; - - isSubscribed = true; - } + // Check subscription status - but don't modify JWT rules since RulesHandler will fetch from DB + let isSubscribed = await this.subscriptionsService.isUserSubscribed(user.uid, userHydrated.office_membership?.uid!); if (!isSubscribed) { console.error("User not subscribed"); @@ -162,20 +131,11 @@ export default class UserController extends ApiController { )) as IUserJwtPayload; let isSubscribed = await this.subscriptionsService.isUserSubscribed(newUserPayload.userId, newUserPayload.office_Id); - if (!isSubscribed && (newUserPayload.role === "admin" || newUserPayload.role === "super-admin")) { - const manageSubscriptionRulesEntity = await this.rulesGroupsService.get({ - where: { uid: "94343601-04c8-44ef-afb9-3047597528a9" }, - include: { rules: true }, - }); - - const manageSubscriptionRules = RulesGroup.hydrateArray(manageSubscriptionRulesEntity, { - strategy: "excludeAll", - }); - if (!manageSubscriptionRules[0]) return; - - newUserPayload.rules = manageSubscriptionRules[0].rules!.map((rule) => rule.name) || []; - - isSubscribed = true; + // Check subscription status - but don't modify JWT rules since RulesHandler will fetch from DB + if (!isSubscribed) { + console.error("User not subscribed during token refresh"); + this.httpUnauthorized(response, "User not subscribed"); + return; } delete newUserPayload.iat; delete newUserPayload.exp; diff --git a/src/app/middlewares/RulesHandler.ts b/src/app/middlewares/RulesHandler.ts index d627100d..5061a6a4 100644 --- a/src/app/middlewares/RulesHandler.ts +++ b/src/app/middlewares/RulesHandler.ts @@ -1,34 +1,92 @@ import HttpCodes from "@Common/system/controller-pattern/HttpCodes"; import { NextFunction, Request, Response } from "express"; +import Container from "typedi"; +import UsersService from "@Services/super-admin/UsersService/UsersService"; +import subscriptionRulesHandler from "./SubscriptionRulesHandler"; export default async function ruleHandler(req: Request, response: Response, next: NextFunction) { try { - const rules = req.body.user.rules; + const userId = req.body.user.userId; const service = req.path && req.path.split("/")[4]; const requiredRule = req.method + " " + service; - if (!rules) { - response.status(HttpCodes.UNAUTHORIZED).send("Unauthorized without rules"); + if (!userId) { + response.status(HttpCodes.UNAUTHORIZED).send("Unauthorized - no user ID"); return; } + // Fetch user rules directly from database instead of trusting JWT + const usersService = Container.get(UsersService); + const user = await usersService.getByUid(userId, { + role: { include: { rules: true } }, + office_role: { include: { rules: true } } + }) as any; // Type assertion for database result with includes + + if (!user) { + console.error(`User not found in database: ${userId}`); + response.status(HttpCodes.UNAUTHORIZED).send("Unauthorized - user not found"); + return; + } + + // Build rules array from database + const rules: string[] = []; + + // Add role rules + if (user.role && user.role.rules) { + user.role.rules.forEach((rule: any) => { + rules.push(rule.name); + }); + } + + // Add office role rules (if any) + if (user.office_role && user.office_role.rules) { + user.office_role.rules.forEach((rule: any) => { + if (!rules.includes(rule.name)) { + rules.push(rule.name); + } + }); + } + + // Check if user has required rule if (!rules.includes(requiredRule)) { - console.error(`Rule check failed for user ${req.body.user.userId}:`); + console.error(`Rule check failed for user ${userId}:`); console.error(` Required rule: "${requiredRule}"`); - console.error(` User rules: [${rules.join(", ")}]`); + console.error(` User role: ${user.role?.name}`); + console.error(` User office role: ${user.office_role?.name}`); + console.error(` Database rules: [${rules.join(", ")}]`); console.error(` Path: ${req.path}`); console.error(` Method: ${req.method}`); console.error(` Service: ${service}`); response.status(HttpCodes.UNAUTHORIZED).send( - `Unauthorized with those rules. Required: "${requiredRule}", Provided: [${rules.join(", ")}]` + `Unauthorized. Required: "${requiredRule}", User has: [${rules.join(", ")}]` ); return; } - next(); + // Update the request with the fresh rules from database + req.body.user.rules = rules; + + console.log(`[RulesHandler] User ${userId} authorized for ${requiredRule}`); + + // Apply subscription rules if needed (for admin/super-admin users) + await subscriptionRulesHandler(req, response, () => { + // Check if user has required rule after subscription rules are applied + if (!req.body.user.rules.includes(requiredRule)) { + console.error(`Rule check failed for user ${userId} after subscription rules:`); + console.error(` Required rule: "${requiredRule}"`); + console.error(` Final rules: [${req.body.user.rules.join(", ")}]`); + + response.status(HttpCodes.UNAUTHORIZED).send( + `Unauthorized. Required: "${requiredRule}", User has: [${req.body.user.rules.join(", ")}]` + ); + return; + } + + next(); + }); } catch (error) { - console.error(error); + console.error(`[RulesHandler] Error checking rules for user:`, error); response.status(HttpCodes.INTERNAL_ERROR).send("Internal server error"); return; } diff --git a/src/app/middlewares/SubscriptionRulesHandler.ts b/src/app/middlewares/SubscriptionRulesHandler.ts new file mode 100644 index 00000000..da6ac1b3 --- /dev/null +++ b/src/app/middlewares/SubscriptionRulesHandler.ts @@ -0,0 +1,63 @@ +import { NextFunction, Request, Response } from "express"; +import Container from "typedi"; +import SubscriptionsService from "@Services/admin/SubscriptionsService/SubscriptionsService"; +import RulesGroupsService from "@Services/admin/RulesGroupsService/RulesGroupsService"; +import { RulesGroup } from "le-coffre-resources/dist/Admin"; + +/** + * Middleware to add subscription management rules for admin/super-admin users + * who aren't subscribed but need to manage subscriptions + */ +export default async function subscriptionRulesHandler(req: Request, response: Response, next: NextFunction) { + try { + const userId = req.body.user.userId; + const userRole = req.body.user.role; + const officeId = req.body.user.office_Id; + + // Only apply to admin/super-admin users + if (userRole !== "admin" && userRole !== "super-admin") { + return next(); + } + + // Check if user is subscribed + const subscriptionsService = Container.get(SubscriptionsService); + const isSubscribed = await subscriptionsService.isUserSubscribed(userId, officeId); + + // If subscribed, no need to add subscription management rules + if (isSubscribed) { + return next(); + } + + // User is admin/super-admin but not subscribed - add subscription management rules + console.log(`[SubscriptionRulesHandler] Admin user ${userId} not subscribed, adding subscription management rules`); + + const rulesGroupsService = Container.get(RulesGroupsService); + const manageSubscriptionRulesEntity = await rulesGroupsService.get({ + where: { uid: "94343601-04c8-44ef-afb9-3047597528a9" }, + include: { rules: true }, + }); + + const manageSubscriptionRules = RulesGroup.hydrateArray(manageSubscriptionRulesEntity, { + strategy: "excludeAll", + }); + + if (!manageSubscriptionRules[0]) { + console.error("[SubscriptionRulesHandler] Subscription rules group not found"); + return next(); + } + + // Add subscription rules to existing rules + const subscriptionRules = manageSubscriptionRules[0].rules!.map((rule) => rule.name) || []; + console.log(`[SubscriptionRulesHandler] Adding subscription rules:`, subscriptionRules); + console.log(`[SubscriptionRulesHandler] Original rules:`, req.body.user.rules); + + req.body.user.rules = [...new Set([...req.body.user.rules, ...subscriptionRules])]; + console.log(`[SubscriptionRulesHandler] Final rules after adding subscription rules:`, req.body.user.rules); + + next(); + } catch (error) { + console.error(`[SubscriptionRulesHandler] Error adding subscription rules:`, error); + // Don't fail the request, just continue without subscription rules + next(); + } +} \ No newline at end of file diff --git a/src/common/databases/seeders/seeder.ts b/src/common/databases/seeders/seeder.ts index df33cb5d..853101af 100644 --- a/src/common/databases/seeders/seeder.ts +++ b/src/common/databases/seeders/seeder.ts @@ -814,14 +814,14 @@ export default async function main() { label: "Notaire", created_at: new Date(), updated_at: new Date(), - rules: [], + rules: globalRules, // Fixed: was empty array }, { name: "default", label: "Utilisateur", created_at: new Date(), updated_at: new Date(), - rules: [], + rules: globalRules, // Fixed: was empty array }, ]; @@ -858,7 +858,7 @@ export default async function main() { contact: contacts[2], office_membership: offices[0], role: roles[2], - office_role: officeRoles[2], + office_role: officeRoles[0], // Fixed: was officeRoles[2] }, { created_at: new Date(), @@ -867,7 +867,7 @@ export default async function main() { contact: contacts[3], office_membership: offices[0], role: roles[1], - office_role: officeRoles[3], + office_role: officeRoles[1], // Fixed: was officeRoles[3] }, { created_at: new Date(), diff --git a/src/services/common/AuthService/AuthService.ts b/src/services/common/AuthService/AuthService.ts index b3fa7591..8623c221 100644 --- a/src/services/common/AuthService/AuthService.ts +++ b/src/services/common/AuthService/AuthService.ts @@ -33,7 +33,7 @@ export interface IUserJwtPayload { }; office_Id: string; role: string; - rules: string[]; + rules: string[]; // Kept for backward compatibility, but will be empty in JWT iat?: number; exp?: number; } @@ -62,26 +62,14 @@ export default class AuthService extends BaseService { if (!user) return null; - const rules: string[] = []; - - user.role.rules.forEach((rule) => { - rules.push(rule.name); - }); - - if (user.office_role) { - user.office_role.rules.forEach((rule) => { - if (!rules.includes(rule.name)) { - rules.push(rule.name); - } - }); - } - + // Since RulesHandler now fetches rules from database, we can simplify JWT payload + // Rules are no longer included in JWT for security reasons return { userId: user.uid, openId: { providerName: providerName, userId: user.idNot }, office_Id: user.office_membership.uid, role: user.role.name, - rules: rules, + rules: [], // Rules will be fetched fresh from database by RulesHandler }; } public generateAccessToken(user: any): string {