Add a backoffice screen
Example used to test this page: GET /admin/items, an admin-only list of
every item, and a /backoffice/items page that shows it in a table, with
a link in the backoffice sidebar.
The backend guard is the security boundary. The frontend gates only decide who sees the page.
Backend steps
Add an admin controller to the feature's module, named
admin-<feature>.controller.ts, the waybackend/src/email/admin-email.controller.tssits in the email module. Put the guards on the class, so a route added later inherits them:@ApiTags('admin-email') @ApiBearerAuth() @UseGuards(SessionGuard, AdminGuard) @Controller('admin/email') export class AdminEmailController {A list route uses
parseOffset/clampLimitfromsrc/common/pagination.tsand returns{ items, total }, asAdminController.listinbackend/src/admin/admin.controller.tsdoes.Register it. Add the controller to the module's
controllersandAdminModuleto itsimports, asbackend/src/email/email.module.tsdoes:imports: [AuthModule, AdminModule],State its policy in
backend/src/common/route-auth.spec.ts. Add aPOLICYentry keyed by the class name, for exampleAdminItemsController: { routes: { list: ADMIN } },. The spec finds every registered controller, so there is nothing to import.What fails if you skip it: "states a policy for every registered controller, and only those" fails and names the controller.
Add the path to
ADMIN_ROUTESinbackend/src/common/route-auth.http.spec.ts, for example['GET', '/admin/items']. Nothing fails if you leave it out, but that spec makes real requests and checks an anonymous caller gets 401 and a signed-in non-admin gets 403, which proves the guard works and is not only declared.Regenerate the contract and types (see step 8 of recipe 1).
Frontend steps
Create
frontend/app/backoffice/<feature>/page.tsx. Copyapp/backoffice/emails/page.tsx: a page component that renders a<Suspense>around an async component that fetches withserverApi(), styled with the shared../ui.module.scss(styles.table,styles.tableWrap,styles.emptyRow,styles.error). Mutations go in a colocatedactions.ts; seeapp/backoffice/users/actions.ts.The gates are already there.
frontend/proxy.tsreturns a 404 for every/backoffice/*path unless the session belongs to an admin, andapp/backoffice/layout.tsxchecks again before rendering. A page underapp/backoffice/needs nothing extra. A page anywhere else is not gated, so keep admin screens under that folder.Add the nav entry to the sidebar in
frontend/app/backoffice/layout.tsx:<nav className={styles.sidebarNav}> <Link href="/backoffice/users">Users</Link>No test checks the link; the build only checks the page compiles.
Run
./verify.sh.
What the test run found
- The admin route, its
POLICYentry and itsADMIN_ROUTESentry, the/backoffice/itemspage and the sidebar link: verify all green, and the build listedƒ /backoffice/items.