@polardbx/pxbaas 0.0.0-stage → 0.1.0-beta.10
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.
- package/README.md +223 -2
- package/bin/pxbaas.js +13 -0
- package/dist/app/local-index.d.ts +70 -0
- package/dist/app/local-index.d.ts.map +1 -0
- package/dist/app/local-index.js +140 -0
- package/dist/app/local-index.js.map +1 -0
- package/dist/app/preflight.d.ts +61 -0
- package/dist/app/preflight.d.ts.map +1 -0
- package/dist/app/preflight.js +115 -0
- package/dist/app/preflight.js.map +1 -0
- package/dist/app/spec-lint.d.ts +45 -0
- package/dist/app/spec-lint.d.ts.map +1 -0
- package/dist/app/spec-lint.js +423 -0
- package/dist/app/spec-lint.js.map +1 -0
- package/dist/app/templates.d.ts +167 -0
- package/dist/app/templates.d.ts.map +1 -0
- package/dist/app/templates.js +151 -0
- package/dist/app/templates.js.map +1 -0
- package/dist/cli/help-guides.d.ts +17 -0
- package/dist/cli/help-guides.d.ts.map +1 -0
- package/dist/cli/help-guides.js +132 -0
- package/dist/cli/help-guides.js.map +1 -0
- package/dist/cli/root.d.ts +3 -0
- package/dist/cli/root.d.ts.map +1 -0
- package/dist/cli/root.js +75 -0
- package/dist/cli/root.js.map +1 -0
- package/dist/commands/app/index.d.ts +8 -0
- package/dist/commands/app/index.d.ts.map +1 -0
- package/dist/commands/app/index.js +1907 -0
- package/dist/commands/app/index.js.map +1 -0
- package/dist/commands/auth/index.d.ts +3 -0
- package/dist/commands/auth/index.d.ts.map +1 -0
- package/dist/commands/auth/index.js +198 -0
- package/dist/commands/auth/index.js.map +1 -0
- package/dist/commands/completion/index.d.ts +3 -0
- package/dist/commands/completion/index.d.ts.map +1 -0
- package/dist/commands/completion/index.js +14 -0
- package/dist/commands/completion/index.js.map +1 -0
- package/dist/commands/data/index.d.ts +55 -0
- package/dist/commands/data/index.d.ts.map +1 -0
- package/dist/commands/data/index.js +407 -0
- package/dist/commands/data/index.js.map +1 -0
- package/dist/commands/db/index.d.ts +29 -0
- package/dist/commands/db/index.d.ts.map +1 -0
- package/dist/commands/db/index.js +500 -0
- package/dist/commands/db/index.js.map +1 -0
- package/dist/commands/doctor/index.d.ts +3 -0
- package/dist/commands/doctor/index.d.ts.map +1 -0
- package/dist/commands/doctor/index.js +305 -0
- package/dist/commands/doctor/index.js.map +1 -0
- package/dist/commands/functions/delete.d.ts +3 -0
- package/dist/commands/functions/delete.d.ts.map +1 -0
- package/dist/commands/functions/delete.js +55 -0
- package/dist/commands/functions/delete.js.map +1 -0
- package/dist/commands/functions/deploy.d.ts +3 -0
- package/dist/commands/functions/deploy.d.ts.map +1 -0
- package/dist/commands/functions/deploy.js +118 -0
- package/dist/commands/functions/deploy.js.map +1 -0
- package/dist/commands/functions/index.d.ts +3 -0
- package/dist/commands/functions/index.d.ts.map +1 -0
- package/dist/commands/functions/index.js +15 -0
- package/dist/commands/functions/index.js.map +1 -0
- package/dist/commands/functions/list.d.ts +6 -0
- package/dist/commands/functions/list.d.ts.map +1 -0
- package/dist/commands/functions/list.js +63 -0
- package/dist/commands/functions/list.js.map +1 -0
- package/dist/commands/functions/new.d.ts +7 -0
- package/dist/commands/functions/new.d.ts.map +1 -0
- package/dist/commands/functions/new.js +72 -0
- package/dist/commands/functions/new.js.map +1 -0
- package/dist/commands/gen/index.d.ts +3 -0
- package/dist/commands/gen/index.d.ts.map +1 -0
- package/dist/commands/gen/index.js +323 -0
- package/dist/commands/gen/index.js.map +1 -0
- package/dist/commands/guide/index.d.ts +8 -0
- package/dist/commands/guide/index.d.ts.map +1 -0
- package/dist/commands/guide/index.js +20 -0
- package/dist/commands/guide/index.js.map +1 -0
- package/dist/commands/instance/index.d.ts +8 -0
- package/dist/commands/instance/index.d.ts.map +1 -0
- package/dist/commands/instance/index.js +481 -0
- package/dist/commands/instance/index.js.map +1 -0
- package/dist/commands/login/index.d.ts +3 -0
- package/dist/commands/login/index.d.ts.map +1 -0
- package/dist/commands/login/index.js +145 -0
- package/dist/commands/login/index.js.map +1 -0
- package/dist/commands/logout/index.d.ts +3 -0
- package/dist/commands/logout/index.d.ts.map +1 -0
- package/dist/commands/logout/index.js +41 -0
- package/dist/commands/logout/index.js.map +1 -0
- package/dist/commands/profile/import-env.d.ts +18 -0
- package/dist/commands/profile/import-env.d.ts.map +1 -0
- package/dist/commands/profile/import-env.js +206 -0
- package/dist/commands/profile/import-env.js.map +1 -0
- package/dist/commands/profile/index.d.ts +3 -0
- package/dist/commands/profile/index.d.ts.map +1 -0
- package/dist/commands/profile/index.js +124 -0
- package/dist/commands/profile/index.js.map +1 -0
- package/dist/commands/rest/index.d.ts +3 -0
- package/dist/commands/rest/index.d.ts.map +1 -0
- package/dist/commands/rest/index.js +102 -0
- package/dist/commands/rest/index.js.map +1 -0
- package/dist/commands/skill/index.d.ts +11 -0
- package/dist/commands/skill/index.d.ts.map +1 -0
- package/dist/commands/skill/index.js +228 -0
- package/dist/commands/skill/index.js.map +1 -0
- package/dist/commands/status/index.d.ts +3 -0
- package/dist/commands/status/index.d.ts.map +1 -0
- package/dist/commands/status/index.js +59 -0
- package/dist/commands/status/index.js.map +1 -0
- package/dist/commands/storage/index.d.ts +3 -0
- package/dist/commands/storage/index.d.ts.map +1 -0
- package/dist/commands/storage/index.js +400 -0
- package/dist/commands/storage/index.js.map +1 -0
- package/dist/commands/tenant/index.d.ts +19 -0
- package/dist/commands/tenant/index.d.ts.map +1 -0
- package/dist/commands/tenant/index.js +667 -0
- package/dist/commands/tenant/index.js.map +1 -0
- package/dist/commands/whoami/index.d.ts +3 -0
- package/dist/commands/whoami/index.d.ts.map +1 -0
- package/dist/commands/whoami/index.js +101 -0
- package/dist/commands/whoami/index.js.map +1 -0
- package/dist/credentials/commit.d.ts +38 -0
- package/dist/credentials/commit.d.ts.map +1 -0
- package/dist/credentials/commit.js +57 -0
- package/dist/credentials/commit.js.map +1 -0
- package/dist/credentials/credential-store.d.ts +86 -0
- package/dist/credentials/credential-store.d.ts.map +1 -0
- package/dist/credentials/credential-store.js +282 -0
- package/dist/credentials/credential-store.js.map +1 -0
- package/dist/credentials/env-file.d.ts +85 -0
- package/dist/credentials/env-file.d.ts.map +1 -0
- package/dist/credentials/env-file.js +193 -0
- package/dist/credentials/env-file.js.map +1 -0
- package/dist/credentials/formatter.d.ts +39 -0
- package/dist/credentials/formatter.d.ts.map +1 -0
- package/dist/credentials/formatter.js +79 -0
- package/dist/credentials/formatter.js.map +1 -0
- package/dist/credentials/index.d.ts +13 -0
- package/dist/credentials/index.d.ts.map +1 -0
- package/dist/credentials/index.js +13 -0
- package/dist/credentials/index.js.map +1 -0
- package/dist/credentials/jwt-introspect.d.ts +39 -0
- package/dist/credentials/jwt-introspect.d.ts.map +1 -0
- package/dist/credentials/jwt-introspect.js +64 -0
- package/dist/credentials/jwt-introspect.js.map +1 -0
- package/dist/credentials/masker.d.ts +23 -0
- package/dist/credentials/masker.d.ts.map +1 -0
- package/dist/credentials/masker.js +40 -0
- package/dist/credentials/masker.js.map +1 -0
- package/dist/credentials/profile.d.ts +95 -0
- package/dist/credentials/profile.d.ts.map +1 -0
- package/dist/credentials/profile.js +207 -0
- package/dist/credentials/profile.js.map +1 -0
- package/dist/credentials/tenant-guard.d.ts +16 -0
- package/dist/credentials/tenant-guard.d.ts.map +1 -0
- package/dist/credentials/tenant-guard.js +53 -0
- package/dist/credentials/tenant-guard.js.map +1 -0
- package/dist/credentials/tty-guard.d.ts +29 -0
- package/dist/credentials/tty-guard.d.ts.map +1 -0
- package/dist/credentials/tty-guard.js +79 -0
- package/dist/credentials/tty-guard.js.map +1 -0
- package/dist/db/apply-schema.d.ts +144 -0
- package/dist/db/apply-schema.d.ts.map +1 -0
- package/dist/db/apply-schema.js +359 -0
- package/dist/db/apply-schema.js.map +1 -0
- package/dist/db/introspect.d.ts +54 -0
- package/dist/db/introspect.d.ts.map +1 -0
- package/dist/db/introspect.js +140 -0
- package/dist/db/introspect.js.map +1 -0
- package/dist/db/sql-file.d.ts +104 -0
- package/dist/db/sql-file.d.ts.map +1 -0
- package/dist/db/sql-file.js +584 -0
- package/dist/db/sql-file.js.map +1 -0
- package/dist/instance/provision.d.ts +89 -0
- package/dist/instance/provision.d.ts.map +1 -0
- package/dist/instance/provision.js +139 -0
- package/dist/instance/provision.js.map +1 -0
- package/dist/instance/zero-store.d.ts +47 -0
- package/dist/instance/zero-store.d.ts.map +1 -0
- package/dist/instance/zero-store.js +91 -0
- package/dist/instance/zero-store.js.map +1 -0
- package/dist/instance/zero-whitelist.d.ts +39 -0
- package/dist/instance/zero-whitelist.d.ts.map +1 -0
- package/dist/instance/zero-whitelist.js +67 -0
- package/dist/instance/zero-whitelist.js.map +1 -0
- package/dist/main.d.ts +3 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +69 -0
- package/dist/main.js.map +1 -0
- package/dist/skill/agents-md.d.ts +25 -0
- package/dist/skill/agents-md.d.ts.map +1 -0
- package/dist/skill/agents-md.js +62 -0
- package/dist/skill/agents-md.js.map +1 -0
- package/dist/skill/assets.d.ts +38 -0
- package/dist/skill/assets.d.ts.map +1 -0
- package/dist/skill/assets.js +111 -0
- package/dist/skill/assets.js.map +1 -0
- package/dist/skill/hint.d.ts +39 -0
- package/dist/skill/hint.d.ts.map +1 -0
- package/dist/skill/hint.js +109 -0
- package/dist/skill/hint.js.map +1 -0
- package/dist/skill/hosts.d.ts +45 -0
- package/dist/skill/hosts.d.ts.map +1 -0
- package/dist/skill/hosts.js +96 -0
- package/dist/skill/hosts.js.map +1 -0
- package/dist/skill/installer.d.ts +100 -0
- package/dist/skill/installer.d.ts.map +1 -0
- package/dist/skill/installer.js +241 -0
- package/dist/skill/installer.js.map +1 -0
- package/dist/skill/ownership.d.ts +35 -0
- package/dist/skill/ownership.d.ts.map +1 -0
- package/dist/skill/ownership.js +52 -0
- package/dist/skill/ownership.js.map +1 -0
- package/dist/tenant/bundle.d.ts +66 -0
- package/dist/tenant/bundle.d.ts.map +1 -0
- package/dist/tenant/bundle.js +132 -0
- package/dist/tenant/bundle.js.map +1 -0
- package/dist/tenant/guide.d.ts +50 -0
- package/dist/tenant/guide.d.ts.map +1 -0
- package/dist/tenant/guide.js +152 -0
- package/dist/tenant/guide.js.map +1 -0
- package/dist/tenant/probe.d.ts +65 -0
- package/dist/tenant/probe.d.ts.map +1 -0
- package/dist/tenant/probe.js +159 -0
- package/dist/tenant/probe.js.map +1 -0
- package/dist/tenant/verify.d.ts +37 -0
- package/dist/tenant/verify.d.ts.map +1 -0
- package/dist/tenant/verify.js +132 -0
- package/dist/tenant/verify.js.map +1 -0
- package/dist/version.d.ts +29 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +65 -0
- package/dist/version.js.map +1 -0
- package/package.json +63 -3
- package/skills/pxbaas-app-spec/SKILL.md +165 -0
- package/skills/pxbaas-app-spec/references/deploy-options.md +158 -0
- package/skills/pxbaas-app-spec/references/guardrails-pitfalls.md +89 -0
- package/skills/pxbaas-app-spec/references/runtime-contract.md +151 -0
- package/skills/pxbaas-app-spec/references/ssr-session.md +121 -0
- package/skills/pxbaas-app-spec/references/templates-examples.md +311 -0
- package/skills/pxbaas-cli/SKILL.md +186 -0
- package/skills/pxbaas-cli/references/api-reference.md +68 -0
- package/skills/pxbaas-cli/references/app-deploy.md +186 -0
- package/skills/pxbaas-cli/references/auth-and-credentials.md +116 -0
- package/skills/pxbaas-cli/references/data-postgrest.md +101 -0
- package/skills/pxbaas-cli/references/db-and-meta.md +102 -0
- package/skills/pxbaas-cli/references/edge-functions.md +71 -0
- package/skills/pxbaas-cli/references/multi-tenant-apps.md +138 -0
- package/skills/pxbaas-cli/references/provision.md +101 -0
- package/skills/pxbaas-cli/references/safety-and-two-phase.md +78 -0
- package/skills/pxbaas-cli/references/storage.md +61 -0
- package/skills/pxbaas-cli/references/tenant-admin.md +122 -0
- package/skills/pxbaas-cli/references/troubleshooting.md +76 -0
- package/templates/webapp/api/index.ts +55 -0
- package/templates/webapp/csr-bff/README.md +81 -0
- package/templates/webapp/csr-bff/backend/index.ts +168 -0
- package/templates/webapp/csr-bff/db/rls.json +13 -0
- package/templates/webapp/csr-bff/db/schema.sql +49 -0
- package/templates/webapp/csr-bff/frontend/dist/admin.html +101 -0
- package/templates/webapp/csr-bff/frontend/dist/article.html +44 -0
- package/templates/webapp/csr-bff/frontend/dist/blog.js +206 -0
- package/templates/webapp/csr-bff/frontend/dist/blogger.html +53 -0
- package/templates/webapp/csr-bff/frontend/dist/index.html +70 -0
- package/templates/webapp/csr-bff/frontend/dist/login.html +56 -0
- package/templates/webapp/csr-bff/frontend/dist/profile.html +59 -0
- package/templates/webapp/csr-bff/frontend/dist/signup.html +78 -0
- package/templates/webapp/csr-bff/frontend/dist/style.css +27 -0
- package/templates/webapp/manifest.json +419 -0
- package/templates/webapp/mpa/about.html +29 -0
- package/templates/webapp/mpa/app.js +42 -0
- package/templates/webapp/mpa/index.html +42 -0
- package/templates/webapp/mpa/index.ts +60 -0
- package/templates/webapp/mpa/style.css +42 -0
- package/templates/webapp/react-vite/frontend/index.html +12 -0
- package/templates/webapp/react-vite/frontend/package-lock.json +1826 -0
- package/templates/webapp/react-vite/frontend/package.json +23 -0
- package/templates/webapp/react-vite/frontend/src/App.tsx +34 -0
- package/templates/webapp/react-vite/frontend/src/main.tsx +13 -0
- package/templates/webapp/react-vite/frontend/src/style.css +46 -0
- package/templates/webapp/react-vite/frontend/tsconfig.json +20 -0
- package/templates/webapp/react-vite/frontend/vite.config.ts +10 -0
- package/templates/webapp/react-vite-hono/backend/index.ts +38 -0
- package/templates/webapp/react-vite-hono/frontend/index.html +12 -0
- package/templates/webapp/react-vite-hono/frontend/package-lock.json +1826 -0
- package/templates/webapp/react-vite-hono/frontend/package.json +23 -0
- package/templates/webapp/react-vite-hono/frontend/src/App.tsx +47 -0
- package/templates/webapp/react-vite-hono/frontend/src/main.tsx +13 -0
- package/templates/webapp/react-vite-hono/frontend/src/style.css +66 -0
- package/templates/webapp/react-vite-hono/frontend/tsconfig.json +20 -0
- package/templates/webapp/react-vite-hono/frontend/vite.config.ts +10 -0
- package/templates/webapp/spa/index.ts +119 -0
- package/templates/webapp/ssr-auth/backend/index.ts +133 -0
- package/templates/webapp/ssr-auth/frontend/dist/index.html +15 -0
- package/templates/webapp/ssr-blog/README.md +111 -0
- package/templates/webapp/ssr-blog/backend/index.ts +764 -0
- package/templates/webapp/ssr-blog/db/rls.json +13 -0
- package/templates/webapp/ssr-blog/db/schema.sql +51 -0
- package/templates/webapp/ssr-blog/frontend/dist/index.html +39 -0
- package/templates/webapp/ssr-frameworks/README.md +64 -0
- package/templates/webapp/ssr-frameworks/backend/index.ts +241 -0
- package/templates/webapp/ssr-frameworks/frontend/dist/index.html +31 -0
- package/templates/webapp/ssr-hono/backend/index.ts +111 -0
- package/templates/webapp/ssr-hono/frontend/dist/index.html +12 -0
- package/templates/webapp/ssr-shop/README.md +80 -0
- package/templates/webapp/ssr-shop/backend/index.ts +491 -0
- package/templates/webapp/ssr-shop/db/rls.json +16 -0
- package/templates/webapp/ssr-shop/db/schema.sql +90 -0
- package/templates/webapp/ssr-shop/frontend/dist/index.html +12 -0
- package/templates/webapp/vanilla-static/frontend/dist/app.js +6 -0
- package/templates/webapp/vanilla-static/frontend/dist/index.html +19 -0
- package/templates/webapp/vanilla-static/frontend/dist/style.css +43 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# {{APP_NAME}} — 框架 SSR 电商参考模板(React 18 + Hono)
|
|
2
|
+
|
|
3
|
+
**形态**:目录 / 详情 / 搜索 = **全量 SSR**(worker 服务端经内网基址取 MySQL 后 `renderToString` 直出整页);购物车 / 结算 / 订单 / 注册登录 / 管理台 = **SSR shell + 浏览器 hydrate**。
|
|
4
|
+
**会话模型**:`browser-side` —— 浏览器持 GoTrue token(localStorage)并**直连** `/rest/v1`、`/auth/v1`;worker 不碰用户凭据。下单门禁落在 **RLS 数据层**(`orders.buyer_email = {{jwt.email}}`),管理员白名单看全量。
|
|
5
|
+
|
|
6
|
+
> 为什么不用 `proxy-injected`:本模板的写操作由浏览器直接带 `Authorization` 打 REST,worker 只做公开数据的服务端渲染,因此不需要平台注入身份。若你要"SSR 首屏就带登录态",参考 `mpa-blog-ssr`(`session: proxy-injected`)。
|
|
7
|
+
|
|
8
|
+
## 目录
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
{{APP_NAME}}/
|
|
12
|
+
├── app.json # schemaVersion 2 webapp manifest(name 必须 == mount appName)
|
|
13
|
+
├── backend/index.ts # 单文件自包含 worker(Hono + React renderToString)
|
|
14
|
+
├── frontend/dist/index.html # 静态入口壳,meta refresh 到 ./shop
|
|
15
|
+
├── db/schema.sql # 幂等数据层:DDL + RLS + 10 件种子商品
|
|
16
|
+
└── README.md
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## 部署前必须做的一件事:填 DEPLOY CONFIG
|
|
20
|
+
|
|
21
|
+
`backend/index.ts` 顶部有两个常量,**默认为空**;留空时 worker 会对所有请求返回 503 并说明原因(刻意设计:绝不回落到任何真实实例,避免静默读写别人的数据)。
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
const INTERNAL_BASE = ""; // 目标实例的内网基址,如 http://pxsp-<id>.pxbaas.pre.rds.aliyuncs.com:8000
|
|
25
|
+
const ANON_KEY = ""; // 单租户=实例 anon key;多租户=该租户 anon key
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
- `INTERNAL_BASE` 是**内网**地址:把公网 host 里的 `http.` / `https.` 中缀去掉(等价于平台自 MR #30497903 起注入的 `SUPABASE_INTERNAL_URL`,也可直接读该 env)。公网 VIP 在沙箱内因 **egress 过滤**(非 hairpin)不可达。
|
|
29
|
+
- 多租户实例必须填**该租户**的 anon key(租户 key 自带 `tenant_id` claim,REST/Auth 据此路由,无需 `X-Tenant-Id`)。填错租户 = 读到别的租户数据。
|
|
30
|
+
- `service_role` key **严禁**出现在 worker 里(`verify_jwt=false` 的公开函数任何人可读源码)。
|
|
31
|
+
|
|
32
|
+
## 数据层
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
sed 's/__SCHEMA__/public/g' db/schema.sql # 单租户
|
|
36
|
+
sed 's/__SCHEMA__/tenant_acme/g' db/schema.sql # 多租户(租户库名)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
逐条执行(单租户可 `pxbaas db query`;多租户走 `POST {endpoint}/pg/query` + **全局** service key)。
|
|
40
|
+
⚠️ RLS 元数据永远写**中心库** `supabase._mysqlrest_rls_*`,且 `table_schema` 填上面的 `__SCHEMA__`;写进租户库里的同名表**没有任何服务读取**,写错会静默失效(等价于关闭 RLS)。
|
|
41
|
+
|
|
42
|
+
管理员账号走 GoTrue(不是 SQL):
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
curl -X POST {endpoint}/auth/v1/signup -H "apikey: $ANON" -H 'Content-Type: application/json' \
|
|
46
|
+
-d '{"email":"admin@shop.demo","password":"admin-2026"}'
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## 部署(mount-id 方式)
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pxbaas app deploy {{APP_NAME}} --dir . # appName 必须与 app.json 的 name 一致
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
若走 Management API(curl),注意三件事:静态/后端文件要用**显式嵌套 filename**(`filename=frontend/dist/index.html`、`filename=backend/index.ts`),否则路径被压平导致 publish 报缺文件;`app.json` 的 `name` 必须等于 mount 的 `appName`,否则 `409 APP_MANIFEST_INVALID`;publish 用 `expectedGeneration` 做 CAS。
|
|
56
|
+
|
|
57
|
+
### 冷启动与 SSR 探针
|
|
58
|
+
|
|
59
|
+
manifest 声明了 `ssrProbePath: "/shop"`,publish 前平台会实探一次。本模板 import 了 `npm:react@18.3.1` + `npm:react-dom@18.3.1/server` + `npm:hono@4.6.12`,**冷启动首次请求可能数十秒**,探针可能报 `409 APP_SSR_PROBE_FAILED`(先 timeout 后 http_500)。这不是代码问题:
|
|
60
|
+
|
|
61
|
+
1. 先把同一 worker 以 legacy 函数部署到**同租户**(`functions deploy`),反复 GET 到 200(首次可能 ~60s)——沙箱按租户复用,模块解析结果会缓存;
|
|
62
|
+
2. 再 `publish`,失败则等 10~20s 重试 3~5 次。
|
|
63
|
+
|
|
64
|
+
## 验收清单
|
|
65
|
+
|
|
66
|
+
| # | 检查 | 期望 |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| 1 | `GET /apps/{mount}/` | 200 静态壳,meta refresh 到 `./shop` |
|
|
69
|
+
| 2 | `GET /apps/{mount}/shop` | 200,**10 件商品服务端渲染**,含 `data-ssr="v1"` |
|
|
70
|
+
| 3 | `GET /apps/{mount}/api/shop` | 200 同样出数据(双形态路由) |
|
|
71
|
+
| 4 | `GET /apps/{mount}/product/1` | 200,商品名 + 由 `price_cents` 换算的价格 |
|
|
72
|
+
| 5 | `GET /apps/{mount}/shop?q=耳机` | 只命中匹配商品(搜索在服务端生效) |
|
|
73
|
+
| 6 | anon 读 `products` / `orders` | 10 行 / `[]` |
|
|
74
|
+
| 7 | 买家写 `orders`(自己 / 他人 `buyer_email`) | 201 / **403 PGRST302** |
|
|
75
|
+
| 8 | 买家读 `orders` / 管理员读 `orders` | 只见自己 / 见全量 |
|
|
76
|
+
|
|
77
|
+
## 已知平台行为(非本模板缺陷)
|
|
78
|
+
|
|
79
|
+
- `capabilities.ssrLibs` 可能为空却仍能运行 React(该字段只反映构建期白名单,低报,见 Aone #87704012);能否使用以实测为准。
|
|
80
|
+
- spec-lint 会按 `ssrLibs` 白名单校验 `npm:` 导入:实例报空时 `pxbaas app deploy` 会以 `no-remote-import` 阻断本模板,需在实例侧补齐 `SSR_LIBS_LIST` 后再部署(不要长期依赖 `--skip-spec-check`)。
|
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
// Shop Demo v2 — React SSR 电商 webapp v5(买家注册/登录 + 下单门禁 + 固定密码管理员)
|
|
2
|
+
//
|
|
3
|
+
// 框架: React 18 renderToString + Hono 4.6.12(平台锁版本缓存)
|
|
4
|
+
// 形态: 目录/详情/搜索 = 全量 SSR(服务端经 DX-12 派生内网基址取 MySQL);
|
|
5
|
+
// 购物车/结算/订单/注册/登录/管理 = SSR shell + 浏览器 hydrate;
|
|
6
|
+
// 鉴权: 浏览器直连 GoTrue(/apps 入口按平台契约剥离用户凭据, 会话 SSR 不成立, af565f8b);
|
|
7
|
+
// 下单门禁在 RLS 数据层: orders/order_items 仅 authenticated 且 buyer_email={{jwt.email}};
|
|
8
|
+
// 管理员固定账号 admin@shop.demo / admin-2026, RLS 邮箱白名单看全量订单。
|
|
9
|
+
// 契约: 单文件自包含(D16) · marker data-ssr="v1"(DX-2) · 双形态路由(D20) · style 对象 ·
|
|
10
|
+
// 客户端 JS 禁反引号/${} · innerHTML 插值过 esc() · anon/public 兜底常量(D18 家族)。
|
|
11
|
+
|
|
12
|
+
import { Hono } from "npm:hono@4.6.12";
|
|
13
|
+
import React from "npm:react@18.3.1";
|
|
14
|
+
import { renderToString } from "npm:react-dom@18.3.1/server";
|
|
15
|
+
|
|
16
|
+
const h = React.createElement;
|
|
17
|
+
|
|
18
|
+
// ─── DEPLOY CONFIG(平台注入优先;缺失则 worker 明确返回 503,绝不回落到任何真实实例)───
|
|
19
|
+
// env 契约(MR #30497903 起):平台按三键白名单向 App candidate 注入 SUPABASE_INTERNAL_URL /
|
|
20
|
+
// SUPABASE_ANON_KEY / SUPABASE_URL,spec-lint 的 deno-env-not-allowed 放行这三键。本模板直读
|
|
21
|
+
// SUPABASE_INTERNAL_URL(数据面唯一可达基址)与 SUPABASE_ANON_KEY(数据面凭据),无需手填明文 key。
|
|
22
|
+
// 若目标实例 sandbox 镜像尚未含该 MR,两值为空 → configError 明确 503,绝不静默连到别的实例/租户。
|
|
23
|
+
// INTERNAL_BASE —— 平台注入的内网基址(把公网 host 的 http./https. 中缀去掉),沙箱内可达;
|
|
24
|
+
// 公网 VIP 因 egress 过滤不可达(非 hairpin),切勿改填公网地址。
|
|
25
|
+
// ANON_KEY —— 单租户=实例 anon key;多租户=**该租户** anon key(自带 tenant_id claim,
|
|
26
|
+
// REST/Auth 据此路由,无需 X-Tenant-Id)。
|
|
27
|
+
// anon key 是公开值(随页面下发给每个浏览器),不是机密;service_role key 严禁出现在 worker 里。
|
|
28
|
+
const INTERNAL_BASE = Deno.env.get("SUPABASE_INTERNAL_URL") ?? "";
|
|
29
|
+
const ANON_KEY = Deno.env.get("SUPABASE_ANON_KEY") ?? "";
|
|
30
|
+
|
|
31
|
+
function configError(): string | null {
|
|
32
|
+
if (!INTERNAL_BASE) return "SUPABASE_INTERNAL_URL 未注入:目标实例 sandbox 镜像早于 MR #30497903,需平台注入内网基址后本 SSR 才能取数。";
|
|
33
|
+
if (!ANON_KEY) return "SUPABASE_ANON_KEY 未注入:目标实例 sandbox 镜像早于 MR #30497903,需平台注入 anon key。";
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
function anonKey(): string {
|
|
37
|
+
return ANON_KEY;
|
|
38
|
+
}
|
|
39
|
+
// 服务端取数唯一合法通道:内网基址(见上方 DEPLOY CONFIG)。
|
|
40
|
+
function internalBase(): string {
|
|
41
|
+
return INTERNAL_BASE.replace(/\/+$/, "");
|
|
42
|
+
}
|
|
43
|
+
function configErrorDoc(msg: string): string {
|
|
44
|
+
return `<!DOCTYPE html><html lang="zh"><head><meta charset="utf-8">
|
|
45
|
+
<meta name="viewport" content="width=device-width, initial-scale=1"><title>部署配置缺失</title>
|
|
46
|
+
<style>body{margin:0;padding:48px 20px;font:15px/1.7 system-ui,-apple-system,sans-serif;background:#0b1020;color:#e8eaf2}
|
|
47
|
+
.card{max-width:660px;margin:0 auto;padding:24px 26px;border-radius:14px;background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.12)}
|
|
48
|
+
code{background:rgba(0,0,0,.35);padding:2px 6px;border-radius:6px}</style></head>
|
|
49
|
+
<body><div class="card" id="__ssr" data-ssr="v1"><h1 style="margin:0 0 8px;font-size:20px">部署配置缺失</h1>
|
|
50
|
+
<p style="color:#9aa3b8;margin:0 0 10px">${msg}</p>
|
|
51
|
+
<p style="color:#9aa3b8;margin:0">请在 <code>backend/index.ts</code> 顶部的 DEPLOY CONFIG 常量块填入目标实例的内网基址与 anon key,然后重新部署。</p>
|
|
52
|
+
</div></body></html>`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface Product {
|
|
56
|
+
id: number;
|
|
57
|
+
name: string;
|
|
58
|
+
description: string;
|
|
59
|
+
price_cents: number;
|
|
60
|
+
stock: number;
|
|
61
|
+
emoji: string;
|
|
62
|
+
category: string;
|
|
63
|
+
created_at?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function fetchProducts(): Promise<Product[]> {
|
|
67
|
+
try {
|
|
68
|
+
const r = await fetch(internalBase() + "/rest/v1/products?select=id,name,description,price_cents,stock,emoji,category&order=id.asc", {
|
|
69
|
+
headers: { apikey: anonKey() },
|
|
70
|
+
});
|
|
71
|
+
if (!r.ok) return [];
|
|
72
|
+
return (await r.json()) as Product[];
|
|
73
|
+
} catch {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function fetchProduct(id: string): Promise<Product | null> {
|
|
78
|
+
try {
|
|
79
|
+
const r = await fetch(internalBase() + "/rest/v1/products?select=*&id=eq." + encodeURIComponent(id), {
|
|
80
|
+
headers: { apikey: anonKey() },
|
|
81
|
+
});
|
|
82
|
+
if (!r.ok) return null;
|
|
83
|
+
const rows = (await r.json()) as Product[];
|
|
84
|
+
return rows[0] || null;
|
|
85
|
+
} catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function money(c: number): string {
|
|
91
|
+
return "¥" + (Number(c) / 100).toFixed(2);
|
|
92
|
+
}
|
|
93
|
+
const HOT_WORDS = ["键盘", "耳机", "音箱", "手表", "充电", "收纳", "路由", "NAS", "显示器", "电竞椅"];
|
|
94
|
+
|
|
95
|
+
const CSS = `
|
|
96
|
+
:root { color-scheme: dark;
|
|
97
|
+
--bg1:#0b1020; --bg2:#171338; --card:rgba(255,255,255,.045); --line:rgba(255,255,255,.10);
|
|
98
|
+
--txt:#e8eaf2; --muted:#9aa3b8; --acc1:#6366f1; --acc2:#ec4899; --gold:#f6c453; }
|
|
99
|
+
* { box-sizing: border-box; }
|
|
100
|
+
html,body { margin:0; }
|
|
101
|
+
body { font-family:-apple-system,"PingFang SC","Microsoft YaHei",sans-serif; color:var(--txt);
|
|
102
|
+
min-height:100vh; padding:0 18px 60px; max-width:1040px; margin:0 auto;
|
|
103
|
+
background:
|
|
104
|
+
radial-gradient(1100px 520px at 85% -10%, rgba(99,102,241,.30), transparent 60%),
|
|
105
|
+
radial-gradient(900px 480px at 8% 108%, rgba(236,72,153,.20), transparent 60%),
|
|
106
|
+
linear-gradient(160deg, var(--bg1), var(--bg2) 55%, var(--bg1));
|
|
107
|
+
background-attachment: fixed; }
|
|
108
|
+
header { position:sticky; top:0; z-index:20; display:flex; align-items:center; gap:18px;
|
|
109
|
+
padding:12px 4px; backdrop-filter: blur(14px);
|
|
110
|
+
background: rgba(11,16,32,.55); border-bottom:1px solid var(--line); }
|
|
111
|
+
.logo { font-size:21px; font-weight:800;
|
|
112
|
+
background: linear-gradient(90deg, var(--acc1), var(--acc2)); -webkit-background-clip:text; background-clip:text; color:transparent; }
|
|
113
|
+
nav { margin-left:auto; }
|
|
114
|
+
nav a { color:#aeb9d6; text-decoration:none; margin-left:16px; font-size:14px; }
|
|
115
|
+
nav a:hover { color:#fff; }
|
|
116
|
+
.badge { background:linear-gradient(90deg,var(--acc1),var(--acc2)); border-radius:999px; padding:1px 8px; font-size:12px; color:#fff; }
|
|
117
|
+
.muted { color:var(--muted); font-size:13px; }
|
|
118
|
+
.topbar { margin:16px 0 4px; }
|
|
119
|
+
.searchbar { display:flex; align-items:stretch; border:2px solid var(--acc1); border-radius:999px;
|
|
120
|
+
overflow:hidden; background:rgba(255,255,255,.06); }
|
|
121
|
+
.searchbar select { background:transparent; color:var(--txt); border:0; outline:none; font:inherit;
|
|
122
|
+
padding:0 14px; border-right:1px solid var(--line); cursor:pointer; }
|
|
123
|
+
.searchbar select option { color:#111; }
|
|
124
|
+
.searchbar .q { flex:1; background:transparent; border:0; outline:none; color:var(--txt);
|
|
125
|
+
padding:13px 18px; font:inherit; margin:0; }
|
|
126
|
+
.searchbar .q::placeholder { color:var(--muted); }
|
|
127
|
+
.searchbar .sbtn { border:0; cursor:pointer; color:#fff; font-size:15px; font-weight:600;
|
|
128
|
+
padding:0 34px; background:linear-gradient(90deg,var(--acc1),var(--acc2)); }
|
|
129
|
+
.hotwords { display:flex; gap:16px; flex-wrap:wrap; margin:8px 4px 0; font-size:13px; }
|
|
130
|
+
.hotwords a { color:#ff8fa3; text-decoration:none; cursor:pointer; }
|
|
131
|
+
.hotwords a:hover { color:#fff; text-decoration:underline; }
|
|
132
|
+
.hero { margin:14px 0 6px; padding:24px 22px; border-radius:18px; border:1px solid var(--line);
|
|
133
|
+
background: linear-gradient(120deg, rgba(99,102,241,.22), rgba(236,72,153,.14)); }
|
|
134
|
+
.hero h2 { margin:0 0 6px; font-size:24px; }
|
|
135
|
+
.toolbar { display:flex; gap:10px; flex-wrap:wrap; align-items:center; margin:12px 0 4px; }
|
|
136
|
+
.chip { border:1px solid var(--line); background:var(--card); color:var(--muted); border-radius:999px;
|
|
137
|
+
padding:6px 14px; font-size:13px; text-decoration:none; }
|
|
138
|
+
.chip.on { color:#fff; border-color:transparent; background:linear-gradient(90deg,var(--acc1),var(--acc2)); }
|
|
139
|
+
.grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(210px,1fr)); gap:16px; margin-top:16px; }
|
|
140
|
+
.card { background:var(--card); border:1px solid var(--line); border-radius:16px; padding:16px;
|
|
141
|
+
backdrop-filter: blur(8px); transition: transform .18s ease, box-shadow .18s ease; }
|
|
142
|
+
.card:hover { transform: translateY(-4px); box-shadow: 0 12px 30px rgba(0,0,0,.35); }
|
|
143
|
+
.thumb { font-size:46px; text-align:center; padding:16px 0; border-radius:14px;
|
|
144
|
+
background: radial-gradient(circle at 50% 40%, rgba(99,102,241,.28), rgba(255,255,255,.03) 70%); }
|
|
145
|
+
.tag { display:inline-block; font-size:11px; color:#c7d2fe; background:rgba(99,102,241,.18);
|
|
146
|
+
border:1px solid rgba(99,102,241,.4); border-radius:999px; padding:2px 9px; margin-bottom:6px; }
|
|
147
|
+
.pname { font-size:15px; font-weight:600; color:#eef1ff; text-decoration:none; }
|
|
148
|
+
.pname:hover { color:#fff; text-decoration:underline; }
|
|
149
|
+
.price { color:var(--gold); font-weight:700; }
|
|
150
|
+
button, a.btn { border:0; border-radius:10px; padding:9px 16px; font:inherit; cursor:pointer; color:#fff;
|
|
151
|
+
background:linear-gradient(90deg,var(--acc1),var(--acc2)); text-decoration:none; display:inline-block; }
|
|
152
|
+
button.ghost, a.btn.ghost { background:rgba(255,255,255,.10); }
|
|
153
|
+
button.danger { background:linear-gradient(90deg,#ef4444,#b3261e); }
|
|
154
|
+
button.mini { padding:4px 10px; border-radius:8px; }
|
|
155
|
+
input { width:100%; background:var(--card); color:var(--txt); border:1px solid var(--line);
|
|
156
|
+
border-radius:10px; padding:10px 12px; font:inherit; margin:6px 0; outline:none; }
|
|
157
|
+
table { width:100%; border-collapse:collapse; margin-top:10px; }
|
|
158
|
+
td, th { padding:10px 8px; border-bottom:1px solid var(--line); text-align:left; font-size:14px; }
|
|
159
|
+
th { color:var(--muted); font-weight:600; }
|
|
160
|
+
.row { display:flex; gap:10px; align-items:center; flex-wrap:wrap; }
|
|
161
|
+
footer { margin-top:40px; padding-top:16px; border-top:1px solid var(--line); color:var(--muted); font-size:12px; text-align:center; }
|
|
162
|
+
.empty { padding:40px 0; text-align:center; color:var(--muted); }
|
|
163
|
+
.authbox { max-width:400px; margin:24px auto; }
|
|
164
|
+
.toast { position:fixed; left:50%; bottom:36px; transform:translateX(-50%) translateY(20px); z-index:60;
|
|
165
|
+
background:linear-gradient(90deg,var(--acc1),var(--acc2)); color:#fff; border-radius:999px;
|
|
166
|
+
padding:10px 26px; font-size:14px; font-weight:600; opacity:0; pointer-events:none;
|
|
167
|
+
transition: opacity .18s ease, transform .18s ease; box-shadow:0 10px 30px rgba(0,0,0,.4); }
|
|
168
|
+
.toast.show { opacity:1; transform:translateX(-50%) translateY(0); }
|
|
169
|
+
.badge.pulse { animation:bp .5s ease; }
|
|
170
|
+
@keyframes bp { 0%{transform:scale(1)} 40%{transform:scale(1.5)} 100%{transform:scale(1)} }
|
|
171
|
+
`;
|
|
172
|
+
|
|
173
|
+
// ---------- React 组件(服务端渲染) ----------
|
|
174
|
+
// base: 相对链接前缀(/product/:id 为二级路径须用 ../,其余 ./)
|
|
175
|
+
function Header(props: { base: string }) {
|
|
176
|
+
const b = props.base;
|
|
177
|
+
return h("header", null,
|
|
178
|
+
h("span", { className: "logo" }, "🛍️ Supabase Shop · React SSR"),
|
|
179
|
+
h("nav", null,
|
|
180
|
+
h("a", { href: b + "shop" }, "商品"),
|
|
181
|
+
h("a", { href: b + "cart" }, "购物车 ", h("span", { className: "badge", id: "cartcount" }, "0")),
|
|
182
|
+
h("a", { href: b + "orders" }, "我的订单"),
|
|
183
|
+
h("span", { id: "autharea", className: "muted" })));
|
|
184
|
+
}
|
|
185
|
+
function SearchBar(props: { q: string; cat: string; cats: string[] }) {
|
|
186
|
+
return h("div", { className: "topbar" },
|
|
187
|
+
h("form", { className: "searchbar", method: "GET", action: "./shop" },
|
|
188
|
+
h("select", { name: "cat" },
|
|
189
|
+
props.cats.map((c) => h("option", { value: c, selected: c === props.cat }, c === "all" ? "全部" : c))),
|
|
190
|
+
h("input", { className: "q", name: "q", placeholder: "搜索商品名 / 描述(服务端渲染结果)…", value: props.q }),
|
|
191
|
+
h("button", { className: "sbtn", type: "submit" }, "搜索")),
|
|
192
|
+
h("div", { className: "hotwords" },
|
|
193
|
+
HOT_WORDS.map((w) => h("a", { href: "./shop?q=" + encodeURIComponent(w) }, w))));
|
|
194
|
+
}
|
|
195
|
+
function ProductCard(p: Product) {
|
|
196
|
+
return h("div", { className: "card" },
|
|
197
|
+
h("div", { className: "thumb" }, p.emoji || "📦"),
|
|
198
|
+
p.category ? h("span", { className: "tag" }, p.category) : null,
|
|
199
|
+
h("div", null, h("a", { className: "pname", href: "./product/" + p.id }, p.name)),
|
|
200
|
+
h("div", { className: "muted", style: { margin: "4px 0 8px" } }, p.description || ""),
|
|
201
|
+
h("div", { className: "row", style: { justifyContent: "space-between" } },
|
|
202
|
+
h("span", { className: "price" }, money(p.price_cents)),
|
|
203
|
+
h("button", { className: "mini", type: "button", "data-add": String(p.id) }, "加购")),
|
|
204
|
+
h("div", { className: "muted", style: { marginTop: "6px" } }, "库存 " + p.stock));
|
|
205
|
+
}
|
|
206
|
+
function CatalogView(props: { products: Product[]; q: string; cat: string; cats: string[]; note: string }) {
|
|
207
|
+
const list = props.products.filter((p) => {
|
|
208
|
+
const okCat = props.cat === "all" || p.category === props.cat;
|
|
209
|
+
const okQ = !props.q || (p.name || "").indexOf(props.q) >= 0 || (p.description || "").indexOf(props.q) >= 0;
|
|
210
|
+
return okCat && okQ;
|
|
211
|
+
});
|
|
212
|
+
return h(React.Fragment, null,
|
|
213
|
+
h("div", { className: "hero" },
|
|
214
|
+
h("h2", null, "发现好物 · 服务端渲染直达"),
|
|
215
|
+
h("div", { className: "muted" }, "共 " + props.products.length + " 件商品 · React renderToString · 数据经内网基址服务端取自 MySQL · " + props.note)),
|
|
216
|
+
h("div", { className: "toolbar" },
|
|
217
|
+
props.cats.map((c) => h("a", {
|
|
218
|
+
className: "chip" + (c === props.cat ? " on" : ""),
|
|
219
|
+
href: "./shop?cat=" + encodeURIComponent(c) + (props.q ? "&q=" + encodeURIComponent(props.q) : ""),
|
|
220
|
+
}, c === "all" ? "全部" : c)),
|
|
221
|
+
props.q ? h("span", { className: "muted" }, "搜索:“" + props.q + "” · " + list.length + " 件") : null),
|
|
222
|
+
list.length
|
|
223
|
+
? h("div", { className: "grid" }, list.map((p) => h(ProductCard, { key: p.id, ...p })))
|
|
224
|
+
: h("div", { className: "empty" }, "没有匹配的商品"));
|
|
225
|
+
}
|
|
226
|
+
function ProductView(props: { p: Product | null; base: string }) {
|
|
227
|
+
const p = props.p;
|
|
228
|
+
if (!p) return h("div", { className: "empty" }, "商品不存在");
|
|
229
|
+
return h("div", { className: "card", style: { maxWidth: "520px", margin: "20px auto" } },
|
|
230
|
+
h("div", { className: "thumb" }, p.emoji || "📦"),
|
|
231
|
+
p.category ? h("span", { className: "tag" }, p.category) : null,
|
|
232
|
+
h("h2", { style: { margin: "6px 0" } }, p.name),
|
|
233
|
+
h("p", { className: "muted" }, p.description || ""),
|
|
234
|
+
h("div", { className: "price", style: { fontSize: "24px" } }, money(p.price_cents)),
|
|
235
|
+
h("div", { className: "muted" }, "库存 " + p.stock + " · 上架 " + String(p.created_at || "")),
|
|
236
|
+
h("div", { className: "row", style: { marginTop: "14px" } },
|
|
237
|
+
h("button", { type: "button", "data-add": String(p.id) }, "加入购物车"),
|
|
238
|
+
h("a", { className: "btn ghost", href: props.base + "shop" }, "返回")));
|
|
239
|
+
}
|
|
240
|
+
function ClientShell(props: { title: string; hint: string }) {
|
|
241
|
+
return h("div", { className: "card authbox", style: { maxWidth: "640px", margin: "20px auto" } },
|
|
242
|
+
h("h2", { style: { marginTop: 0 } }, props.title),
|
|
243
|
+
h("div", { className: "muted" }, props.hint),
|
|
244
|
+
h("div", { id: "client-root" }, h("div", { className: "empty" }, "hydrate 加载中…")));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ---------- 客户端脚本(禁反引号/${};innerHTML 插值过 esc) ----------
|
|
248
|
+
function clientScript(anon: string): string {
|
|
249
|
+
const js = `
|
|
250
|
+
var ANON = "__ANON__";
|
|
251
|
+
var REST = location.origin + '/rest/v1';
|
|
252
|
+
var AUTH = location.origin + '/auth/v1';
|
|
253
|
+
var CART_KEY = 'shop_cart_v2';
|
|
254
|
+
var AUTH_KEY = 'shop_auth_v1';
|
|
255
|
+
var ADMIN_EMAIL = 'admin@shop.demo';
|
|
256
|
+
function el(id){return document.getElementById(id);}
|
|
257
|
+
function esc(s){return String(s).replace(/[&<>"']/g,function(c){return{'&':'&','<':'<','>':'>','"':'"',"'":'''}[c];});}
|
|
258
|
+
function money(c){return '¥'+(Number(c)/100).toFixed(2);}
|
|
259
|
+
function hdrs(){return {'apikey':ANON,'Authorization':'Bearer '+ANON};}
|
|
260
|
+
function getJSON(u,h){return fetch(u,{headers:h||hdrs()}).then(function(r){return r.json();});}
|
|
261
|
+
function postJSON(u,b,h){return fetch(u,{method:'POST',headers:Object.assign(h||hdrs(),{'Content-Type':'application/json','Prefer':'return=representation'}),body:JSON.stringify(b)}).then(function(r){return r.json().then(function(j){return {ok:r.ok,status:r.status,body:j};});});}
|
|
262
|
+
function getCart(){try{return JSON.parse(localStorage.getItem(CART_KEY)||'{}');}catch(e){return {};}}
|
|
263
|
+
function setCart(c){localStorage.setItem(CART_KEY,JSON.stringify(c));refreshBadge();}
|
|
264
|
+
function cartCount(){var c=getCart(),n=0;for(var k in c)n+=c[k];return n;}
|
|
265
|
+
function refreshBadge(){var b=el('cartcount');if(b)b.textContent=cartCount();}
|
|
266
|
+
var toastTimer=null;
|
|
267
|
+
function toast(msg){var t=el('shoptoast');if(!t){t=document.createElement('div');t.id='shoptoast';t.className='toast';document.body.appendChild(t);}
|
|
268
|
+
t.textContent=msg;t.classList.add('show');if(toastTimer)clearTimeout(toastTimer);toastTimer=setTimeout(function(){t.classList.remove('show');},1200);}
|
|
269
|
+
function addToCart(id){var c=getCart();c[id]=(c[id]||0)+1;setCart(c);
|
|
270
|
+
var b=el('cartcount');if(b){b.classList.remove('pulse');void b.offsetWidth;b.classList.add('pulse');}
|
|
271
|
+
toast('✓ 已加入购物车 · 共 ' + cartCount() + ' 件');}
|
|
272
|
+
document.addEventListener('click',function(e){var t=e.target&&e.target.closest?e.target.closest('[data-add]'):null;if(t){addToCart(Number(t.getAttribute('data-add')));}});
|
|
273
|
+
function setQty(id,q){var c=getCart();if(q<=0)delete c[id];else c[id]=q;setCart(c);if(location.pathname.indexOf('/cart')>=0)viewCart();}
|
|
274
|
+
function getAuth(){try{return JSON.parse(localStorage.getItem(AUTH_KEY)||'null');}catch(e){return null;}}
|
|
275
|
+
function setAuth(a){if(a){localStorage.setItem(AUTH_KEY,JSON.stringify(a));}else{localStorage.removeItem(AUTH_KEY);}refreshAuthUI();}
|
|
276
|
+
function authHdr(){var h=hdrs();var a=getAuth();if(a&&a.token){h['Authorization']='Bearer '+a.token;}return h;}
|
|
277
|
+
function refreshAuthUI(){var box=el('autharea');if(!box)return;var a=getAuth();
|
|
278
|
+
if(a){box.innerHTML=' · <span style="color:#c7d2fe">'+esc(a.email)+'</span> <a href="#" onclick="doLogout();return false;" style="color:#aeb9d6">退出</a>'+(a.email===ADMIN_EMAIL?' <a href="./admin" style="color:#f6c453">管理台</a>':'');}
|
|
279
|
+
else{box.innerHTML=' · <a href="./login" style="color:#aeb9d6">登录</a> <a href="./signup" style="color:#aeb9d6">注册</a>';} }
|
|
280
|
+
function doLogout(){setAuth(null);location.href='./shop';}
|
|
281
|
+
function authCall(kind,email,pass){
|
|
282
|
+
var url = kind==='signup' ? AUTH+'/signup' : AUTH+'/token?grant_type=password';
|
|
283
|
+
return fetch(url,{method:'POST',headers:{'apikey':ANON,'Content-Type':'application/json'},
|
|
284
|
+
body:JSON.stringify({email:email,password:pass})})
|
|
285
|
+
.then(function(r){return r.json().then(function(j){return {ok:r.ok,status:r.status,body:j};});});
|
|
286
|
+
}
|
|
287
|
+
function viewLogin(){
|
|
288
|
+
var next=new URL(location.href).searchParams.get('next')||'./shop';
|
|
289
|
+
el('client-root').innerHTML='<input id="lemail" placeholder="邮箱"><input id="lpass" type="password" placeholder="密码">'+
|
|
290
|
+
'<div class="row" style="margin-top:10px"><button onclick="doLogin(\\''+esc(next)+'\\')">登录</button>'+
|
|
291
|
+
'<a class="btn ghost" href="./signup">注册买家</a></div><div class="muted" id="lmsg" style="margin-top:10px"></div>'+
|
|
292
|
+
'<div class="muted" style="margin-top:14px">管理员入口:<a href="./admin" style="color:#f6c453">./admin</a>(固定密码账号)</div>';
|
|
293
|
+
}
|
|
294
|
+
function doLogin(next){
|
|
295
|
+
var email=el('lemail').value.trim(),pass=el('lpass').value;
|
|
296
|
+
el('lmsg').textContent='登录中…';
|
|
297
|
+
authCall('login',email,pass).then(function(r){
|
|
298
|
+
if(r.ok&&r.body.access_token){setAuth({token:r.body.access_token,email:email});location.href=next;}
|
|
299
|
+
else{el('lmsg').textContent='登录失败: '+(r.body&&r.body.error_description||r.body&&r.body.msg||r.status);}
|
|
300
|
+
}).catch(function(e){el('lmsg').textContent='登录失败: '+e;});
|
|
301
|
+
}
|
|
302
|
+
function viewSignup(){
|
|
303
|
+
el('client-root').innerHTML='<input id="semail" placeholder="邮箱(买家账号)"><input id="spass" type="password" placeholder="密码(≥6位)">'+
|
|
304
|
+
'<div class="row" style="margin-top:10px"><button onclick="doSignup()">注册并登录</button>'+
|
|
305
|
+
'<a class="btn ghost" href="./login">已有账号登录</a></div><div class="muted" id="smsg" style="margin-top:10px"></div>';
|
|
306
|
+
}
|
|
307
|
+
function doSignup(){
|
|
308
|
+
var email=el('semail').value.trim(),pass=el('spass').value;
|
|
309
|
+
el('smsg').textContent='注册中…';
|
|
310
|
+
authCall('signup',email,pass).then(function(r){
|
|
311
|
+
if(r.ok&&r.body.access_token){setAuth({token:r.body.access_token,email:email});location.href='./shop';}
|
|
312
|
+
else if(r.ok){el('smsg').textContent='注册成功(需确认邮件场景未启用时请直接登录)';setTimeout(function(){location.href='./login';},800);}
|
|
313
|
+
else{el('smsg').textContent='注册失败: '+(r.body&&r.body.msg||r.status);}
|
|
314
|
+
}).catch(function(e){el('smsg').textContent='注册失败: '+e;});
|
|
315
|
+
}
|
|
316
|
+
function viewCart(){
|
|
317
|
+
var c=getCart(),ids=Object.keys(c);
|
|
318
|
+
if(!ids.length){el('client-root').innerHTML='<div class="empty">购物车为空。<a href="./shop" style="color:#aeb9d6">去逛逛 →</a></div>';return;}
|
|
319
|
+
getJSON(REST+'/products?select=id,name,price_cents,emoji').then(function(all){
|
|
320
|
+
var byId={};all.forEach(function(p){byId[p.id]=p;});
|
|
321
|
+
var total=0;
|
|
322
|
+
var html='<table><tr><th>商品</th><th>单价</th><th>数量</th><th>小计</th><th></th></tr>';
|
|
323
|
+
ids.forEach(function(id){var p=byId[id];if(!p)return;var q=c[id];total+=p.price_cents*q;
|
|
324
|
+
html+='<tr><td>'+esc(p.emoji||'')+' '+esc(p.name)+'</td><td>'+money(p.price_cents)+'</td>'+
|
|
325
|
+
'<td><button class="mini ghost" onclick="setQty('+id+','+(q-1)+')">−</button> '+q+' <button class="mini ghost" onclick="setQty('+id+','+(q+1)+')">+</button></td>'+
|
|
326
|
+
'<td>'+money(p.price_cents*q)+'</td><td><button class="mini danger" onclick="setQty('+id+',0)">移除</button></td></tr>';});
|
|
327
|
+
html+='</table><div class="row" style="margin-top:16px;justify-content:space-between"><span class="price" style="font-size:20px">合计 '+money(total)+'</span><a class="btn" href="./checkout">去结算 →</a></div>';
|
|
328
|
+
el('client-root').innerHTML=html;});
|
|
329
|
+
}
|
|
330
|
+
function viewCheckout(){
|
|
331
|
+
var a=getAuth();
|
|
332
|
+
if(!a){el('client-root').innerHTML='<div class="empty">下单需先登录(RLS 数据层强制)。<br><br><a class="btn" href="./login?next=./checkout">去登录 →</a> <a class="btn ghost" href="./signup">注册买家</a></div>';return;}
|
|
333
|
+
var c=getCart();if(!Object.keys(c).length){el('client-root').innerHTML='<div class="empty">购物车为空,无法结算。</div>';return;}
|
|
334
|
+
el('client-root').innerHTML='<div class="muted">下单账号:'+esc(a.email)+'(订单绑定买家邮箱,RLS 强制)</div>'+
|
|
335
|
+
'<input id="cname" placeholder="收货人姓名 *"><input id="cemail" placeholder="邮箱(可选)"><input id="caddr" placeholder="收货地址(可选)">'+
|
|
336
|
+
'<div class="row" style="margin-top:12px"><button onclick="placeOrder()">提交订单</button><a class="btn ghost" href="./cart">返回购物车</a></div><div class="muted" id="cmsg" style="margin-top:10px"></div>';
|
|
337
|
+
}
|
|
338
|
+
function placeOrder(){
|
|
339
|
+
var a=getAuth();if(!a){el('cmsg').textContent='未登录,无法下单';return;}
|
|
340
|
+
var name=el('cname').value.trim();if(!name){el('cmsg').textContent='请填写姓名';return;}
|
|
341
|
+
var email=el('cemail').value.trim(),addr=el('caddr').value.trim();
|
|
342
|
+
var c=getCart(),ids=Object.keys(c);el('cmsg').textContent='提交中…';
|
|
343
|
+
getJSON(REST+'/products?select=id,name,price_cents').then(function(all){
|
|
344
|
+
var byId={};all.forEach(function(p){byId[p.id]=p;});
|
|
345
|
+
var total=0;ids.forEach(function(id){if(byId[id])total+=byId[id].price_cents*c[id];});
|
|
346
|
+
return postJSON(REST+'/orders',{customer_name:name,customer_email:email,customer_address:addr,buyer_email:a.email,total_cents:total,status:'paid'},authHdr()).then(function(r){
|
|
347
|
+
if(!r.ok){throw new Error((r.body&&(r.body.message||r.body.code))||r.status);}
|
|
348
|
+
var ord=r.body;var orderId=(ord&&ord[0])?ord[0].id:ord.id;
|
|
349
|
+
var chain=Promise.resolve();
|
|
350
|
+
ids.forEach(function(id){var p=byId[id];if(!p)return;chain=chain.then(function(){
|
|
351
|
+
return postJSON(REST+'/order_items',{order_id:orderId,product_id:Number(id),product_name:p.name,qty:c[id],unit_price_cents:p.price_cents,buyer_email:a.email},authHdr()).then(function(r2){if(!r2.ok)throw new Error(r2.status);});});});
|
|
352
|
+
return chain.then(function(){return orderId;});});
|
|
353
|
+
}).then(function(orderId){setCart({});el('cmsg').textContent='下单成功, 订单号 '+orderId+'(已写入 MySQL, 绑定 '+a.email+')';
|
|
354
|
+
setTimeout(function(){location.href='./orders';},700);})
|
|
355
|
+
.catch(function(e){el('cmsg').textContent='下单失败(未登录会被 RLS 拒绝): '+e;});
|
|
356
|
+
}
|
|
357
|
+
function renderOrders(orders,itemsByOrder,heading){
|
|
358
|
+
if(!orders.length){el('client-root').innerHTML='<div class="empty">暂无订单。<a href="./shop" style="color:#aeb9d6">去下单 →</a></div>';return;}
|
|
359
|
+
el('client-root').innerHTML='<h3 style="margin-top:0">'+esc(heading)+'</h3>'+orders.map(function(o){var items=itemsByOrder[o.id]||[];
|
|
360
|
+
return '<div class="card" style="margin:12px 0"><div class="row" style="justify-content:space-between"><b>订单 #'+o.id+'</b><span class="muted">'+esc(o.customer_name)+' · '+esc(o.buyer_email||'')+' · '+esc(String(o.created_at||''))+'</span><span class="price">'+money(o.total_cents)+'</span></div>'+
|
|
361
|
+
'<table><tr><th>商品</th><th>单价</th><th>数量</th><th>小计</th></tr>'+items.map(function(it){return '<tr><td>'+esc(it.product_name)+'</td><td>'+money(it.unit_price_cents)+'</td><td>'+it.qty+'</td><td>'+money(it.unit_price_cents*it.qty)+'</td></tr>';}).join('')+'</table></div>';}).join('');
|
|
362
|
+
}
|
|
363
|
+
function loadOrders(withItems,heading){
|
|
364
|
+
var H=authHdr();
|
|
365
|
+
getJSON(REST+'/orders?select=*&order=id.desc',H).then(function(orders){
|
|
366
|
+
if(!Array.isArray(orders)){el('client-root').innerHTML='<div class="empty">读取订单失败(需登录)</div>';return;}
|
|
367
|
+
var chain=Promise.resolve();var itemsByOrder={};
|
|
368
|
+
if(withItems){orders.forEach(function(o){chain=chain.then(function(){return getJSON(REST+'/order_items?select=*&order_id=eq.'+o.id,H).then(function(items){itemsByOrder[o.id]=items;});});});}
|
|
369
|
+
chain.then(function(){renderOrders(orders,itemsByOrder,heading);});
|
|
370
|
+
}).catch(function(e){el('client-root').innerHTML='<div class="empty">读取订单失败: '+esc(String(e))+'</div>';});
|
|
371
|
+
}
|
|
372
|
+
function viewOrders(){
|
|
373
|
+
var a=getAuth();
|
|
374
|
+
if(!a){el('client-root').innerHTML='<div class="empty">查看订单需登录。<br><br><a class="btn" href="./login?next=./orders">去登录 →</a></div>';return;}
|
|
375
|
+
loadOrders(true,'我的订单(RLS 仅本人可见)');
|
|
376
|
+
}
|
|
377
|
+
function viewAdmin(){
|
|
378
|
+
var a=getAuth();
|
|
379
|
+
if(a&&a.email===ADMIN_EMAIL){loadOrders(true,'管理台 · 全部买家订单(RLS 管理员白名单)');return;}
|
|
380
|
+
el('client-root').innerHTML='<div class="muted">管理员固定账号:<code>'+ADMIN_EMAIL+'</code> / <code>admin-2026</code></div>'+
|
|
381
|
+
'<input id="aemail" value="'+ADMIN_EMAIL+'"><input id="apass" type="password" placeholder="管理员密码">'+
|
|
382
|
+
'<div class="row" style="margin-top:10px"><button onclick="doAdminLogin()">管理员登录</button></div><div class="muted" id="amsg" style="margin-top:10px"></div>';
|
|
383
|
+
}
|
|
384
|
+
function doAdminLogin(){
|
|
385
|
+
var email=el('aemail').value.trim(),pass=el('apass').value;
|
|
386
|
+
el('amsg').textContent='登录中…';
|
|
387
|
+
authCall('login',email,pass).then(function(r){
|
|
388
|
+
if(r.ok&&r.body.access_token){setAuth({token:r.body.access_token,email:email});viewAdmin();}
|
|
389
|
+
else{el('amsg').textContent='管理员登录失败: '+(r.body&&r.body.msg||r.status);}
|
|
390
|
+
}).catch(function(e){el('amsg').textContent='登录失败: '+e;});
|
|
391
|
+
}
|
|
392
|
+
(function(){
|
|
393
|
+
refreshBadge();refreshAuthUI();
|
|
394
|
+
var p=location.pathname;
|
|
395
|
+
if(p.indexOf('/cart')>=0)viewCart();
|
|
396
|
+
else if(p.indexOf('/checkout')>=0)viewCheckout();
|
|
397
|
+
else if(p.indexOf('/orders')>=0)viewOrders();
|
|
398
|
+
else if(p.indexOf('/login')>=0)viewLogin();
|
|
399
|
+
else if(p.indexOf('/signup')>=0)viewSignup();
|
|
400
|
+
else if(p.indexOf('/admin')>=0)viewAdmin();
|
|
401
|
+
})();
|
|
402
|
+
`;
|
|
403
|
+
return "<script>" + js.replace("__ANON__", anon) + "</script>";
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function pageDoc(bodyInner: React.ReactElement, serverIso: string, anon: string): string {
|
|
407
|
+
const body = renderToString(bodyInner);
|
|
408
|
+
return "<!DOCTYPE html><html lang=\"zh\"><head><meta charset=\"utf-8\">" +
|
|
409
|
+
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">" +
|
|
410
|
+
"<title>Supabase Shop · React SSR</title><style>" + CSS + "</style></head><body>" +
|
|
411
|
+
"<div id=\"__ssr\" data-ssr=\"v1\">" + body + "</div>" +
|
|
412
|
+
"<footer>React 18 renderToString @ Hono 4.6.12 · SSR " + serverIso + " · 买家鉴权 GoTrue · 下单门禁 RLS · 管理员 admin@shop.demo</footer>" +
|
|
413
|
+
clientScript(anon) +
|
|
414
|
+
"</body></html>";
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// ---------- Hono 路由 ----------
|
|
418
|
+
const app = new Hono();
|
|
419
|
+
|
|
420
|
+
function norm(path: string): string {
|
|
421
|
+
return path.replace(/^\/app_[^/]+/, "") || path;
|
|
422
|
+
}
|
|
423
|
+
function catsOf(products: Product[]): string[] {
|
|
424
|
+
const cats = ["all"];
|
|
425
|
+
products.forEach((p) => { if (p.category && cats.indexOf(p.category) < 0) cats.push(p.category); });
|
|
426
|
+
return cats;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
app.get("*", async (c) => {
|
|
430
|
+
const p = norm(new URL(c.req.url).pathname);
|
|
431
|
+
const iso = new Date().toISOString();
|
|
432
|
+
const anon = anonKey();
|
|
433
|
+
const cfgErr = configError();
|
|
434
|
+
if (cfgErr) return c.html(configErrorDoc(cfgErr), 503);
|
|
435
|
+
|
|
436
|
+
if (p === "/" || p === "/index.html") return c.redirect("./shop", 302);
|
|
437
|
+
|
|
438
|
+
if (p === "/shop" || p === "/shop/") {
|
|
439
|
+
const q = (c.req.query("q") || "").trim();
|
|
440
|
+
const cat = c.req.query("cat") || "all";
|
|
441
|
+
const products = await fetchProducts();
|
|
442
|
+
const note = products.length ? "服务端取数 OK" : "内网基址不可达或无商品(降级空目录)";
|
|
443
|
+
return c.html(pageDoc(
|
|
444
|
+
h(React.Fragment, null,
|
|
445
|
+
h(Header, { base: "./" }),
|
|
446
|
+
h(SearchBar, { q, cat, cats: catsOf(products) }),
|
|
447
|
+
h("main", null, h(CatalogView, { products, q, cat, cats: catsOf(products), note }))),
|
|
448
|
+
iso, anon));
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const pm = p.match(/^\/product\/(\d+)$/);
|
|
452
|
+
if (pm) {
|
|
453
|
+
const prod = await fetchProduct(pm[1]);
|
|
454
|
+
return c.html(pageDoc(
|
|
455
|
+
h(React.Fragment, null, h(Header, { base: "../" }), h("main", null, h(ProductView, { p: prod, base: "../" }))),
|
|
456
|
+
iso, anon));
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (p === "/cart" || p === "/checkout" || p === "/orders" || p === "/login" || p === "/signup" || p === "/admin") {
|
|
460
|
+
const titles: Record<string, [string, string]> = {
|
|
461
|
+
"/cart": ["购物车", "浏览器 hydrate 直连 REST(localStorage 购物车)"],
|
|
462
|
+
"/checkout": ["结算", "需登录:订单绑定买家邮箱,RLS 数据层强制门禁"],
|
|
463
|
+
"/orders": ["我的订单", "需登录:RLS 仅本人订单可见(管理员见全量)"],
|
|
464
|
+
"/login": ["买家登录", "GoTrue 密码登录 · 会话存 localStorage · 浏览器直连鉴权"],
|
|
465
|
+
"/signup": ["买家注册", "游客注册成为买家 · GoTrue signup(免邮箱确认已开)"],
|
|
466
|
+
"/admin": ["后台管理", "固定密码账号 admin@shop.demo / admin-2026 · RLS 白名单看全量订单"],
|
|
467
|
+
};
|
|
468
|
+
const t = titles[p];
|
|
469
|
+
return c.html(pageDoc(
|
|
470
|
+
h(React.Fragment, null, h(Header, { base: "./" }), h("main", null,
|
|
471
|
+
h(ClientShell, { title: t[0], hint: t[1] }))),
|
|
472
|
+
iso, anon));
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (p === "/api/dbg" || p === "/dbg") {
|
|
476
|
+
const info: Record<string, unknown> = { hasUrl: !!Deno.env.get("SUPABASE_URL"), hasAnon: !!Deno.env.get("SUPABASE_ANON_KEY"), base: internalBase() };
|
|
477
|
+
try {
|
|
478
|
+
const r = await fetch(internalBase() + "/rest/v1/products?select=id", { headers: { apikey: anonKey() } });
|
|
479
|
+
info.fetchStatus = r.status;
|
|
480
|
+
} catch (e) { info.fetchErr = String(e).slice(0, 160); }
|
|
481
|
+
return c.json(info);
|
|
482
|
+
}
|
|
483
|
+
if (p === "/api/health" || p === "/health") return c.json({ ok: true, engine: "hono@4.6.12", ssr: "react@18.3.1" });
|
|
484
|
+
if (p === "/api/products" || p === "/products") return c.json(await fetchProducts());
|
|
485
|
+
|
|
486
|
+
return c.html(pageDoc(
|
|
487
|
+
h(React.Fragment, null, h(Header, { base: "./" }), h("main", null, h("div", { className: "empty" }, "404 · 页面不存在 " + p))),
|
|
488
|
+
iso, anon), 404);
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
Deno.serve(app.fetch);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "Declarative RLS for the shop webapp. Applied by `pxbaas app db-init` (single-tenant AND multi-tenant). table_schema is NEVER declared here: single-tenant binds it to --schema (default public); multi-tenant binds it server-side to the tenant db. using/check expressions may embed {{request.jwt.claim.*}} templates.",
|
|
3
|
+
"tables": [
|
|
4
|
+
{ "table": "products", "rlsEnabled": true, "rlsForced": false },
|
|
5
|
+
{ "table": "orders", "rlsEnabled": true, "rlsForced": false },
|
|
6
|
+
{ "table": "order_items", "rlsEnabled": true, "rlsForced": false }
|
|
7
|
+
],
|
|
8
|
+
"policies": [
|
|
9
|
+
{ "table": "products", "name": "products_select_anon", "role": "anon", "command": "SELECT", "using": "1=1", "check": null, "permissive": true },
|
|
10
|
+
{ "table": "products", "name": "products_select_auth", "role": "authenticated", "command": "SELECT", "using": "1=1", "check": null, "permissive": true },
|
|
11
|
+
{ "table": "orders", "name": "orders_select_auth", "role": "authenticated", "command": "SELECT", "using": "`buyer_email` = {{request.jwt.claim.email}} OR {{request.jwt.claim.email}} = 'admin@shop.demo'", "check": null, "permissive": true },
|
|
12
|
+
{ "table": "orders", "name": "orders_insert_auth", "role": "authenticated", "command": "INSERT", "using": null, "check": "`buyer_email` = {{request.jwt.claim.email}}", "permissive": true },
|
|
13
|
+
{ "table": "order_items", "name": "order_items_select_auth", "role": "authenticated", "command": "SELECT", "using": "`buyer_email` = {{request.jwt.claim.email}} OR {{request.jwt.claim.email}} = 'admin@shop.demo'", "check": null, "permissive": true },
|
|
14
|
+
{ "table": "order_items", "name": "order_items_insert_auth", "role": "authenticated", "command": "INSERT", "using": null, "check": "`buyer_email` = {{request.jwt.claim.email}}", "permissive": true }
|
|
15
|
+
]
|
|
16
|
+
}
|