Skip to content

feat(db): 自建 PostgreSQL + pgAdmin GUI + 自动备份,替代 Neon#12

Merged
longsizhuo merged 5 commits intomainfrom
feat/self-hosted-db
Apr 17, 2026
Merged

feat(db): 自建 PostgreSQL + pgAdmin GUI + 自动备份,替代 Neon#12
longsizhuo merged 5 commits intomainfrom
feat/self-hosted-db

Conversation

@longsizhuo
Copy link
Copy Markdown
Member

背景

Neon 免费月度 100 CU-h 配额耗尽后计算节点被暂停,/api/events、登录、文档等全部报错(2026-04-17 事故)。

方案

本机 compose 自建 postgres:18-alpine 替代 Neon,附带:

  • pgAdmin(:8082):带完整 Backup / Restore 按钮的 Web GUI,预注册连接和密码,登录即用
  • pg-backup:每天 03:00 自动 pg_dump,保留 30d / 8w / 12m

迁移过程

  1. pg_dump -Fc 从 Neon pooler 拉出完整快照(14 张表、5487 行)
  2. 先导入临时库 involution_hell_test 比对行数 ✅
  3. 停 backend → 再 dump 一次(cutover)→ drop + restore 到本地 involution_hell
  4. .env PGHOST 从 Neon endpoint 改为 compose 服务名 postgres
  5. docker compose up -d --force-recreate 重启全栈
  6. 校验:/api/events 返回 4 条真实数据 ✅,/actuator/health db UP ✅

变更内容

  • docker-compose.yml:新增 pgadmin + pg-backup 服务和共享卷 pg-backups
  • docker/pgadmin/servers.json:预注册 InvolutionHell (local) 连接
  • docker/pgadmin/pgpass.example + .gitignore:pgpass 走只读挂载,实际文件不入库
  • docs/database.md:完整运维手册(日常使用、手动/自动备份、GUI/CLI 恢复、初始化步骤)

部署机需要做的

本 PR 合并后,部署机上:

  1. 根据 docs/database.md 的 "初始化 pgpass 文件" 生成 docker/pgadmin/pgpass(UID 5050, 0600)
  2. .env 补上 PGADMIN_EMAIL / PGADMIN_PASSWORD,并把 PGHOST / JDBC URL 指向 postgres
  3. docker compose up -d

测试

  • backend → 本地 PG 连接正常
  • /api/events 返回真实数据
  • pgAdmin :8082 登录正常,左侧树显示预注册服务器
  • pg-backup 手动触发 /backup.sh 成功产出 .sql.gz
  • pgAdmin 能看到备份目录 /var/lib/pgadmin/storage/.../backups/

Neon 免费月度 100 CU-h 配额耗尽后计算节点被暂停,业务全挂(2026-04-17 事故)。
改用本机 compose 起 postgres:18-alpine,附带 pgAdmin 作为带按钮的备份/恢复 GUI、
pg-backup 容器做每日自动快照(保留 30d/8w/12m)。

- docker-compose.yml 新增 pgadmin 与 pg-backup 服务,共享 pg-backups 命名卷
- pgAdmin 预注册 InvolutionHell 服务器,pgpass 通过只读挂载提供,不需每次手填
- pgpass 走 .gitignore,提供 pgpass.example 作模板
- docs/database.md 完整记录:日常使用、手动/定时备份、GUI/CLI 恢复流程、迁移历史

.env 的 PGHOST 已在服务器上从 Neon endpoint 改为 compose 服务名 postgres,
仓库中 .env 不入库故未一并提交,需按 docs/database.md 描述在部署机上同步更新。
Copilot AI review requested due to automatic review settings April 17, 2026 20:28
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

将数据库从 Neon 迁移到自建 Docker PostgreSQL,并补齐 pgAdmin GUI 与定时备份能力,降低因外部配额/暂停导致的全站不可用风险。

Changes:

  • docker-compose.yml 增加 pgadminpg-backup 服务,并新增 pgadmin-datapg-backups 命名卷
  • 增加 pgAdmin 预注册连接配置(servers.json)与 pgpass 示例/忽略规则
  • 新增数据库运维手册(备份/恢复/初始化与迁移记录)

Reviewed changes

Copilot reviewed 4 out of 5 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
docs/database.md 新增自建 PG + pgAdmin + 备份/恢复操作手册与迁移记录
docker/pgadmin/servers.json pgAdmin 预注册服务器连接配置
docker/pgadmin/pgpass.example pgpass 示例文件
docker-compose.yml 增加 pgAdmin 与定时备份容器、挂载备份卷与健康依赖
.gitignore 忽略真实 pgpass 密码文件

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread docker-compose.yml Outdated
Comment on lines +101 to +103
# 备份卷挂到 pgAdmin 用户默认存储目录,这样在 Restore 对话框里能直接选到
# pg-backup 容器每天生成的 dump 文件。路径格式:<email 替换 @ 为 _>/
- pg-backups:/var/lib/pgadmin/storage/admin_involutionhell.com/backups
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pg-backups 被挂载到 pgAdmin 的路径硬编码为 admin_involutionhell.com(依赖 PGADMIN_DEFAULT_EMAIL@ 替换成 _ 的规则)。一旦部署时用自定义 PGADMIN_EMAIL,pgAdmin 的默认 storage 路径会变化,导致 Restore/Storage Manager 看不到同一个备份卷目录。建议将卷挂载到更稳定的路径(例如挂到 /backups 或挂到 /var/lib/pgadmin/storage 顶层并在文档中说明目录名),或明确强制/校验 PGADMIN_EMAIL 必须是 admin@involutionhell.com

Copilot uses AI. Check for mistakes.
Comment thread docker-compose.yml Outdated
Comment on lines +90 to +94
PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_PASSWORD:-change_me}
PGADMIN_CONFIG_SERVER_MODE: "False"
PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: "False"
ports:
- "8082:80"
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pgAdmin 通过 ports: "8082:80" 直接对外暴露,但同时给了默认账号/密码(admin@involutionhell.com / change_me)并关闭 master password。若部署机未显式配置 .env,会产生可被弱口令/默认口令访问的管理面板风险。建议去掉默认密码(要求必须配置 PGADMIN_PASSWORD),并考虑仅绑定到 127.0.0.1:8082 或放到反向代理后加额外认证/访问控制。

Suggested change
PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_PASSWORD:-change_me}
PGADMIN_CONFIG_SERVER_MODE: "False"
PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: "False"
ports:
- "8082:80"
PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_PASSWORD:?PGADMIN_PASSWORD must be set}
PGADMIN_CONFIG_SERVER_MODE: "False"
PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: "False"
ports:
- "127.0.0.1:8082:80"

Copilot uses AI. Check for mistakes.
Comment thread docs/database.md
docker-compose.yml 里四个相关服务:
postgres — PostgreSQL 18 主库,数据卷 involution-postgres-data(命名卷,持久化)
backend — Spring Boot,通过内网 jdbc:postgresql://postgres:5432/involution_hell 访问
pg-backup — prodrigestivill/postgres-backup-local,每天 03:00 跑 pg_dump,保留 30 天日备/8 周周备/12 月月备
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里写“每天 03:00 跑 pg_dump”,但当前 compose 里 pg-backupSCHEDULE@daily(不一定是 03:00)。文档应与实际配置保持一致:要么改文档时间描述,要么把 SCHEDULE 改成明确的 03:00 cron 表达式并注明时区。

Suggested change
pg-backup — prodrigestivill/postgres-backup-local,每天 03:00 跑 pg_dump,保留 30 天日备/8 周周备/12 月月备
pg-backup — prodrigestivill/postgres-backup-local,`@daily` 调度运行 pg_dump,保留 30 天日备/8 周周备/12 月月备

Copilot uses AI. Check for mistakes.
Comment thread docs/database.md
```bash
docker exec involution-pg-backup /backup.sh
# 产物:pg-backups 卷里 last/daily/weekly/monthly 各一份
# 文件名示例:involution_hell-20260417-201149.sql.gz(plain SQL + gzip)
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

手动备份这一段把产物描述为 .sql.gz(plain SQL + gzip),但 compose 里 pg-backup 已配置 POSTGRES_EXTRA_OPTS: "-Fc --blobs"(custom format),通常需要配合 pg_restore,且文件后缀/压缩方式可能不是 .sql.gz。建议统一:要么把 compose 改回 plain SQL + gzip(并在 env 设置对应 suffix),要么把本文这里的产物示例和后续恢复命令改为 custom format 的写法。

Suggested change
# 文件名示例:involution_hell-20260417-201149.sql.gz(plain SQL + gzip)
# 文件为 pg_dump -Fc --blobs 生成的 custom format,文件名/后缀以容器实际产物为准
# 恢复这类备份时通常使用 pg_restore

Copilot uses AI. Check for mistakes.
Comment thread docs/database.md
Comment on lines +50 to +63
# 1. 选一个备份文件
FILE=involution_hell-20260417.sql.gz

# 2. 停写入(避免恢复期间 backend 又写进去造成冲突)
docker stop involution-hell-backend

# 3. 清空当前库
docker exec involution-postgres psql -U neondb_owner -d postgres \
-c "DROP DATABASE involution_hell WITH (FORCE);" \
-c "CREATE DATABASE involution_hell OWNER neondb_owner;"

# 4. 恢复(plain SQL.gz 格式用 psql + gunzip;若是 -Fc 自定义格式则改用 pg_restore)
docker exec involution-pg-backup sh -c \
"gunzip -c /backups/daily/$FILE | psql -h postgres -U neondb_owner -d involution_hell"
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

恢复命令示例默认用 gunzip | psql 并且 FILE 也写成 .sql.gz,但当前 compose 已开启 -Fc(custom format)。在 custom format 下应使用 pg_restore(并匹配实际备份文件后缀/路径),否则按文档操作会直接失败。建议把示例拆成两套:plain SQL.gz 的恢复流程和 custom -Fc 的恢复流程,并确保默认配置对应其中一套。

Copilot uses AI. Check for mistakes.
@@ -0,0 +1 @@
postgres:5432:*:involution:change_me
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

该示例文件使用的用户名是 involution,但本 PR 的 servers.json 预注册服务器用户名是 neondb_owner,文档里恢复命令也用 neondb_owner。如果照这个示例生成 pgpass,pgAdmin 仍会因为用户不匹配而无法自动填充密码/连接失败。建议让 pgpass.example 的用户与 servers.json/默认 POSTGRES_USER 保持一致,或在文档中明确三处必须同步修改。

Suggested change
postgres:5432:*:involution:change_me
postgres:5432:*:neondb_owner:change_me

Copilot uses AI. Check for mistakes.
Comment thread docs/database.md
Comment on lines +17 to +19
备份文件写入 `pg-backups` 命名卷,pgAdmin 也挂载同一个卷到
`/var/lib/pgadmin/storage/admin_involutionhell.com/backups/`,
在 pgAdmin 的 Restore 对话框里直接选得到。
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里的 pgAdmin storage 路径同样硬编码了 admin_involutionhell.com,并依赖 PGADMIN_EMAIL 的具体值(@ 替换为 _)。如果部署时改了 PGADMIN_EMAIL,文档中给的路径将不再成立,可能导致“pgAdmin 看不到备份文件”的困惑。建议在文档里改为描述规则/动态路径,或要求固定 PGADMIN_EMAIL 并在 compose/文档中显式写出该约束。

Suggested change
备份文件写入 `pg-backups` 命名卷,pgAdmin 也挂载同一个卷到
`/var/lib/pgadmin/storage/admin_involutionhell.com/backups/`
在 pgAdmin 的 Restore 对话框里直接选得到。
备份文件写入 `pg-backups` 命名卷,pgAdmin 也挂载同一个卷。
在容器内,pgAdmin 可见的目录通常位于
`/var/lib/pgadmin/storage/<由 PGADMIN_EMAIL 派生的目录名>/backups/`
其中目录名依赖 `.env` 里的 `PGADMIN_EMAIL`(通常可理解为将 `@` 替换为 `_`)。
如果部署时修改了 `PGADMIN_EMAIL`,请按实际派生后的目录查看;在 pgAdmin 的 Restore 对话框里可直接选到该目录中的备份文件。

Copilot uses AI. Check for mistakes.
Comment thread docker-compose.yml
- ./docker/pgadmin/servers.json:/pgadmin4/servers.json:ro
# pgpass 主机文件必须是 UID 5050 所有且 0600,否则 pgAdmin 拒绝加载。
# 用 `sudo chown 5050:5050 docker/pgadmin/pgpass && sudo chmod 600 …` 设好。
- ./docker/pgadmin/pgpass:/tmp/pgpass
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pgpass 在 PR 描述里提到“只读挂载”,但这里的 bind mount 没有加 :ro。如果容器内进程(或被入侵后)能改写该文件,会影响后续连接行为;也不符合上面注释的“只读”意图。建议把该挂载改成只读,或更新注释/文档与实际保持一致。

Suggested change
- ./docker/pgadmin/pgpass:/tmp/pgpass
- ./docker/pgadmin/pgpass:/tmp/pgpass:ro

Copilot uses AI. Check for mistakes.
Comment thread docker-compose.yml
Comment on lines +110 to +123
# 自动定时备份:每天 03:00 对 postgres 容器做 pg_dump(custom format),
# 输出到共享卷 pg-backups,pgAdmin 能直接在 Restore 对话框里看到这些文件。
# 保留最近 30 天日备 / 8 周周备 / 12 个月月备。
pg-backup:
image: prodrigestivill/postgres-backup-local:18-alpine
container_name: involution-pg-backup
restart: unless-stopped
environment:
POSTGRES_HOST: postgres
POSTGRES_DB: ${POSTGRES_DB:-involution_hell}
POSTGRES_USER: ${POSTGRES_USER:-involution}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-change_me}
POSTGRES_EXTRA_OPTS: "-Fc --blobs"
SCHEDULE: "@daily"
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

注释写“每天 03:00”,但实际 SCHEDULE 配置为 @daily,通常只表示“每天一次”而不保证 03:00(具体取决于镜像的 cron 实现/时区)。建议将注释与配置对齐:要么把 SCHEDULE 改为明确的 cron 表达式(并在文档注明容器时区),要么把注释和运维手册里的时间描述改成“每天一次”。

Copilot uses AI. Check for mistakes.
Comment on lines +8 to +9
"MaintenanceDB": "involution_hell",
"Username": "neondb_owner",
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

servers.json 里将 Username 固定为 neondb_ownerMaintenanceDB 固定为 involution_hell。这会与 compose/.env 默认的 POSTGRES_USER=involution 不一致,并且在按文档执行 drop/recreate 数据库时,维护库如果就是 involution_hell 会导致 pgAdmin 连接在恢复窗口期直接断开/无法重连。建议:1) 将 MaintenanceDB 设为更稳定的 postgres;2) 账号名与 compose 默认保持一致,或在文档里明确要求部署必须把 POSTGRES_USER/导入账号设为 neondb_owner,避免 pgAdmin 预注册连接不可用。

Suggested change
"MaintenanceDB": "involution_hell",
"Username": "neondb_owner",
"MaintenanceDB": "postgres",
"Username": "involution",

Copilot uses AI. Check for mistakes.
主站 involutionhell.com/admin/database 页面 iframe pgAdmin,管理员一个入口
解决。pgAdmin 端口从 0.0.0.0 收回到 127.0.0.1:8082,只接受 Caddy 从本机转发。

- SCRIPT_NAME=/admin/pgadmin 让 pgAdmin 自生成 URL 带正确前缀
- X_FRAME_OPTIONS 清空,由上游 Caddy 用 CSP frame-ancestors 控制
- WTF_CSRF_SSL_STRICT 关闭,避免跨子域 iframe 触发 CSRF 拒绝

Caddy 配置(独立文件 /home/ubuntu/caddy-gateway/Caddyfile,不在本仓库)同步
添加 /admin/pgadmin/* handle,剥 X-Frame-Options 并下发
CSP: frame-ancestors 'self' https://involutionhell.com https://*.involutionhell.com ...

docs/database.md 补充 iframe 架构与环境变量说明。
pgAdmin 自身跑在 SERVER_MODE=False(无登录页,desktop 模式)对公网暴露是个
critical 漏洞:扫到路径就能对生产 DB 跑 SQL。改方案:外层 Caddy 在
/admin/pgadmin/* 前插 forward_auth 钩子,钩本接口;接口用 @SaCheckRole("admin")
判定当前请求带的 cookie.satoken 对应的用户是不是 admin,通过就 200,否则 sa-token
自动抛异常走 401 / 403。

- 新建 admin/controller 包专门放跨业务的基础设施级接口(不塞进 events/controller
  避免语义混淆)
- 响应体故意空壳,Caddy 只看状态码不读 body
- superadmin 自动包含 admin 角色,天然放行,无需额外分支
- sa-token.is-read-cookie 默认开启,不用改 application.properties

配套前端改动:InvolutionHell/involutionhell#301 登录时把 satoken 同步写到
.involutionhell.com 域名 cookie,浏览器跨子域自动带。
配套 Caddy:/home/ubuntu/caddy-gateway/Caddyfile 改 handle 块,见 docs/database.md。
替换掉上一版 iframe-嵌入的描述(那版已经废弃,走不通 CSRF)。新架构:

- pgAdmin SERVER_MODE=False 无自身登录页,仅 127.0.0.1:8082 监听
- 唯一公网入口 api.involutionhell.com/admin/pgadmin/* 由 Caddy handle 块
  管控,前置 forward_auth 调 127.0.0.1:8080/api/admin/pgadmin-check
- 后端 @SaCheckRole("admin") 依赖 sa-token 从 cookie 读 token
- 前端 lib/use-auth.tsx 登录成功时把 satoken 同步写 .involutionhell.com
  域名 cookie,浏览器跨子域自动带

补 Caddy 配置片段 + 后端 Controller 片段 + 前端同步逻辑说明,
future reviewer 不用再去三处代码翻找。
- 注释里显式写清楚:pgAdmin 跑 desktop 模式(无登录页)的安全前提是外层
  Caddy forward_auth 必须先到位。直接暴露 8082 是重大漏洞。
- pg-backups 卷从 /var/lib/pgadmin/storage/... 改挂到 /backups:ro。
  之前 SERVER_MODE=True 尝试时会因为 root 所有的备份目录触发
  'user does not have permission to read and write the specified storage directory'
  让 pgAdmin 无法启动;切回 desktop 模式后也避免继续污染 pgAdmin 自己的
  storage 路径。restore 对话框现在要手填 /backups/daily/xxx.dump 路径。
longsizhuo added a commit to InvolutionHell/involutionhell that referenced this pull request Apr 17, 2026
配合后端 /api/admin/pgadmin-check 和 Caddy forward_auth 的整条链:用户直连
api.involutionhell.com/admin/pgadmin/* 时浏览器不会主动发 satoken header,
必须靠 cookie 自动携带。

- 新加 syncTokenCookie(token):登录 / 刷新有效 session / 登出全部打点
  localhost 域不写 Domain(浏览器默认绑当前 host);
  生产写 Domain=.involutionhell.com 让主域 + 所有子域共享
  SameSite=Lax 刚好够——顶层导航 / 子资源 GET 都会带;跨站 POST 不带但我们
  也不需要(pgAdmin 的 CSRF 有自己的 cookie)
  Max-Age=2592000 与 sa-token.timeout 保持一致
- token 无效 / 登出时清掉 cookie,避免 stale 身份残留

服务端配套:InvolutionHell/involutionhell-backend#12
@longsizhuo longsizhuo merged commit 6dd5efd into main Apr 17, 2026
@longsizhuo longsizhuo deleted the feat/self-hosted-db branch April 17, 2026 22:09
longsizhuo added a commit to InvolutionHell/involutionhell that referenced this pull request Apr 17, 2026
* feat(admin): /admin/database 页面嵌入 pgAdmin iframe

管理员用一个主站入口进 pgAdmin 做备份/恢复/查表/跑 SQL,不再打开
api.involutionhell.com:8082 这种裸页面。pgAdmin 本身的 UI 风格跟主站不搭,
但用户明确说"管理员不配享受好 UI",优先接通能力。

- 新增 app/admin/database/page.tsx:AdminGuard 兜底权限,iframe src 走
  https://api.involutionhell.com/admin/pgadmin/(可由 NEXT_PUBLIC_PGADMIN_URL 覆盖)
- /admin 首页加"数据库管理"入口卡片

真实的权限/流量控制在后端 compose + Caddy 那边(见 involutionhell-backend#12):
Caddy 反向代理 /admin/pgadmin/* 到 127.0.0.1:8082,剥 X-Frame-Options,
下发 CSP frame-ancestors 放行 involutionhell.com 主域。

* feat(chat): onFinish 改 fetch 后端 /api/chat/sessions/save,不再直连 Prisma

背景:Neon → 自建 Docker PG 迁移后,前端 Prisma 还指向 Neon,AI 对话持久
化会写进旧库,和后端读自建 PG 分叉出脏数据。方案 A:把 chat + message 写
入挪到后端统一走,前端 onFinish 只发一次 HTTP。

- 删掉 import { prisma } from "@/lib/db",运行时再无 Prisma 依赖
- onFinish 原来三次 prisma 调用(chat upsert + user 消息 + assistant 消息)
  合并成一次 fetch(BACKEND_URL + "/api/chat/sessions/save")
- 后端接口匿名允许,登录时通过 satoken header 关联 userId,行为语义和原
  Prisma 版完全一致(匿名写 userId=NULL,登录补挂 userId)
- BACKEND_URL 未配或后端返回非 2xx 时 console.warn 不抛错,保持
  "持久化失败不阻塞对话流式返回"的原语义

Vercel AI SDK 流式路径(streamText / convertToModelMessages 等)完全未动,
前端 UX 无感知。

配套后端 PR:InvolutionHell/involutionhell-backend#13

* refactor(admin): /admin/database 去掉 iframe,改新标签打开 pgAdmin

iframe 嵌入两种嵌法都是坑:
  - 跨域嵌:pgAdmin session/CSRF cookie 走 SameSite=Lax,子域 iframe POST
    不带 cookie,登录永远报 "CSRF session token is missing"
  - 同源代理嵌:pgAdmin 会发绝对 URL 的重定向(host 是容器自己以为的值),
    浏览器跟着跳到 http://localhost:8082 变成 ERR_CONNECTION_REFUSED

管理员不高频用数据库,没必要为了 UI 嵌在主站里搭这些管道。改成一个大按钮,
target=_blank 打开 pgAdmin 自己的页面——cookie / CSRF 都在它自己域里,
一切正常工作。

同步删掉上一版临时加的 Next.js /admin/pgadmin/:path* rewrite。

* feat(auth): 登录成功同步 satoken 到 .involutionhell.com cookie

配合后端 /api/admin/pgadmin-check 和 Caddy forward_auth 的整条链:用户直连
api.involutionhell.com/admin/pgadmin/* 时浏览器不会主动发 satoken header,
必须靠 cookie 自动携带。

- 新加 syncTokenCookie(token):登录 / 刷新有效 session / 登出全部打点
  localhost 域不写 Domain(浏览器默认绑当前 host);
  生产写 Domain=.involutionhell.com 让主域 + 所有子域共享
  SameSite=Lax 刚好够——顶层导航 / 子资源 GET 都会带;跨站 POST 不带但我们
  也不需要(pgAdmin 的 CSRF 有自己的 cookie)
  Max-Age=2592000 与 sa-token.timeout 保持一致
- token 无效 / 登出时清掉 cookie,避免 stale 身份残留

服务端配套:InvolutionHell/involutionhell-backend#12

* feat(admin/database): hostname=localhost 时按钮自动指本地 pgAdmin

开发时访问 localhost:3010/admin/database 点按钮会直接打 prod
api.involutionhell.com,需要 cookie 但 localhost 登录时 cookie 写不到
.involutionhell.com 域,只能卡 401。

改成客户端挂载后读 window.location.hostname:
  - localhost / 127.0.0.1 → http://localhost:8082/admin/pgadmin/
    (要求开发者先 ssh -L 8082:127.0.0.1:8082 server 引端口)
  - 其他 → 原来的公网 URL(走 Caddy forward_auth 链)

NEXT_PUBLIC_PGADMIN_URL 仍然最高优先级,想覆盖任何时候都能覆盖。

useEffect 里 setState 走 Promise.resolve 异步化,绕开 React
"cascading renders" lint 规则。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants