-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathroutes.ts
More file actions
134 lines (120 loc) · 7.28 KB
/
Copy pathroutes.ts
File metadata and controls
134 lines (120 loc) · 7.28 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
import router from '@adonisjs/core/services/router'
import { middleware } from '#start/kernel'
import { enforceQuota } from '@adonisjs-lasagna/saas-tenancy/middleware'
import { multitenancyRoutes } from '@adonisjs-lasagna/saas-tenancy/health'
import { multitenancyAdminRoutes } from '@adonisjs-lasagna/admin'
import { multitenancyBillingRoutes } from '@adonisjs-lasagna/billing'
import { multitenancyReportingRoutes } from '@adonisjs-lasagna/reporting'
import { multitenancyAiRoutes } from '@adonisjs-lasagna/ai/routes'
/**
* Lazy controller imports — keeps the route file small and lets the
* framework instantiate controllers per request via the IoC container,
* which is required for `@inject()`-decorated constructor parameters.
*/
const TenantsController = () => import('#app/controllers/demo/tenants_controller')
const NotesController = () => import('#app/controllers/demo/notes_controller')
const QuotaController = () => import('#app/controllers/demo/quota_controller')
const DoctorController = () => import('#app/controllers/demo/doctor_controller')
const CircuitController = () => import('#app/controllers/demo/circuit_controller')
const AuditController = () => import('#app/controllers/demo/audit_controller')
const LogController = () => import('#app/controllers/demo/log_controller')
const WebhooksController = () => import('#app/controllers/demo/webhooks_controller')
const FeatureFlagsController = () => import('#app/controllers/demo/feature_flags_controller')
const BrandingController = () => import('#app/controllers/demo/branding_controller')
const SsoController = () => import('#app/controllers/demo/sso_controller')
const BillingController = () => import('#app/controllers/demo/billing_controller')
/* ─── Operational endpoints (livez / readyz / healthz / metrics) ─────────── */
// `/livez` and `/readyz` stay public for k8s probes. `/metrics` leaks tenant
// enumeration + business KPIs, so it is fail-closed: gate it with the same auth
// as the admin API. (Pass `metricsMiddleware: false` only to mount it public
// behind a trusted network boundary.)
multitenancyRoutes({ metricsMiddleware: [middleware.demoAdminAuth()] })
/* ─── Package admin REST API (header-token gated) ────────────────────────── */
multitenancyAdminRoutes({
prefix: '/admin',
middleware: [middleware.demoAdminAuth()],
// The demo auth is a static shared token with no user identity, so we read
// the acting admin from an optional `x-admin-id` header (a real app returns
// `auth.user?.id`). The fallback keeps impersonation working, which needs a
// non-null actor; the header lets the e2e suite assert audit attribution.
// `tenant_audit_logs.actor_id` is a uuid column, so the id must be a uuid.
resolveAdminActor: (ctx) =>
ctx.request.header('x-admin-id') ?? 'dec0ffee-0000-4000-8000-000000000000',
})
/* ─── Stripe webhook receiver (ungated — in ignorePaths) ─────────────────── */
multitenancyBillingRoutes()
/* ─── Cross-tenant reporting dashboard (fail-closed, admin-gated) ─────────── */
// Fleet-wide analytics, so it carries the same admin auth as /admin and /metrics.
// `openapi: true` mounts /admin/reporting/openapi.json + /docs under the same auth.
multitenancyReportingRoutes({
prefix: '/admin/reporting',
middleware: [middleware.demoAdminAuth()],
openapi: true,
// Cache dashboard responses; config.reporting.cache.invalidateOnFlush clears
// them on every tenant:metrics:flush so the view stays fresh.
cacheTtlMs: 60_000,
})
/* ─── AI gateway (@adonisjs-lasagna/ai) — tenant-scoped, fail-closed mount ── */
// POST /ai/chat (SSE), /ai/embed, /ai/retrieve. TenantGuard FIRST per the mount
// contract; config.ai.authorizeAIAccess is the per-request membership gate. The
// demo runs fully offline through the mock providers registered in AppProvider.
multitenancyAiRoutes({ middleware: [middleware.tenantGuard()] })
/* ─── Impersonation verify probe ─────────────────────────────────────────── */
// Echoes the verified impersonation context attached by ImpersonationMiddleware,
// so a request carrying a token minted via the admin API can prove it resolves
// end to end (the e2e impersonation flow asserts on `impersonation`). No tenant
// guard: the middleware binds the token to its issuing tenant on its own.
router
.get('/demo/impersonation-check', async (ctx: any) => {
return ctx.response.ok({ impersonation: ctx.impersonation ?? null })
})
.use([middleware.impersonation()])
/* ─── /demo: tenant CRUD (no tenant guard — no tenant context yet) ───────── */
router
.group(() => {
router.get('/tenants', [TenantsController, 'list'])
router.post('/tenants', [TenantsController, 'create'])
router.get('/tenants/:id', [TenantsController, 'show'])
router.post('/tenants/:id/activate', [TenantsController, 'activate'])
router.post('/tenants/:id/suspend', [TenantsController, 'suspend'])
router.delete('/tenants/:id', [TenantsController, 'destroy'])
})
.prefix('/demo')
/* ─── /demo: tenant-scoped feature surface (TenantGuardMiddleware) ───────── */
router
.group(() => {
// Schema isolation probe
router.get('/connection', [TenantsController, 'connection'])
// Notes (raw-SQL through the tenant connection) + per-day quota gate
router.get('/notes', [NotesController, 'list'])
router.get('/notes/read', [NotesController, 'listFromReplica'])
router.post('/notes', [NotesController, 'create']).use(enforceQuota('apiCallsPerDay'))
// Quotas / doctor / circuit / audit / contextual-logging probe
router.get('/quota/state', [QuotaController, 'state'])
router.post('/quota/track', [QuotaController, 'track'])
router.get('/doctor', [DoctorController, 'run'])
router.get('/circuit', [CircuitController, 'state'])
router.get('/audit', [AuditController, 'list'])
router.get('/log/emit', [LogController, 'emit'])
// Webhook subscriptions + manual fire
router.get('/webhooks', [WebhooksController, 'list'])
router.post('/webhooks', [WebhooksController, 'subscribe'])
router.post('/webhooks/fire', [WebhooksController, 'fire'])
// Satellites: feature flags / branding / SSO
router.get('/feature-flags', [FeatureFlagsController, 'list'])
router.post('/feature-flags', [FeatureFlagsController, 'set'])
router.delete('/feature-flags/:flag', [FeatureFlagsController, 'destroy'])
router.get('/branding', [BrandingController, 'show'])
router.put('/branding', [BrandingController, 'update'])
router.get('/sso', [SsoController, 'show'])
router.put('/sso', [SsoController, 'update'])
// Billing (Stripe) — added incrementally alongside the satellites above
router.get('/billing', [BillingController, 'show'])
router.post('/billing/checkout', [BillingController, 'checkout'])
})
.prefix('/demo')
.use(middleware.tenantGuard())
// Feed the metrics pipeline: count every tenant-scoped request. bypassInTestEnv
// is false so the e2e suite exercises the recording path (it resolves the tenant
// from the request header even after the guard's async scope unwinds).
.use([middleware.trackMetrics({ bypassInTestEnv: false })])