ReviewOS

also looking at this

stacks/pet-store

umbrella: pet-store storefront fixes (rolling)

#3
Merged glennmichael123 wants to merge umbrella/pet-store into main
586 files +38082 -7487

Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.

.env.examplemodified+6-0
Changes to .env.example
@@ -89,3 +89,9 @@ FRONTEND_APP_ENV="${APP_ENV}"
8989FRONTEND_APP_URL="${APP_URL}"
9090
9191FRONTEND_STRIPE_PUBLIC_KEY=
92
93# Admin gate (see app/Gates.ts → 'access-admin').
94# Either env grants admin access; both are comma-separated lists.
95# Leave both empty to deny all admin access by default.
96ADMIN_EMAILS=
97ADMIN_EMAIL_DOMAINS=
app/Gates.tsmodified+29-2
Changes to app/Gates.ts
@@ -25,10 +25,37 @@ import type { UserModel } from '@stacksjs/orm'
2525 */
2626export const gates = {
2727 /**
28 * Check if user can access admin area
28 * Check if a user can access the admin area.
29 *
30 * Driven by two env vars; either one matching is enough:
31 * - ADMIN_EMAILS comma-separated list of exact emails
32 * (e.g. `chris@barebowl.com,glenn@example.com`)
33 * - ADMIN_EMAIL_DOMAINS comma-separated list of email domains
34 * (with or without leading `@`, e.g.
35 * `barebowl.com,stacksjs.org`)
36 *
37 * Default is deny-all when neither is set — safer than carrying a
38 * hardcoded `@stacksjs.org` allowlist into apps that aren't run by
39 * stacksjs. Comparison is case-insensitive on the email side and
40 * matched against the suffix `@<domain>` so a user typing
41 * `Chris@BareBowl.com` still matches an `ADMIN_EMAIL_DOMAINS=barebowl.com`
42 * entry.
2943 */
3044 'access-admin': (user: UserModel | null) => {
31 return user?.email?.endsWith('@stacksjs.org') ?? false
45 const email = user?.email?.toLowerCase()
46 if (!email) return false
47
48 const explicit = (process.env.ADMIN_EMAILS ?? '')
49 .split(',')
50 .map(s => s.trim().toLowerCase())
51 .filter(Boolean)
52 if (explicit.includes(email)) return true
53
54 const domains = (process.env.ADMIN_EMAIL_DOMAINS ?? '')
55 .split(',')
56 .map(s => s.trim().toLowerCase().replace(/^@/, ''))
57 .filter(Boolean)
58 return domains.some(domain => email.endsWith(`@${domain}`))
3259 },
3360
3461 /**
storage/framework/defaults/app/Gates.tsmodified+24-2
Changes to storage/framework/defaults/app/Gates.ts
@@ -25,10 +25,32 @@ import type { UserModel } from '@stacksjs/orm'
2525 */
2626export const gates = {
2727 /**
28 * Check if user can access admin area
28 * Check if a user can access the admin area.
29 *
30 * Driven by two env vars; either one matching is enough:
31 * - ADMIN_EMAILS comma-separated list of exact emails
32 * - ADMIN_EMAIL_DOMAINS comma-separated list of email domains
33 * (with or without leading `@`)
34 *
35 * Default is deny-all when neither is set — safer than carrying a
36 * hardcoded allowlist into every Stacks app that scaffolds from
37 * this default.
2938 */
3039 'access-admin': (user: UserModel | null) => {
31 return user?.email?.endsWith('@stacksjs.org') ?? false
40 const email = user?.email?.toLowerCase()
41 if (!email) return false
42
43 const explicit = (process.env.ADMIN_EMAILS ?? '')
44 .split(',')
45 .map(s => s.trim().toLowerCase())
46 .filter(Boolean)
47 if (explicit.includes(email)) return true
48
49 const domains = (process.env.ADMIN_EMAIL_DOMAINS ?? '')
50 .split(',')
51 .map(s => s.trim().toLowerCase().replace(/^@/, ''))
52 .filter(Boolean)
53 return domains.some(domain => email.endsWith(`@${domain}`))
3254 },
3355
3456 /**