Merge GitHub upstream into Gitea primary
This commit is contained in:
@@ -2113,3 +2113,51 @@
|
||||
- 防复发:任何送往旧事件引擎的日期必须由精度契约测试断言;新增日期表示必须先走共享日历校验,不能在调用端自行拼接或仅增加 Agent 重试步数。
|
||||
- 相关记录:BUG-116、BUG-118、BUG-120
|
||||
- 修复版本:本记录所在 staging 发布提交
|
||||
|
||||
## BUG-122 | self-hosted staging 管理员看不到独立后台入口
|
||||
|
||||
- 状态:superseded by BUG-123
|
||||
- 首次发现:2026-07-29
|
||||
- 最近更新:2026-07-29
|
||||
- 影响面:self-hosted staging 账户菜单、`GET /api/account`、独立后台入口;不影响后台独立登录与 `requireAdminSession`
|
||||
- 用户现象:身份库已持久化 `admin` 或 `viewer` 角色的用户登录主站后,账户菜单不显示后台入口;即使显示旧入口,主站 `/admin` 路径也会返回 404。
|
||||
- 触发条件:`AUTH_PROVIDER=self-hosted`,后台部署在与主站不同的 `AUTH_ADMIN_ORIGIN`,用户角色以逗号分隔形式持久化在 `identity.users.role`。
|
||||
- 根因:主站 `isAdminUser` 对 self-hosted 模式直接返回 `false`,没有读取持久化角色;侧栏又把入口写死为主站相对路径 `/admin/codes`。既有后台鉴权已按持久化角色执行,但主站入口发现逻辑没有复用同一授权事实,独立域名部署合同也没有进入账户响应。
|
||||
- 修复:self-hosted 分支通过现有 `ADMIN_DATABASE_URL` 管理只读连接查询当前用户的 `identity.users.role`,仅 `admin` 或 `viewer` 可见入口,且不使用 `ADMIN_EMAILS` 替代角色授权;`GET /api/account` 在服务端解析身份配置并返回 `AUTH_ADMIN_ORIGIN + /admin/codes`,Supabase 模式继续返回 `/admin/codes`;账户与侧栏类型透传该 URL,并将文案改为“后台管理”。后台独立登录和 `requireAdminSession` 保持不变。
|
||||
- 验证:`frontend/tests/admin-contracts.test.ts`、`frontend/tests/admin-users-contract.test.ts`、`frontend/tests/account-api.test.ts`、`frontend/tests/sidebar-contract.test.ts` 锁定持久化角色、独立后台 URL、服务端环境边界和后台写权限门禁;目标 TypeScript、构建与 staging 登录态 smoke 结果另行记录。
|
||||
- 防复发:self-hosted 主站入口发现必须以 `identity.users.role` 为授权事实,不能退回邮箱 allowlist;客户端不得读取后台 origin 环境变量或硬编码主站 `/admin` 路径;后台 API 必须继续独立执行 `requireAdminSession`,入口可见性不得被当作授权。
|
||||
- 相关记录:BUG-010、BUG-083、BUG-084、BUG-123
|
||||
- 复发自:BUG-010
|
||||
- 修复版本:已由 BUG-123 的同域单会话架构取代
|
||||
|
||||
## BUG-123 | self-hosted staging 双域后台与主站会话模型冲突
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-07-29
|
||||
- 最近更新:2026-07-29
|
||||
- 影响面:staging Better Auth 配置、后台页面与 API、登录、账户入口、Caddy、部署校验和 smoke;生产配置不变。
|
||||
- 用户现象:管理员需要第二套后台域名和浏览器会话才能进入后台,主站登录态不能直接使用;`viewer` 还被当作后台只读角色,与仅数据库 `admin` 可进入的产品合同冲突。
|
||||
- 触发条件:self-hosted staging 同时配置用户与后台 origin/secret、Caddy 拆分两个站点,并按 Host 选择 Better Auth 实例。
|
||||
- 根因:早期隔离设计把后台浏览器 surface 当成第二套身份系统,导致入口发现、登录、Cookie、部署变量和授权策略重复;同时把入口可见性与 API 权限错误扩展到 `viewer`。
|
||||
- 架构决策:后台复用主站 Better Auth user session;`identity.users.role` 的持久化 `admin` 是唯一后台授权事实。Better Auth 插件的 `/api/auth/admin` endpoint 继续在主站 fail-closed `404`,未知 Host 继续 `421`。
|
||||
- 修复:删除活动运行时后台 origin/secret 与 `services.admin`,服务端数据 client 和 `requireAdminSession` 统一读取 user session;后台 layout 增加服务端 gate,匿名转 `/login`、非 admin 不渲染;所有后台 API 保留独立 guard,payments/packages 改用 `requireAdminSession`;`isAdminUser`、账户入口和 Refine policy 收敛为 admin-only;登录取消 Host 分流;staging Caddy、Compose、环境校验、部署脚本、工作流和 smoke 收敛为同域。
|
||||
- 验证:身份 config/host/auth、admin policy/contracts、account/sidebar/login、部署/工作流与 admin layout/API guard 合同更新;针对性测试、TypeScript、Next build 与 `git diff --check` 结果记录在本次交付报告。生产部署未执行。
|
||||
- 防复发:活动运行配置和测试不得重新引入独立后台域名、`AUTH_ADMIN_ORIGIN`、`BETTER_AUTH_ADMIN_SECRET` 或浏览器 admin auth service;`viewer` 对后台入口、页面、读 API 和写 API 均必须为 `403`;入口可见性不能替代 route guard。
|
||||
- 相关记录:BUG-010、BUG-083、BUG-084、BUG-122
|
||||
- 复发自:BUG-122
|
||||
- 修复版本:`435e628806390e7ae138363491e7bae63ee801d4`,staging 已验收
|
||||
|
||||
## BUG-124 | 后台支付入口分散且界面风格不一致
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-07-29
|
||||
- 最近更新:2026-07-30
|
||||
- 影响面:后台 Refine 侧栏、`/admin/payments`、`/admin/packages`、易支付配置与对话页充值入口。
|
||||
- 用户现象:支付记录与支付配置占用两个导航项,页面仍使用主站 `standalone-page/admin-header/admin-section` 样式;套餐新增表单常驻页面,后台默认退出入口还会触发登出,管理员难以直接返回对话;对话页支付入口缺少安全默认关闭和服务端创建订单硬门禁。2026-07-29 复发时,Z-Pay 配置不能折叠且占据长页面,后台受全局 `html/body overflow:hidden` 限制无法纵向滚动,套餐 API 与易支付配置 API 仍调用 self-hosted adapter 不支持的 Supabase builder/RPC。2026-07-30 部署 `dd8e2ad9c7e76d0152b4563c43a45b1e26137035` 后,`GET /api/admin/payments` 与套餐管理仍返回 500。
|
||||
- 触发条件:进入同域 `/admin` 后管理支付记录或套餐,或点击 Refine 侧栏底部默认 Logout;复发条件为进入支付管理、展开长配置或调用套餐 CRUD / 易支付配置读写。2026-07-30 的数据库权限复发在 `admin_runtime` 通过 `ADMIN_DATABASE_URL` 查询支付表时稳定触发。
|
||||
- 根因:首轮支付后台实现依赖 Supabase 专用关联 select、分页、计数和 Admin Auth 查询;self-hosted staging 的本地 PostgreSQL adapter 不支持这些 builder 能力,支付记录因此统一降级为“支付记录服务暂时不可用”。同页套餐设计也不符合最新后台信息架构,易支付配置响应漏投影 `chat_enabled`,chat 创建订单又依赖服务端提交网关后猜测跳转地址,不兼容标准易支付收银台表单页。复发遗漏源于上轮只把支付记录切换到 PostgreSQL,套餐与配置契约测试没有锁定 self-hosted 数据链,且未覆盖聊天全局滚动边界下的后台专用滚动容器。2026-07-30 的直接根因是 `20260727020000_epay_packages_orders.sql` 只向 Supabase 的 `service_role` / `authenticated` 授权,未向 self-hosted 后台实际使用的 `admin_runtime` 授予 `payment_packages`、`payment_orders` 权限,也未添加对应 RLS 策略;因此数据库健康且新 SHA 已部署,后台 SQL 仍被 PostgreSQL权限门禁拒绝。同日还确认 staging 迁移与部署工作流错误地要求目标 SHA 属于 `main` 历史,使完全独立的测试分支被生产分支阻塞;该控制面耦合导致为恢复 staging 而误合并生产 main。
|
||||
- 修复:支付记录改为通过 `queryAdminRows` 执行参数化 SQL,联表 `public.payment_orders`、`public.payment_packages` 和 `identity.users`,以窗口计数保留分页合同并用独立聚合 SQL输出统计;不再使用 Supabase builder 或 Admin Auth。后台在支付管理之后新增独立“套餐管理”资源和页面,套餐新增、编辑、停用、错误重试及原字段保持完整,支付页只保留概览、Z-Pay(易支付)渠道配置和支付记录。配置读取补回 `chat_enabled` 与 `chatEnabled`。创建订单完成登录、开关、配置、SSRF、套餐和订单校验后,直接返回带 `sign/sign_type` 的标准 `submit.php` 收银台 URL,不服务端请求网关、不返回商户密钥;对话页用浏览器打开该 URL,套餐加载异常显示安全错误,正常 `enabled=false` 仍静默隐藏。复发修复将 Z-Pay 配置改为默认收起的 Ant Design `Collapse`,展开后才显示表单和操作;为 AdminApp 增加 `admin-app-shell` 的 `100dvh` 独立纵向滚动边界而不改聊天全局规则;套餐 CRUD 全部改用 `queryAdminRows` 参数化 SQL、UUID 校验、`returning` 与 404;易支付读取仅在 PostgreSQL `42P01` 时回退环境变量,保存直接参数化调用 `public.admin_save_epay_settings` 并使用函数返回行,保留原子审计和脱敏响应。2026-07-30 新增前向迁移 `20260730010000_admin_payment_permissions.sql`,向 `admin_runtime` 最小授予套餐读写、订单只读、易支付配置读取及保存函数执行权限,并为启用 RLS 的支付表补齐角色策略;不授予订单写入或删除权限。Gitea 与 GitHub 的 staging 迁移、部署和测试环境运维工作流统一 checkout `staging`,删除 staging SHA 属于 `main` 历史的要求;生产工作流保持不变。误合入 main 的 PR #1 已由 PR #2 的 revert 恢复,恢复后 main 内容树与合并前提交 `43581ac0f75e7f157032503475e878bd53ad161d` 完全一致。针对 run 1309,Gitea 两条远端工作流的 previous-SHA 探测与 registry login/logout 保持 `sudo -n docker`;脚本只接受受控的 `docker` 或 `sudo -n docker` 数组分支并拒绝其他值,不使用 `eval`。run 1313 证明 sudo Docker login 已成功,但 `run-staging-migration.sh` 第 37 行无法写入 root-owned 部署树下的 `/opt/jyotisha-staging/.state/mutation.lock`。因此迁移与部署工作流改为通过 `sudo -n env` 传入受控环境并以 root 启动整个脚本,脚本内固定 `DOCKER_BIN=docker`,`DOCKER_CONFIG` 仍指向 incoming 的 `.docker`;cleanup 使用 `sudo -n rm -rf` 删除脚本可能创建的 root-owned incoming 内容,Docker logout 仍使用 sudo。
|
||||
- 验证:`frontend/tests/admin-contracts.test.ts` 锁定支付、套餐资源顺序;`frontend/tests/admin-payments-contract.test.ts` 锁定本地参数化 SQL、`identity.users` 联表、套餐 SQL CRUD/UUID/404、独立套餐页面、默认折叠和后台专用滚动容器;`frontend/tests/epay-settings.test.ts` 锁定 `chatEnabled` 回显、`queryAdminRows` 读取、参数化 `admin_save_epay_settings`、不依赖 Supabase builder/RPC、默认折叠和不泄露 key。2026-07-29 运行三份契约测试共 27 项全部通过;ESLint、TypeScript 与 `git diff --check` 结果记录在本次交付报告。2026-07-30 线上健康响应证明部署 SHA 为 `dd8e2ad9c7e76d0152b4563c43a45b1e26137035` 且本地业务库、身份库均健康;静态权限审计确认支付迁移缺少 `admin_runtime` grant/RLS。新增权限迁移契约后,支付、套餐、配置三组 21 项回归全部通过。首次独立 staging 迁移 run 1300 在镜像校验阶段暴露 `docker manifest inspect --verbose` 对 ACR 返回单元素数组,而解析器只接受对象,触发 `AttributeError: 'list' object has no attribute 'get'`;Gitea staging 迁移与部署已兼容单平台数组并增加聚焦契约测试。run 1309 进一步确认远端 `deploy` 用户对 `/var/run/docker.sock` 无权限;run 1313 的 sudo Docker login 已成功,随后在迁移脚本第 37 行因 deploy 用户不能写 root-owned `/opt/jyotisha-staging/.state/mutation.lock` 而终止,证明仅提升 Docker 命令不足以覆盖部署树写入。工作流回归现锁定整个脚本由 `sudo -n env` 启动、脚本内 `DOCKER_BIN=docker`、incoming Docker 配置不变、root-owned cleanup 使用 sudo,并继续保留 runner 对两种固定 Docker 命令形式的契约。staging 最终部署 SHA 为 `1f44892a2cf210797e7dc74f49721a8f10c8849d`;迁移台账确认 `20260730010000_admin_payment_permissions.sql` 于 2026-07-30 05:58:38 UTC 应用,数据库 ACL/RLS 与 `admin_save_epay_settings` 的 `admin_runtime` 执行权限均已生效,web/api/postgres 容器健康,三个未登录管理 API 正确返回 401,部署后日志无相关 500。支付、套餐与易支付配置 21 项针对性回归通过,管理员随后确认 `/admin/payments` 与 `/admin/packages` 已恢复。
|
||||
- 防复发:self-hosted staging 后台查询不得依赖 LocalPostgresDataClient 未实现的 Supabase builder、RPC 或 Admin Auth 能力;支付与套餐必须保持独立资源顺序。套餐与易支付配置契约必须显式拒绝 Supabase builder/RPC 并锁定参数化 SQL、404、原子函数写入和安全错误响应;支付配置必须默认折叠,后台必须拥有独立滚动容器且不得放宽聊天的全局 `overflow:hidden`。易支付配置读写测试必须同时覆盖数据库列和公开字段;创建订单只生成经公网 SSRF 校验的签名收银台 URL,商户密钥只能参与服务端签名,不得进入 URL、响应、日志或审计。对话支付默认关闭,UI 与创建订单 API 必须共享服务端开关;可用性测试不得提交伪订单或返回 URL、PID、密钥、headers/body。
|
||||
- 相关记录:BUG-122、BUG-123
|
||||
- 修复版本:`d44a414`(权限迁移),staging 部署 `1f44892a2cf210797e7dc74f49721a8f10c8849d`
|
||||
|
||||
@@ -4,29 +4,27 @@ Staging uses Better Auth and the private local PostgreSQL cluster for both ident
|
||||
|
||||
## Staging mode
|
||||
|
||||
Keep these two values exactly as shown:
|
||||
Keep these values exactly as shown:
|
||||
|
||||
```dotenv
|
||||
AUTH_PROVIDER=self-hosted
|
||||
SELF_HOSTED_IDENTITY_ENABLED=true
|
||||
AUTH_USER_ORIGIN=https://staging.jyotisha.chat
|
||||
```
|
||||
|
||||
This makes both login hosts use isolated Better Auth surfaces. Public and admin sessions have different secrets and host-only cookie prefixes. Server routes translate the Better Auth session into PostgreSQL request claims and use the reviewed existing RLS/RPC business contract. The browser uses only same-origin APIs and does not need Supabase configuration.
|
||||
Staging has one browser identity surface on the main site. The same Better Auth user session serves ordinary pages and `/admin`; there is no independent admin origin, secret, cookie, or login host. Server routes translate that session into PostgreSQL request claims. Admin authorization then reads the persisted `identity.users.role` value and permits only `admin`; `viewer` and ordinary users receive `403`. The main auth route continues to return `404` for Better Auth `/api/auth/admin` plugin endpoints, and unknown hosts fail closed with `421`.
|
||||
|
||||
Use [the tracked staging identity example](../../deploy/.env.staging.identity.example) as a list of names only. Replace bracketed values directly on the server and keep `/opt/jyotisha-staging/.env.staging` owned by `deploy` with mode `0600`.
|
||||
|
||||
Generate separate secrets locally on the server:
|
||||
Generate `BETTER_AUTH_USER_SECRET` locally on the server:
|
||||
|
||||
```bash
|
||||
openssl rand -base64 32
|
||||
openssl rand -base64 32
|
||||
```
|
||||
|
||||
Do not reuse either value as a PostgreSQL password. `IDENTITY_DATABASE_URL`, `APP_DATABASE_URL`, and `ADMIN_DATABASE_URL` use their matching passwords from `.env.staging.database`, percent-encoded only in each URL password component. All three must point to the private Compose hostname `postgres:5432/jyotisha`; never publish PostgreSQL on a host port.
|
||||
Do not reuse it as a PostgreSQL password. `IDENTITY_DATABASE_URL`, `APP_DATABASE_URL`, and `ADMIN_DATABASE_URL` use their matching passwords from `.env.staging.database`, percent-encoded only in each URL password component. All three must point to the private Compose hostname `postgres:5432/jyotisha`; never publish PostgreSQL on a host port.
|
||||
|
||||
The Resend key must be staging-only. `RESEND_FROM_EMAIL` must use a sender/domain verified in Resend. CI never receives this key and uses an in-memory sender.
|
||||
|
||||
Set `ADMIN_EMAILS` to the staging administrator allowlist. Generate an independent `JYOTISH_DYNAMIC_RECTIFICATION_TOKEN` and place the same value in the shared application env consumed by the web and private API containers; do not reuse a database or Better Auth secret.
|
||||
The Resend key must be staging-only. `RESEND_FROM_EMAIL` must use a sender/domain verified in Resend. CI never receives this key and uses an in-memory sender. `ADMIN_EMAILS` remains relevant only to the legacy Supabase production path; it is not self-hosted admin authorization.
|
||||
|
||||
Validate without printing values:
|
||||
|
||||
@@ -38,17 +36,17 @@ bash deploy/validate-staging-env.sh .env.staging
|
||||
|
||||
## Migration and smoke checks
|
||||
|
||||
Apply the reviewed PostgreSQL migrations through the existing `Migrate Staging Database` workflow before deploying the web image. The workflow first ensures the compatibility roles exist, then applies the identity schema, the local `auth` compatibility layer, and all reviewed business migrations under the migration ledger. Better Auth users are transactionally projected into `auth.users`, which creates their business profile through the existing trigger.
|
||||
Apply the reviewed PostgreSQL migrations through the existing `Migrate Staging Database` workflow before deploying the web image. Better Auth users are transactionally projected into `auth.users`, which creates their business profile through the existing trigger.
|
||||
|
||||
After deployment:
|
||||
After deployment, verify the single-domain contract:
|
||||
|
||||
```bash
|
||||
curl -fsS https://admin.staging.jyotisha.chat/login >/dev/null
|
||||
curl -fsS https://admin.staging.jyotisha.chat/api/auth/get-session
|
||||
test "$(curl -sS -o /dev/null -w '%{http_code}' https://staging.jyotisha.chat/admin/codes)" = 404
|
||||
curl -fsS https://staging.jyotisha.chat/login >/dev/null
|
||||
test "$(curl -sS -o /dev/null -w '%{http_code}' https://staging.jyotisha.chat/api/admin/session)" = 401
|
||||
test "$(curl -sS -o /dev/null -w '%{http_code}' https://staging.jyotisha.chat/api/account)" = 401
|
||||
```
|
||||
|
||||
The admin root redirects to `/admin/codes`; the public host rejects `/admin` and `/api/admin` paths. An unknown or unpromoted email cannot create an admin session. Promote an imported staging user only through a reviewed database/admin operation; the persisted `identity.users.role` value must include `admin` before the admin OTP flow can issue a cookie.
|
||||
An anonymous `/admin` request redirects to `/login`. An authenticated non-admin, including a persisted `viewer`, must not render the admin layout and every `/api/admin/*` route must independently return `403`. Promote a staging user only through a reviewed database operation; the persisted role must include `admin` before the main-site session can enter the backend.
|
||||
|
||||
## Optional import rehearsal
|
||||
|
||||
@@ -75,8 +73,8 @@ Reruns are idempotent by UUID and the whole import is transactional. Duplicate c
|
||||
|
||||
## Rollback and rotation
|
||||
|
||||
An application rollback must use a previously validated staging image and does not reverse database migrations. Existing self-hosted sessions and data remain in PostgreSQL; do not delete identity or business rows during application rollback. Returning staging to Supabase would require a separate reviewed data-reconciliation and provider-switch change, not an environment-only toggle.
|
||||
An application rollback must use a previously validated staging image and does not reverse database migrations. Existing self-hosted sessions and data remain in PostgreSQL; do not delete identity or business rows during application rollback. Returning staging to Supabase requires a separate reviewed data-reconciliation and provider-switch change.
|
||||
|
||||
Rotating either Better Auth secret invalidates only that surface's existing sessions. Rotate user and admin secrets separately, restart the web service, and verify the corresponding host. Rotate a leaked Resend key in Resend first, replace the server value, then restart. Never print the old or new values.
|
||||
Rotating `BETTER_AUTH_USER_SECRET` invalidates all self-hosted browser sessions, including admins. Restart the web service and verify login, anonymous admin API rejection, admin access, and viewer rejection. Rotate a leaked Resend key in Resend first, replace the server value, then restart. Never print old or new values.
|
||||
|
||||
Production `AUTH_PROVIDER=self-hosted` remains blocked until data reconciliation passes, production backups and restore drills exist, operational monitoring is ready, and a separate reviewed production cutover plan is approved.
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
> Superseded 2026-07-29: staging browser identity and admin access now use one main-site Better Auth user session; the dual-domain admin surface in this historical plan is inactive.
|
||||
|
||||
# Self-Hosted Identity Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
> Superseded 2026-07-29: staging browser identity and admin access now use one main-site Better Auth user session; the dual-domain admin surface in this historical specification is inactive.
|
||||
|
||||
# Jyotisha Supabase Exit and Self-Hosted Backend Design
|
||||
|
||||
Date: 2026-07-20
|
||||
|
||||
Reference in New Issue
Block a user