@stacksjs/defaults 0.75.94 → 0.75.96

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -311,6 +311,48 @@ review and must not be silently recorded.
311
311
  - `/kanban` - model-backed board management
312
312
  - `/ci`, `/buddy` - CI and Buddy workflows
313
313
 
314
+ ## Who may reach a dashboard surface
315
+
316
+ Two axes, declared in different places.
317
+
318
+ **Route groups** are gated in `config/dashboard.ts`, keyed by the group's path
319
+ prefix:
320
+
321
+ ```ts
322
+ access: {
323
+ '/api/commerce': ['admin', 'superadmin'],
324
+ '/dashboard/commerce': ['admin', 'superadmin'],
325
+ '/cms': ['admin', 'editor'],
326
+ }
327
+ ```
328
+
329
+ Each entry appends `role:<the list>` after that group's `auth`, so any one of
330
+ the named roles admits. Every group ships `auth` and nothing more, so an
331
+ omitted group stays reachable by any signed-in user; naming it is the only way
332
+ to tighten it, since `defaults/routes/dashboard.ts` is framework-owned
333
+ (stacksjs/stacks#2883). Keyed by prefix, so gating `/api/commerce` does not
334
+ gate `/dashboard/commerce`: name both when you mean both. The prefixes are a
335
+ typed union, so a typo is a compile error rather than a gate that is quietly
336
+ absent.
337
+
338
+ **A model's own generated API** is gated on the model, not here: either
339
+ `dashboard: { roles: ['admin'] }`, which gates the sidebar row and every
340
+ generated route together, or `useApi.middleware` for per-ability control. See
341
+ `stacks-models`.
342
+
343
+ **The sidebar** filters itself. A row carrying roles (from a model's
344
+ `dashboard.roles` or a `nav` item's `roles`) is hidden client-side from viewers
345
+ holding none of them, permissively for unauthenticated viewers so the local dev
346
+ dashboard stays usable (#1843). That is presentation: it hides the link, not
347
+ the endpoint, so it is never the gate on its own.
348
+
349
+ **SSR pages** take the same middleware names as an API route, because the page
350
+ servers build their registry from `app/Middleware.ts`:
351
+
352
+ ```stx
353
+ definePageMeta({ middleware: ['auth', 'role:admin'] })
354
+ ```
355
+
314
356
  ## Dashboard Components (250+)
315
357
 
316
358
  ### Layout Components
@@ -132,6 +132,79 @@ shape, authorization boundary, or aggregation differs from generic CRUD. Do
132
132
  not expose a sensitive model through unguarded generated routes just because a
133
133
  separate dashboard endpoint is protected.
134
134
 
135
+ #### Who may reach the generated routes
136
+
137
+ Omit `middleware` and reads AND writes get `auth`: an undeclared read route is
138
+ how a customer list leaks (#2224). Four declaration forms, narrowest last:
139
+
140
+ ```ts
141
+ middleware: ['auth'] // every ability
142
+ middleware: [] // every ability public; warns at boot
143
+ middleware: { read: [], write: ['auth'] } // public catalog, guarded writes
144
+ middleware: { write: ['auth'], destroy: ['auth', 'role:admin'] } // per ability
145
+ ```
146
+
147
+ `read` covers `index` and `show`; `write` covers `store`, `update` and
148
+ `destroy`; an ability name overrides its side; and `destroy` covers bulk-delete
149
+ with the single delete. Use the per-ability form for the "fewer actions" half of
150
+ a tiered role - "may file an order, may not void one" is `store` versus
151
+ `destroy`, which the read/write split cannot express. Declaring one ability
152
+ leaves the other four on `auth`, so narrowing one route never widens another.
153
+
154
+ A model is also gated by the roles it declares for the dashboard:
155
+
156
+ ```ts
157
+ dashboard: { roles: ['admin'] } // hides the sidebar row AND gates every generated route
158
+ ```
159
+
160
+ That one declaration covers both surfaces (#2883). It appends `role:admin` to
161
+ the resolved middleware of each ability, unless that ability names a `role:`
162
+ itself (explicit wins) or was explicitly emptied (a deliberately public surface
163
+ is reported at boot, not quietly closed). `dashboard: { roles: [...], enforce:
164
+ false }` keeps the row hidden and the API as the middleware leaves it, for a row
165
+ hidden only to reduce clutter.
166
+
167
+ Row-level scoping is separate and already on: a model scopes rows by an
168
+ `ownership` config or a `team_id` column, and `security.api.rowScoping` defaults
169
+ to `'deny'`, which withholds store/update/destroy from a model that scopes
170
+ neither (#2375). A role says who may call the route; ownership says which rows
171
+ are theirs. A tiered app usually needs both.
172
+
173
+ #### Per-row rules: a policy
174
+
175
+ Middleware answers "may this caller use this endpoint". A policy answers "may
176
+ this caller do this to THIS row", which is the part a role cannot express. The
177
+ generated routes consult one when the model has one (#2883):
178
+
179
+ | Route | Ability | Subject |
180
+ |---|---|---|
181
+ | `index` | `viewAny` | none |
182
+ | `show` | `view` | the row |
183
+ | `store` | `create` | none |
184
+ | `update` | `update` | the existing row |
185
+ | `destroy`, bulk-delete | `delete` | the row, one call each |
186
+
187
+ ```ts
188
+ // app/Policies/OrderPolicy.ts
189
+ export default class OrderPolicy {
190
+ viewAny(user) { return user !== null }
191
+ view(user, order) { return order.customer_id === user?.id }
192
+ delete(user, order) { return order.status === 'draft' && order.customer_id === user?.id }
193
+ }
194
+ ```
195
+
196
+ Register it in `app/Gates.ts` under `policies`, or name it `<Model>Policy` under
197
+ `app/Policies/` and it is discovered by convention. A model with no policy is
198
+ unaffected, so adding one is opt-in per model. A refusal is a 403 carrying the
199
+ policy's own message; an ability the policy does not implement is a refusal too,
200
+ so a partial policy does not leave the rest open.
201
+
202
+ `Gate.inspectFor(model, ability, user, row)` is the same check from your own
203
+ code, keyed by model NAME rather than by instance - which is what a dashboard
204
+ action holding a query-builder row needs. It answers null for "no policy
205
+ registered", which is deliberately distinct from a denial; `Gate.allowsFor`
206
+ is the boolean form that reads null as allowed.
207
+
135
208
  Generated store and update routes accept both spellings of every fillable
136
209
  attribute and each foreign key implied by `belongsTo`. Declaring Product as a
137
210
  belongs-to relation therefore accepts `productId` or `product_id` without
@@ -2,7 +2,7 @@
2
2
  "publisher": "Stacks",
3
3
  "name": "vscode-stacks",
4
4
  "displayName": "Stacks",
5
- "version": "0.75.94",
5
+ "version": "0.75.96",
6
6
  "description": "Stacks, built into VS Code: stx templates, pickier linting and formatting, encrypted env files, the dev server preview and every buddy command. No other extensions required.",
7
7
  "license": "MIT",
8
8
  "funding": "https://github.com/sponsors/chrisbbreuer",
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/defaults",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.75.94",
5
+ "version": "0.75.96",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/stacksjs/stacks.git",
@@ -55,8 +55,8 @@
55
55
  "dependencies": {
56
56
  "@iconify-json/f7": "^1.2.2",
57
57
  "@iconify-json/hugeicons": "^1.2.35",
58
- "@stacksjs/mobile": "^0.75.94",
59
- "@stacksjs/sanitizer": "^0.2.395",
58
+ "@stacksjs/mobile": "^0.75.96",
59
+ "@stacksjs/sanitizer": "^0.2.406",
60
60
  "@xterm/addon-fit": "^0.11.0",
61
61
  "@xterm/xterm": "^6.0.0",
62
62
  "ts-qr-codes": "^0.1.8"
@@ -12,7 +12,7 @@ interface NativeAccountButtonProps {
12
12
  label?: string
13
13
  }
14
14
 
15
- const { href, label = 'Account' } = defineProps<NativeAccountButtonProps>()
15
+ defineProps<NativeAccountButtonProps>()
16
16
  </script>
17
17
 
18
18
  <script client>
@@ -21,6 +21,11 @@ import { haptics } from '@stacksjs/mobile'
21
21
  // The account at the top right of a tab's screen, as iOS apps put it: the
22
22
  // tab bar keeps its tabs for the places the app is used from. Reactive, so it
23
23
  // follows the signed-in person and the count as they change.
24
+ // Every prop through useReactiveProp, under names of its own: read as plain
25
+ // {{ label }}, a host's client signal of the same name (a pull-to-refresh's
26
+ // "Refreshing") was shown instead of the button's own.
27
+ const accountHref = useReactiveProp<string>('href', '')
28
+ const accountLabel = useReactiveProp<string>('label', 'Account')
24
29
  const accountName = useReactiveProp<string>('name', '')
25
30
  const accountImage = useReactiveProp<string>('image', '')
26
31
  const accountBadge = useReactiveProp<string | number>('badge', '')
@@ -42,8 +47,8 @@ function tapAccount(): void {
42
47
  }
43
48
  </script>
44
49
 
45
- <a href="{{ href }}" class="native-account-button" draggable="false" data-stx-link data-stx-prefetch="eager" @click="tapAccount()">
46
- <span class="native-account-label">{{ label }}<span :if="accountCount()">, {{ accountCount() }}</span></span>
50
+ <a :href="accountHref()" class="native-account-button" draggable="false" data-stx-link data-stx-prefetch="eager" @click="tapAccount()">
51
+ <span class="native-account-label">{{ accountLabel() }}<span :if="accountCount()">, {{ accountCount() }}</span></span>
47
52
  <span aria-hidden="true" class="native-account-face" :style="accountPhoto()">
48
53
  <span :if="!accountImage() && accountInitials()">{{ accountInitials() }}</span>
49
54
  <i :if="!accountImage() && !accountInitials()" class="i-lucide-circle-user-round native-account-glyph"></i>
@@ -1,6 +1,7 @@
1
- import { describe, expect, it } from 'bun:test'
1
+ import { describe, expect, it, spyOn } from 'bun:test'
2
2
  import { readFileSync } from 'node:fs'
3
3
  import { join } from 'node:path'
4
+ import { processDirectives } from '@stacksjs/stx'
4
5
 
5
6
  const read = (name: string) => readFileSync(join(import.meta.dir, `${name}.stx`), 'utf8')
6
7
 
@@ -113,6 +114,30 @@ describe('Native components', () => {
113
114
  expect(control).toContain("emit('change', value)")
114
115
  })
115
116
 
117
+ it('renders its segments in the browser, from whatever the options prop holds', async () => {
118
+ // `options` is both the prop and the derived list the segments loop over.
119
+ // stx once took the prop's value in the render context for server data and
120
+ // turned the `:for` into a server `@foreach(options())`, which rendered an
121
+ // empty track (the workout player's rest picker).
122
+ const warn = spyOn(console, 'warn')
123
+ try {
124
+ const html = await processDirectives(
125
+ `<NativeSegmentedControl label="Rest" :options="[{ label: '30s', value: 30 }, { label: '60s', value: 60 }]" />`,
126
+ {},
127
+ join(import.meta.dir, 'page.stx'),
128
+ { componentsDir: import.meta.dir, partialsDir: import.meta.dir } as Parameters<typeof processDirectives>[3],
129
+ new Set<string>(),
130
+ )
131
+ expect(html).toContain('class="native-segmented"')
132
+ expect(html).toContain(':for="(option, index) in options()"')
133
+ expect(html).not.toContain('Foreach Error')
134
+ expect(warn.mock.calls.some(call => String(call[0]).includes('is not iterable server-side'))).toBe(false)
135
+ }
136
+ finally {
137
+ warn.mockRestore()
138
+ }
139
+ })
140
+
116
141
  it('steps the tab bar away for a screen that is a task of its own', () => {
117
142
  const shell = read('NativeAppShell')
118
143
  expect(shell).toContain('.native-app-shell:has([data-native-hide-tab-bar]) .native-app-tab-slot { display: none; }')
@@ -16,7 +16,47 @@
16
16
  */
17
17
 
18
18
  import { isLocalDeployment } from '@stacksjs/env'
19
- import { route } from '@stacksjs/router'
19
+ import { projectPath } from '@stacksjs/path'
20
+ import { dashboardGroupMiddleware, describeUnknownDashboardGroups, resolveDashboardAccess, route } from '@stacksjs/router'
21
+ import { log } from '@stacksjs/logging/runtime'
22
+
23
+ /**
24
+ * Which roles may reach each group below, from `config/dashboard.ts`
25
+ * (stacksjs/stacks#2883).
26
+ *
27
+ * Every group here shipped `middleware: 'auth'` and nothing more, so any
28
+ * signed-in user could reach `/api/commerce`, `/cms` or `/models` whatever the
29
+ * sidebar showed them - and an app could not tighten it, because this file is
30
+ * framework-owned and nothing attached middleware to a path.
31
+ *
32
+ * Read by path rather than through `@stacksjs/config`, matching how the ORM
33
+ * route generator reads `config/security.ts`: registration runs at import time,
34
+ * which is before `overridesReady` resolves, so the config package would hand
35
+ * back framework defaults here. A missing or malformed file resolves to no
36
+ * gating at all, which is exactly how this file behaved before.
37
+ */
38
+ const dashboardAccess = await (async () => {
39
+ try {
40
+ const configured = (await import(projectPath('config/dashboard.ts'))).default?.access
41
+ return resolveDashboardAccess(configured)
42
+ }
43
+ catch {
44
+ return resolveDashboardAccess(undefined)
45
+ }
46
+ })()
47
+
48
+ {
49
+ // A typo'd prefix gates nothing, and would read as a gate that is quietly
50
+ // absent. Named once, not per group.
51
+ const report = describeUnknownDashboardGroups(dashboardAccess.unknown)
52
+ if (report)
53
+ log.warn(report)
54
+ }
55
+
56
+ /** One group's middleware: its `auth` base, plus any role the app declared. */
57
+ function guarded(prefix: string, base?: Parameters<typeof dashboardGroupMiddleware>[2]) {
58
+ return dashboardGroupMiddleware(prefix, dashboardAccess, base)
59
+ }
20
60
 
21
61
  // ============================================================================
22
62
  // Email
@@ -138,9 +178,10 @@ route.get('/robots.txt', 'Actions/RobotsAction')
138
178
  // a public AI endpoint (e.g. a homepage chat widget), it can register the
139
179
  // route in `routes/api.ts` without the middleware — user routes load before
140
180
  // framework routes, so the unauthenticated copy wins.
141
- route.group({ middleware: 'auth' }, () => {
142
- route.post('/ai/ask', 'Actions/AI/AskAction')
143
- route.post('/ai/summary', 'Actions/AI/SummaryAction')
181
+ // Prefixed so `access` can name it; `/ai/ask` and `/ai/summary` as before.
182
+ route.group({ prefix: '/ai', middleware: guarded('/ai') }, () => {
183
+ route.post('/ask', 'Actions/AI/AskAction')
184
+ route.post('/summary', 'Actions/AI/SummaryAction')
144
185
  })
145
186
 
146
187
  // ============================================================================
@@ -152,7 +193,7 @@ route.group({ middleware: 'auth' }, () => {
152
193
  // Always auth-gated.
153
194
  // ============================================================================
154
195
 
155
- route.group({ prefix: '/voide', middleware: 'auth' }, () => {
196
+ route.group({ prefix: '/voide', middleware: guarded('/voide') }, () => {
156
197
  route.get('/state', 'Actions/Buddy/BuddyStateAction')
157
198
  route.post('/repo', 'Actions/Buddy/BuddyRepoOpenAction')
158
199
  route.post('/repo/validate', 'Actions/Buddy/BuddyRepoValidateAction')
@@ -180,7 +221,7 @@ route.group({ prefix: '/voide', middleware: 'auth' }, () => {
180
221
  // the newsletter signup lives at /api/email/subscribe — those stay open.
181
222
  // ============================================================================
182
223
 
183
- route.group({ prefix: '/dashboard', middleware: 'auth' }, () => {
224
+ route.group({ prefix: '/dashboard', middleware: guarded('/dashboard') }, () => {
184
225
  route.get('/home', 'Actions/Dashboard/DashboardHomeAction')
185
226
  route.get('/stats', 'Actions/Dashboard/DashboardStatsAction')
186
227
  route.get('/activity', 'Actions/Dashboard/DashboardActivityAction')
@@ -201,7 +242,7 @@ route.group({ prefix: '/dashboard', middleware: 'auth' }, () => {
201
242
  // action must STILL scope to the authenticated user (derive the customer from
202
243
  // `await request.user()`, never trust the `{id}` path param) to prevent an
203
244
  // authenticated user from reaching another user's billing.
204
- route.group({ prefix: '/payments', middleware: 'auth' }, () => {
245
+ route.group({ prefix: '/payments', middleware: guarded('/payments') }, () => {
205
246
  route.get('/fetch-customer/{id}', 'Actions/Payment/FetchPaymentCustomerAction')
206
247
  route.get('/fetch-transaction-history/{id}', 'Actions/Payment/FetchTransactionHistoryAction')
207
248
  route.get('/fetch-user-subscriptions/{id}', 'Actions/Payment/FetchUserSubscriptionsAction')
@@ -232,11 +273,11 @@ route.group({ prefix: '/payments', middleware: 'auth' }, () => {
232
273
  // Auth-gated to match every other operational dashboard group — these expose
233
274
  // internal job/queue and websocket state and were the only siblings missing
234
275
  // `auth`, leaking infra telemetry to anonymous callers.
235
- route.group({ prefix: '/queues', middleware: 'auth' }, () => {
276
+ route.group({ prefix: '/queues', middleware: guarded('/queues') }, () => {
236
277
  route.get('/', 'Actions/Queue/FetchQueuesAction')
237
278
  })
238
279
 
239
- route.group({ prefix: '/realtime', middleware: 'auth' }, () => {
280
+ route.group({ prefix: '/realtime', middleware: guarded('/realtime') }, () => {
240
281
  route.get('/websockets', 'Actions/Realtime/FetchWebsocketsAction')
241
282
  route.get('/stats', 'Actions/Dashboard/Realtime/RealtimeStatsAction')
242
283
  })
@@ -245,7 +286,7 @@ route.group({ prefix: '/realtime', middleware: 'auth' }, () => {
245
286
  // Query Dashboard
246
287
  // ============================================================================
247
288
 
248
- route.group({ prefix: '/api/queries', middleware: 'auth' }, () => {
289
+ route.group({ prefix: '/api/queries', middleware: guarded('/api/queries') }, () => {
249
290
  route.get('/dashboard', 'Actions/Dashboard/Queries/QueryIndexAction')
250
291
  route.get('/stats', 'Controllers/QueryController@getStats')
251
292
  route.get('/recent', 'Controllers/QueryController@getRecentQueries')
@@ -260,7 +301,7 @@ route.group({ prefix: '/api/queries', middleware: 'auth' }, () => {
260
301
  // Monitoring / Error Tracking
261
302
  // ============================================================================
262
303
 
263
- route.group({ prefix: '/api/monitoring', middleware: 'auth' }, () => {
304
+ route.group({ prefix: '/api/monitoring', middleware: guarded('/api/monitoring') }, () => {
264
305
  route.get('/errors', 'Actions/Monitoring/ErrorIndexAction')
265
306
  route.get('/errors/stats', 'Actions/Monitoring/ErrorStatsAction')
266
307
  route.get('/errors/timeline', 'Actions/Monitoring/ErrorTimelineAction')
@@ -280,7 +321,7 @@ route.group({ prefix: '/api/monitoring', middleware: 'auth' }, () => {
280
321
  // is no longer a DB/CMS mirror. /cms below remains the authoring API.
281
322
  // ============================================================================
282
323
 
283
- route.group({ prefix: '/cms', middleware: 'auth' }, () => {
324
+ route.group({ prefix: '/cms', middleware: guarded('/cms') }, () => {
284
325
  route.get('/dashboard', 'Actions/Dashboard/Content/ContentDashboardAction')
285
326
  route.get('/posts', 'Actions/Cms/PostIndexAction')
286
327
  route.get('/posts/{id}', 'Actions/Cms/PostShowAction')
@@ -334,7 +375,7 @@ route.group({ prefix: '/cms', middleware: 'auth' }, () => {
334
375
  // underlying actions, since user routes load before framework routes.
335
376
  // ============================================================================
336
377
 
337
- route.group({ prefix: '/api/commerce', middleware: 'auth' }, () => {
378
+ route.group({ prefix: '/api/commerce', middleware: guarded('/api/commerce') }, () => {
338
379
  route.get('/dashboard', 'Actions/Dashboard/Commerce/CommerceDashboardAction')
339
380
  route.get('/pos', 'Actions/Dashboard/Commerce/PosIndexAction')
340
381
  route.get('/products', 'Actions/Commerce/Product/ProductIndexAction')
@@ -509,7 +550,7 @@ route.group({ prefix: '/api/commerce', middleware: 'auth' }, () => {
509
550
  // Analytics
510
551
  // ============================================================================
511
552
 
512
- route.group({ prefix: '/api/analytics', middleware: 'auth' }, () => {
553
+ route.group({ prefix: '/api/analytics', middleware: guarded('/api/analytics') }, () => {
513
554
  route.get('/sales', 'Actions/Dashboard/Analytics/SalesAnalyticsAction')
514
555
  route.get('/web', 'Actions/Dashboard/Analytics/WebAnalyticsAction')
515
556
  route.get('/blog', 'Actions/Dashboard/Analytics/BlogAnalyticsAction')
@@ -527,13 +568,13 @@ route.group({ prefix: '/api/analytics', middleware: 'auth' }, () => {
527
568
  // Jobs & Queue
528
569
  // ============================================================================
529
570
 
530
- route.group({ prefix: '/jobs', middleware: 'auth' }, () => {
571
+ route.group({ prefix: '/jobs', middleware: guarded('/jobs') }, () => {
531
572
  route.get('/', 'Actions/Dashboard/Jobs/JobIndexAction')
532
573
  route.get('/stats', 'Actions/Dashboard/Jobs/JobStatsAction')
533
574
  route.post('/{id}/retry', 'Actions/Dashboard/Jobs/JobRetryAction')
534
575
  })
535
576
 
536
- route.group({ prefix: '/queue', middleware: 'auth' }, () => {
577
+ route.group({ prefix: '/queue', middleware: guarded('/queue') }, () => {
537
578
  route.get('/stats', 'Actions/Dashboard/Queue/QueueStatsAction')
538
579
  route.get('/workers', 'Actions/Dashboard/Queue/QueueWorkersAction')
539
580
  route.post('/retry-failed', 'Actions/Dashboard/Queue/QueueRetryFailedAction')
@@ -543,7 +584,7 @@ route.group({ prefix: '/queue', middleware: 'auth' }, () => {
543
584
  // Releases
544
585
  // ============================================================================
545
586
 
546
- route.group({ prefix: '/releases', middleware: 'auth' }, () => {
587
+ route.group({ prefix: '/releases', middleware: guarded('/releases') }, () => {
547
588
  route.get('/', 'Actions/Dashboard/Releases/ReleaseIndexAction')
548
589
  route.get('/stats', 'Actions/Dashboard/Releases/ReleaseIndexAction')
549
590
  })
@@ -552,7 +593,7 @@ route.group({ prefix: '/releases', middleware: 'auth' }, () => {
552
593
  // Settings
553
594
  // ============================================================================
554
595
 
555
- route.group({ prefix: '/api/settings', middleware: 'auth' }, () => {
596
+ route.group({ prefix: '/api/settings', middleware: guarded('/api/settings') }, () => {
556
597
  route.get('/mail', 'Actions/Dashboard/Settings/MailSettingsGetAction')
557
598
  route.put('/mail', 'Actions/Dashboard/Settings/MailSettingsUpdateAction')
558
599
  })
@@ -561,7 +602,7 @@ route.group({ prefix: '/api/settings', middleware: 'auth' }, () => {
561
602
  // Data Management
562
603
  // ============================================================================
563
604
 
564
- route.group({ prefix: '/api/data', middleware: 'auth' }, () => {
605
+ route.group({ prefix: '/api/data', middleware: guarded('/api/data') }, () => {
565
606
  route.get('/subscribers', 'Actions/Dashboard/Data/SubscriberIndexAction')
566
607
  route.get('/teams', 'Actions/Dashboard/Data/TeamIndexAction')
567
608
  route.get('/users', 'Actions/Dashboard/Data/UserIndexAction')
@@ -572,7 +613,7 @@ route.group({ prefix: '/api/data', middleware: 'auth' }, () => {
572
613
  // Infrastructure
573
614
  // ============================================================================
574
615
 
575
- route.group({ prefix: '/infrastructure', middleware: 'auth' }, () => {
616
+ route.group({ prefix: '/infrastructure', middleware: guarded('/infrastructure') }, () => {
576
617
  route.get('/servers', 'Actions/Dashboard/Infrastructure/ServerIndexAction')
577
618
  route.get('/dns', 'Actions/Dashboard/Infrastructure/DnsIndexAction')
578
619
  route.get('/environment', 'Actions/Dashboard/Infrastructure/EnvironmentIndexAction')
@@ -589,7 +630,7 @@ route.get('/api/serverless', 'Actions/Dashboard/Cloud/ServerlessIndexAction').mi
589
630
  // Dashboard Views — Commerce
590
631
  // ============================================================================
591
632
 
592
- route.group({ prefix: '/dashboard/commerce', middleware: 'auth' }, () => {
633
+ route.group({ prefix: '/dashboard/commerce', middleware: guarded('/dashboard/commerce') }, () => {
593
634
  route.get('/customers', 'Actions/Dashboard/Commerce/CommerceCustomersAction')
594
635
  route.get('/orders', 'Actions/Dashboard/Commerce/CommerceOrdersAction')
595
636
  route.get('/products', 'Actions/Dashboard/Commerce/CommerceProductsAction')
@@ -605,7 +646,7 @@ route.group({ prefix: '/dashboard/commerce', middleware: 'auth' }, () => {
605
646
  // Dashboard Views — CMS Content
606
647
  // ============================================================================
607
648
 
608
- route.group({ prefix: '/dashboard/cms', middleware: 'auth' }, () => {
649
+ route.group({ prefix: '/dashboard/cms', middleware: guarded('/dashboard/cms') }, () => {
609
650
  route.get('/posts', 'Actions/Dashboard/Content/PostIndexAction')
610
651
  route.get('/pages', 'Actions/Dashboard/Content/PageIndexAction')
611
652
  route.get('/categories', 'Actions/Dashboard/Content/CategoryIndexAction')
@@ -618,7 +659,7 @@ route.group({ prefix: '/dashboard/cms', middleware: 'auth' }, () => {
618
659
  // Marketing
619
660
  // ============================================================================
620
661
 
621
- route.group({ prefix: '/api/marketing', middleware: 'auth' }, () => {
662
+ route.group({ prefix: '/api/marketing', middleware: guarded('/api/marketing') }, () => {
622
663
  route.get('/campaigns', 'Actions/Dashboard/Marketing/CampaignIndexAction')
623
664
  route.get('/lists', 'Actions/Dashboard/Marketing/ListIndexAction')
624
665
  route.get('/social-posts', 'Actions/Dashboard/Marketing/SocialPostIndexAction')
@@ -628,7 +669,7 @@ route.group({ prefix: '/api/marketing', middleware: 'auth' }, () => {
628
669
  // Notifications
629
670
  // ============================================================================
630
671
 
631
- route.group({ prefix: '/api/notifications', middleware: 'auth' }, () => {
672
+ route.group({ prefix: '/api/notifications', middleware: guarded('/api/notifications') }, () => {
632
673
  route.get('/dashboard', 'Actions/Dashboard/Notifications/NotificationDeliveryOverviewAction')
633
674
  route.get('/email', 'Actions/Dashboard/Notifications/NotificationDeliveryIndexAction')
634
675
  route.get('/sms', 'Actions/Dashboard/Notifications/NotificationDeliveryIndexAction')
@@ -639,7 +680,7 @@ route.group({ prefix: '/api/notifications', middleware: 'auth' }, () => {
639
680
  // Library & Packages
640
681
  // ============================================================================
641
682
 
642
- route.group({ prefix: '/library', middleware: 'auth' }, () => {
683
+ route.group({ prefix: '/library', middleware: guarded('/library') }, () => {
643
684
  route.get('/dependencies', 'Actions/Dashboard/Library/DependencyIndexAction')
644
685
  route.get('/packages', 'Actions/Dashboard/Library/PackageIndexAction')
645
686
  route.get('/functions', 'Actions/Dashboard/Library/GetFunctions')
@@ -658,7 +699,7 @@ route.group({ prefix: '/library', middleware: 'auth' }, () => {
658
699
  // Deployments
659
700
  // ============================================================================
660
701
 
661
- route.group({ prefix: '/deployments', middleware: 'auth' }, () => {
702
+ route.group({ prefix: '/deployments', middleware: guarded('/deployments') }, () => {
662
703
  route.get('/', 'Actions/Dashboard/Deployments/GetDeployments')
663
704
  route.get('/count', 'Actions/Dashboard/Deployments/GetDeploymentCount')
664
705
  route.get('/recent', 'Actions/Dashboard/Deployments/GetRecentDeployments')
@@ -674,7 +715,7 @@ route.group({ prefix: '/deployments', middleware: 'auth' }, () => {
674
715
  // Models
675
716
  // ============================================================================
676
717
 
677
- route.group({ prefix: '/models', middleware: 'auth' }, () => {
718
+ route.group({ prefix: '/models', middleware: guarded('/models') }, () => {
678
719
  route.get('/', 'Actions/Dashboard/Models/GetModels')
679
720
  route.get('/user-count', 'Actions/Dashboard/Models/GetUserCount')
680
721
  route.get('/subscriber-count', 'Actions/Dashboard/Models/GetSubscriberCount')