Skip to content

AI Prompt: Forma3D.Connect — Phase: RBAC + Tenant-Ready (Single Tenant)

Purpose: Instruct an AI (Claude Opus 4.5) to implement Role-Based Access Control (RBAC) and tenant-ready data isolation for Forma3D.Connect
Estimated Effort: 18–30 hours (implementation + tests)
Output: Multi-user authentication + RBAC enforced across API and dashboard, and tenant-aware persistence using one default tenant (no customer multi-tenant UX yet)
Status: 🚧 TODO


🎯 Mission

Implement RBAC and tenant-ready multi-tenancy in Forma3D.Connect.

Critical constraints:

  • Do NOT integrate Keycloak or OpenID Connect yet.
  • Implement in-app authentication (local users) and in-app RBAC (roles/permissions).
  • Implement tenant-aware schema and repository enforcement, but run with exactly one default tenant for now.
  • Preserve the existing operational model: webhooks and machine integrations should remain functional.

Deliverables:

  • Multiple user accounts can sign in to the dashboard.
  • Each user has one or more roles and effective permissions.
  • API endpoints enforce permissions server-side (no UI-only security).
  • Database writes/reads are tenant-scoped through repositories.
  • System runs single-tenant by default (one tenant seeded/ensured).

📌 Context (Current State)

  • The dashboard currently uses a single shared API key login concept.
  • The backend is NestJS + Prisma + PostgreSQL with repository pattern expectations.
  • Webhooks exist for Shopify and SimplyPrint (and other integrations).

Goal of this phase: introduce RBAC and tenant boundaries without prematurely adding external identity providers.


🛠️ Tech Stack Reference

  • Backend: NestJS (TypeScript), Prisma, PostgreSQL
  • Frontend: React 19 + React Router + TanStack Query
  • Monorepo: Nx + pnpm
  • Testing: Jest (API), Vitest (web), Playwright acceptance tests

🏗️ Architecture Requirements

1) Layering Rules (must respect)

  • Controllers are HTTP-only
  • Services hold business logic
  • Repositories hold Prisma access
  • No Prisma calls outside repositories
  • DTOs required for all IO

1b) Observability + Auditing (must implement)

  • All security-relevant actions must be audited (persisted) with actor identity + tenant context.
  • Identity context must be attached to Sentry (frontend + backend) where possible.
  • Never log sensitive secrets (passwords, tokens, API keys). Redact where needed.

2) RBAC model (explicit, non-magical)

  • Permissions are string constants (stable identifiers), e.g. orders.read, orders.write.
  • Roles are named bundles of permissions, e.g. admin, operator, viewer.
  • Users can have multiple roles (effective permissions = union).
  • Authorization uses typed errors and no silent failures.

3) Tenant model (tenant-ready, single tenant now)

  • Add a Tenant table.
  • Add tenantId foreign keys to all tenant-owned data.
  • Enforce tenant scoping in repositories and service entrypoints.
  • System behavior defaults to a single seeded tenant (e.g. default).

📁 Files to Create/Modify (high-level)

Backend (NestJS)

apps/api/src/
├── auth/
│   ├── dto/
│   │   ├── login.dto.ts
│   │   ├── session.dto.ts
│   │   └── user.dto.ts
│   ├── guards/
│   │   ├── session.guard.ts
│   │   └── permissions.guard.ts
│   ├── decorators/
│   │   ├── current-user.decorator.ts
│   │   └── require-permissions.decorator.ts
│   ├── services/
│   │   ├── auth.service.ts
│   │   ├── password-hasher.service.ts
│   │   └── permission.service.ts
│   ├── auth.controller.ts
│   ├── auth.module.ts
│   └── permissions.ts
│
├── audit/
│   ├── dto/
│   │   └── audit-event.dto.ts
│   ├── audit.repository.ts
│   ├── audit.service.ts
│   └── audit.module.ts
│
├── tenancy/
│   ├── tenant-context.ts
│   ├── tenant-context.service.ts
│   ├── tenant.resolver.ts
│   └── tenancy.module.ts
│
├── users/
│   ├── dto/
│   │   ├── create-user.dto.ts
│   │   ├── update-user-roles.dto.ts
│   │   └── user-list.dto.ts
│   ├── users.controller.ts
│   ├── users.service.ts
│   ├── users.repository.ts
│   └── users.module.ts
│
└── (existing modules updated to enforce permissions + tenant scoping)

Frontend (React dashboard)

apps/web/src/
├── auth/
│   ├── api/
│   │   └── auth.api.ts
│   ├── hooks/
│   │   ├── use-auth.ts
│   │   └── use-permissions.ts
│   ├── auth-context.tsx
│   └── types.ts
│
├── pages/
│   ├── login/ (update to support user login)
│   └── admin/
│       └── users/ (minimal user management UI)
│
└── (routes updated to gate admin pages by permission)

Database (Prisma)

prisma/schema.prisma   # UPDATE: add Tenant, User, Role, Permission, join tables, tenantId FKs
prisma/seed.ts         # UPDATE: ensure default tenant + initial admin user/role
prisma/migrations/     # NEW migration(s)

🔐 Authentication Approach (No Keycloak Yet)

Implement local user authentication with secure password hashing and server-side sessions.

Requirements

  • Use httpOnly cookie sessions (recommended) OR short-lived JWT + refresh token (cookie). Prefer a minimal, secure approach.
  • Passwords must be hashed with argon2 (preferred) or bcrypt with strong settings.
  • Include secure cookie settings:
  • httpOnly: true
  • secure: true in production
  • sameSite: 'lax' (or 'strict' if feasible)
  • Provide endpoints:
  • POST /api/v1/auth/login
  • POST /api/v1/auth/logout
  • GET /api/v1/auth/me

Backward compatibility (important)

  • Keep the current API key mechanism available as a legacy operator bootstrap for now, but:
  • Gate it behind an env flag (e.g. AUTH_ALLOW_LEGACY_API_KEY=true).
  • Treat legacy API key sessions as a specific principal with a very explicit role (e.g. legacy-admin) and log usage.

🧾 Auditing Requirements (Security + Compliance)

What to audit (minimum set)

Audit events must be persisted (database) for:

  • Authentication:
  • login success
  • login failure (without storing passwords)
  • logout
  • Authorization:
  • permission denied (include required permissions and route)
  • User & Role administration:
  • user created / deactivated
  • password reset / password changed
  • roles assigned/removed
  • role created/updated
  • Tenant context changes (future-proof):
  • tenant created (even if only default tenant exists now)

Audit event fields (required)

Every audit record must include:

  • tenantId
  • actorUserId (nullable for unauthenticated events like failed login)
  • actorEmail (nullable)
  • actorRoles (array or computed snapshot)
  • action (string identifier, e.g. auth.login.success)
  • targetType + targetId (nullable, for actions on specific entities)
  • success boolean
  • ipAddress and userAgent
  • requestId / correlation id (if available)
  • createdAt
  • metadata (JSON) for structured details (never secrets)

Implementation notes

  • Prefer integrating with the existing logging/event infrastructure if present, but do not conflate “business event logs” with “security audit logs” unless the model cleanly supports the required fields above.
  • Audit logging must be best-effort: a failure to write an audit record must not crash webhook processing, but it must be reported to Sentry (with identity context if available).

✅ RBAC Requirements

Permissions (explicit list)

Create apps/api/src/auth/permissions.ts with constants such as:

  • Orders:
  • orders.read
  • orders.write
  • Print Jobs:
  • printJobs.read
  • printJobs.write
  • Product Mappings:
  • mappings.read
  • mappings.write
  • Shipments:
  • shipments.read
  • shipments.write
  • Logs/Observability:
  • logs.read
  • observability.read
  • Admin:
  • users.read
  • users.write
  • roles.read
  • roles.write

Roles (initial defaults)

Seed at least:

  • admin: all permissions
  • operator: day-to-day operations (read/write orders, print jobs, shipments, mappings; read logs)
  • viewer: read-only access (read orders/print jobs/shipments/mappings/logs)

Enforcement (server-side)

  • Implement @RequirePermissions(...) decorator and a PermissionsGuard.
  • Apply it to all sensitive controllers:
  • Orders, Print Jobs, Shipments, Product Mappings, Event Logs, Settings/Admin.
  • The guard must compute effective permissions from the authenticated user’s roles.

🧩 Multi-Tenancy (Tenant-Ready, Single Tenant Now)

Tenant resolution rules (for now)

  • For authenticated users: tenant is user.tenantId.
  • For legacy API key principal: tenant is the single default tenant.
  • For webhooks (Shopify/SimplyPrint): assign events to the single default tenant for now, but implement the code so it can later map:
  • Shopify: x-shopify-shop-domain -> tenantId
  • SimplyPrint: per-tenant token or per-tenant webhook path (future)

Repository enforcement (non-negotiable)

  • Every repository method that reads/writes tenant-owned entities must require a tenantId input (or a TenantContext object).
  • Do not allow “global” reads by default.
  • Add appropriate compound indexes/unique constraints using tenant scoping, e.g.:
  • (tenantId, shopifyOrderId) unique
  • (tenantId, webhookId) for idempotency where relevant

🧭 Identity Context Propagation (Web → API) + Sentry Enrichment

Goal

Ensure the same identity context established in the UI (after login) flows into the API handling and is applied consistently to:

  • backend logs and Sentry events
  • frontend Sentry events
  • audit events

Backend requirements (Sentry scope)

On each authenticated request, enrich Sentry scope with:

  • user.id, user.email
  • tags: tenantId, userId
  • context: roles (and optionally permissions if reasonably small)
  • request context: requestId, route/controller/action if available

Also:

  • clear user context for unauthenticated requests
  • ensure permission-denied exceptions include required permission(s) in Sentry breadcrumbs/tags (but not secrets)

Frontend requirements (Sentry scope)

When the user is authenticated in the dashboard:

  • call Sentry setUser({ id, email })
  • set tags/contexts: tenantId, roles

When the user logs out:

  • clear Sentry user/context (setUser(null)), clear related tags/contexts

“Identity context coming from the UI should also flow into the API”

Implement this in a way that remains compatible with secure session cookies:

  • If using cookie sessions, the API must derive identity from the server-side session, but still propagate:
  • a stable requestId / correlation id
  • (optional) a lightweight header like x-client-request-id for debugging (must be non-secret)
  • Ensure the API uses the authenticated principal as the source of truth for identity (never trust client-provided userId/roles/tenantId).

🧪 Testing Requirements

Backend unit tests (Jest)

Add/extend tests for:

  • Auth login/logout/me
  • Password hashing verification (no plaintext comparisons)
  • Permissions guard:
  • allows when permission present
  • denies when missing
  • denies when unauthenticated
  • Tenant context resolution:
  • user tenant
  • legacy API key tenant
  • webhook default tenant
  • Repository scoping:
  • queries include tenantId filter
  • uniqueness constraints are tenant-scoped

  • Auditing:

  • audit records are created for login success/failure, logout
  • audit record is created for permission denied
  • audit records include tenantId + actor fields correctly

  • Sentry identity context:

  • Sentry scope contains user + tenant tags on authenticated requests (mock Sentry client)
  • Sentry scope is cleared/empty on unauthenticated requests

Frontend tests (Vitest)

  • Auth context:
  • login success stores user + permissions
  • logout clears session state
  • Route gating:
  • admin users page requires users.read (or users.write for edit actions)

  • Sentry identity context:

  • login sets Sentry user + tenant tags
  • logout clears Sentry user/context

Acceptance tests (Playwright + Gherkin)

Add scenarios:

  • Admin can log in and access Users page
  • Viewer cannot access Users page (gets denied/redirected)
  • Operator cannot perform admin-only actions
  • Unauthorized actions are denied and audited (validate via API or admin audit log UI if added)

✅ Validation Checklist

  • pnpm nx build api succeeds
  • pnpm nx build web succeeds
  • pnpm lint passes
  • No TypeScript errors
  • Prisma migration runs cleanly
  • Default tenant is present in DB
  • At least one admin user exists (seeded)
  • API enforces permissions (cannot bypass via direct HTTP calls)
  • Webhooks still work (processed under default tenant)

🎬 Execution Order

  1. Add Prisma models: Tenant, User, Role, Permission (+ join tables) and tenant foreign keys
  2. Create migration + update seed:
  3. ensure default tenant
  4. seed admin/operator/viewer roles and permissions
  5. seed initial admin user with secure password
  6. Implement backend auth:
  7. session/cookie auth
  8. /auth/login, /auth/logout, /auth/me
  9. Implement backend RBAC:
  10. permission constants
  11. permissions computation
  12. @RequirePermissions + guard
  13. apply to controllers
  14. Implement tenancy enforcement:
  15. tenant context
  16. repository scoping rules
  17. update existing repositories/services accordingly
  18. Update web app auth:
  19. login with user credentials
  20. auth context + protected routes
  21. permission-aware navigation/UI
  22. Add minimal admin UI for user management (create user, assign roles)
  23. Add tests (unit + acceptance) and ensure they pass

📊 Expected Output

Verification Commands

# Database
pnpm prisma migrate dev
pnpm prisma db seed

# Build
pnpm nx build api
pnpm nx build web

# Lint + tests
pnpm lint
pnpm test
pnpm nx test acceptance-tests

Success Criteria

  • You can create at least 3 users (admin/operator/viewer) and verify:
  • Admin can manage users and roles.
  • Operator can perform operational tasks but cannot manage users.
  • Viewer can only read.
  • All tenant-owned data is automatically scoped to the (single) default tenant.
  • The codebase is tenant-ready for future multi-tenant enablement (mapping integrations to tenants later).

📝 Documentation Updates

Update docs to reflect:

  • How to create users / rotate the legacy API key (if still enabled)
  • How RBAC permissions and roles are defined
  • How tenant scoping works (today: default tenant; future: multiple)
  • How webhook tenant mapping will evolve (Shopify shop domain mapping; SimplyPrint token/path strategy)

🚫 Constraints and Rules

MUST DO

  • Use strict typing; do not introduce any, unknown, ts-ignore, or eslint disables
  • Enforce RBAC server-side with guards
  • Enforce tenant scoping in repositories
  • Keep webhooks functional (default tenant for now)
  • Provide a safe bootstrap path (seeded admin user)

MUST NOT

  • Do not add Keycloak/OIDC now
  • Do not store secrets in client code
  • Do not rely on UI-only gating for security
  • Do not introduce “magic” implicit tenant selection; tenant context must be explicit and testable

END OF PROMPT

This prompt implements RBAC and tenant-ready persistence (single default tenant today) for Forma3D.Connect. The AI should implement the full end-to-end feature set (API + web + database + tests) with minimal disruption to existing webhook/integration flows, and without introducing external identity providers yet.