Access control — Guards & Hooks
Full implementation: useAppRights hook, Protected component, NestJS AppRightGuard.
Access control — Guards & Hooks
This chapter covers the implementation code on the frontend side (React hooks, the
<Protected>component) and the backend side (NestJS guards). Read Rights model first.
Frontend implementation
useAppRights hook
typescript// hooks/use-app-rights.ts import { useAuth } from "@/lib/auth-context"; export function useAppRights() { const { user } = useAuth(); const appRights = user?.appRights ?? []; return { // Check a single right hasRight: (code: string) => appRights.includes(code), // At least one of the rights present hasAnyRight: (...codes: string[]) => codes.some(c => appRights.includes(c)), // All rights present hasAllRights: (...codes: string[]) => codes.every(c => appRights.includes(c)), // Admin level check 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; }, }; }
No
hasFeature/hasRolein this hook: feature activation is not a synchronous field of/me, it is checked via a dedicated call (GET /api/config/app-features/check/{customerWid}/{featureCode}, see Feature flags and cascade); and thewakaRoles/cross-cutting business role concept has been decommissioned (see Rights model).
<Protected> component
typescript// components/protected.tsx import { useAppRights } from "@/hooks/use-app-rights"; interface ProtectedProps { right?: string; anyRight?: string[]; allRights?: string[]; adminLevel?: string; fallback?: React.ReactNode; children: React.ReactNode; } export function Protected({ right, anyRight, allRights, adminLevel, fallback = null, children, }: ProtectedProps) { const { hasRight, hasAnyRight, hasAllRights, isAdminAtLeast } = useAppRights(); const allowed = (!right || hasRight(right)) && (!anyRight || hasAnyRight(...anyRight)) && (!allRights || hasAllRights(...allRights)) && (!adminLevel || isAdminAtLeast(adminLevel)); return allowed ? <>{children}</> : <>{fallback}</>; }
The feature check (
app-features/check/...) is asynchronous — combine it with<Protected>by resolving it before render (e.g. in a route loader), not as a synchronous prop of this component.
Usage in a component:
tsxfunction UserManagementPage() { return ( <div> {/* Visible only if the user can create users */} <Protected right="users.create"> <CreateUserButton /> </Protected> {/* Visible if deploy OR write on infra */} <Protected anyRight={["infra.deploy", "infra.write"]}> <DeployButton /> </Protected> {/* The invitations.create right is enough here; if the invitations feature can be disabled for this customer, check it upstream via GET /api/config/app-features/check/{customerWid}/invitations and condition the render of this whole block on that result. */} <Protected right="invitations.create"> <InviteUserButton /> </Protected> {/* Visible if at least CustomerAdmin level */} <Protected adminLevel="CustomerAdmin"> <CustomerSettingsPanel /> </Protected> </div> ); }
Backend implementation with NestJS (in your Wakapp)
If your Wakapp has its own backend, check rights by delegating to WakaStart's authorization API: POST /api/token/authorize.
Do not do this — Never verify the
x-enriched-tokensignature locally. The wakaToken is a platform implementation detail; only the response from/api/token/authorizeis contractual.
@RequireAppRight decorator
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);
AppRightGuard — delegation to /api/token/authorize
typescript// guards/app-right.guard.ts import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common"; import { Reflector } from "@nestjs/core"; import { REQUIRED_RIGHT_KEY } from "../decorators/require-app-right.decorator"; @Injectable() export class AppRightGuard implements CanActivate { constructor(private reflector: Reflector) {} async canActivate(context: ExecutionContext): Promise<boolean> { const requiredRight = this.reflector.getAllAndOverride<string>( REQUIRED_RIGHT_KEY, [context.getHandler(), context.getClass()], ); if (!requiredRight) return true; const request = context.switchToHttp().getRequest(); const enrichedToken = request.headers["x-enriched-token"]; if (!enrichedToken) { throw new ForbiddenException("Missing enriched token"); } const res = await fetch(`${process.env.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: enrichedToken, checks: { appRights: { rights: [requiredRight], mode: "all" }, }, }), }); if (!res.ok) { throw new ForbiddenException("Invalid token"); } const { authorized } = (await res.json()) as { authorized: boolean }; if (!authorized) { throw new ForbiddenException(`Required right: ${requiredRight}`); } return true; } }
To limit latency under heavy traffic, cache the
authorizedresponse for 30 to 60 seconds on the Wakapp side, indexed by(token, requiredRight). Beyond that, the token may have been revoked.
Usage in a NestJS controller
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) { // ... } }
Best practices
- Log denials: log
403s with the missing right (but never the full token). - Cache invalidation: after a profile change by an admin, the user must re-login or call
/meagain.
Go further
- Feature flags and cascade: AppFeature, NetworkAppFeature, CustomerAppFeature
- /me user context: source of rights
- Endpoint reference: the
/token/authorizeendpoint