WakaStart
Sécurité

Contrôle d'accès — Guards & Hooks

Implémentation complète : hook useAppRights, composant Protected, guard NestJS AppRightGuard.

Version v1.03 min de lecture

Contrôle d'accès — Guards & Hooks

Ce chapitre couvre le code d'implémentation côté frontend (React hooks, composant <Protected>) et backend (guards NestJS). Lisez Modèle de droits d'abord.


Implémentation côté frontend

Hook useAppRights

typescript
// hooks/use-app-rights.ts import { useAuth } from "@/lib/auth-context"; export function useAppRights() { const { user } = useAuth(); const appRights = user?.appRights ?? []; const features = user?.features ?? []; return { // Vérification d'un droit unique hasRight: (code: string) => appRights.includes(code), // Au moins un des droits présent hasAnyRight: (...codes: string[]) => codes.some(c => appRights.includes(c)), // Tous les droits présents hasAllRights: (...codes: string[]) => codes.every(c => appRights.includes(c)), // Vérification du niveau admin isAdminAtLeast: (level: string) => { const levels = ["None", "User", "CustomerAdmin", "NetworkAdmin", "AppsAdmin", "OwnerAdmin", "WakaAdmin"]; const current = levels.indexOf(user?.adminLevel ?? "None"); const required = levels.indexOf(level); return current >= required; }, // Feature activée en cascade hasFeature: (feature: string) => features.includes(feature), // Rôle métier hasRole: (role: string) => (user?.wakaRoles ?? []).includes(role), }; }

Composant <Protected>

typescript
// components/protected.tsx import { useAppRights } from "@/hooks/use-app-rights"; interface ProtectedProps { right?: string; anyRight?: string[]; allRights?: string[]; adminLevel?: string; feature?: string; role?: string; fallback?: React.ReactNode; children: React.ReactNode; } export function Protected({ right, anyRight, allRights, adminLevel, feature, role, fallback = null, children, }: ProtectedProps) { const { hasRight, hasAnyRight, hasAllRights, isAdminAtLeast, hasFeature, hasRole } = useAppRights(); const allowed = (!right || hasRight(right)) && (!anyRight || hasAnyRight(...anyRight)) && (!allRights || hasAllRights(...allRights)) && (!adminLevel || isAdminAtLeast(adminLevel)) && (!feature || hasFeature(feature)) && (!role || hasRole(role)); return allowed ? <>{children}</> : <>{fallback}</>; }

Usage dans un composant :

tsx
function UserManagementPage() { return ( <div> {/* Visible uniquement si l'utilisateur peut créer des users */} <Protected right="users.create"> <CreateUserButton /> </Protected> {/* Visible si deploy OU write sur infra */} <Protected anyRight={["infra.deploy", "infra.write"]}> <DeployButton /> </Protected> {/* Visible uniquement si la feature invitations est activée */} <Protected feature="invitations" right="invitations.create"> <InviteUserButton /> </Protected> {/* Visible si niveau CustomerAdmin minimum */} <Protected adminLevel="CustomerAdmin"> <CustomerSettingsPanel /> </Protected> </div> ); }

Implémentation côté backend NestJS (dans votre Wakapp)

Si votre Wakapp a son propre backend, vous devez vérifier les droits en décodant le wakaToken enrichi reçu dans x-enriched-token.

Décorateur @RequireAppRight

typescript
// decorators/require-app-right.decorator.ts import { SetMetadata } from "@nestjs/common"; export const REQUIRED_RIGHT_KEY = "requiredRight"; export const RequireAppRight = (right: string) => SetMetadata(REQUIRED_RIGHT_KEY, right);

Guard AppRightGuard

typescript
// guards/app-right.guard.ts import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common"; import { Reflector } from "@nestjs/core"; import * as jwt from "jsonwebtoken"; import { REQUIRED_RIGHT_KEY } from "../decorators/require-app-right.decorator"; @Injectable() export class AppRightGuard implements CanActivate { constructor(private reflector: Reflector) {} canActivate(context: ExecutionContext): boolean { const requiredRight = this.reflector.getAllAndOverride<string>( REQUIRED_RIGHT_KEY, [context.getHandler(), context.getClass()] ); if (!requiredRight) return true; // Pas de restriction const request = context.switchToHttp().getRequest(); const enrichedToken = request.headers["x-enriched-token"]; if (!enrichedToken) { throw new ForbiddenException("Token enrichi manquant"); } try { const payload = jwt.verify(enrichedToken, process.env.WAKASTART_JWT_SECRET!) as any; const appRights: string[] = payload.appRights ?? []; if (!appRights.includes(requiredRight)) { throw new ForbiddenException(`Droit requis : ${requiredRight}`); } return true; } catch (err) { throw new ForbiddenException("Token invalide"); } } }

Alternative recommandée : déléguer au service token

typescript
// Votre backend délègue la vérification à la Public API async function verifyRight(token: string, right: string): Promise<boolean> { const res = await fetch(`${WAKASTART_API_URL}/api/token/authorize`, { method: "POST", headers: { "x-api-key": process.env.WAKASTART_API_KEY!, "Content-Type": "application/json", }, body: JSON.stringify({ token, checks: { appRights: { rights: [right], mode: "all" }, }, }), }); const { authorized } = await res.json(); return authorized; }

Usage dans un contrôleur NestJS

typescript
@Controller("users") @UseGuards(AppRightGuard) export class UsersController { @Post() @RequireAppRight("users.create") async createUser(@Body() dto: CreateUserDto) { // ... } @Delete(":id") @RequireAppRight("users.delete") async deleteUser(@Param("id") id: string) { // ... } }

Bonnes pratiques

  • Logging des refus : loggez les 403 avec le droit manquant (mais jamais le token complet).
  • Invalidation du cache : après une modification de profil par un admin, l'utilisateur doit se re-loguer ou appeler /me à nouveau.

Aller plus loin