Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,6 @@ build/

ga4-sa-key.json
.env

# pgAdmin 密码文件(含明文,不入库)
docker/pgadmin/pgpass
75 changes: 75 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,84 @@ services:
networks:
- InvolutionHell-net

# pgAdmin:PostgreSQL 的 Web GUI,带完整的备份/恢复按钮。
# 登录 http://<host>:8082,预注册服务器见 docker/pgadmin/servers.json,
# 密码通过 /pgpass 自动填充,不需要每次手输。
# 备份卷挂到 /var/lib/pgadmin/storage/.../backups,在 Restore 对话框里直接看得到。
pgadmin:
image: dpage/pgadmin4:latest
container_name: involution-pgadmin
restart: unless-stopped
environment:
PGADMIN_DEFAULT_EMAIL: ${PGADMIN_EMAIL:-admin@involutionhell.com}
PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_PASSWORD:-change_me}
# pgAdmin 保持 SERVER_MODE=False(desktop / single-user,无登录页)——
# 关键前提:**外层 Caddy 做 forward_auth**,只有带合法 admin satoken
# cookie 的请求才被代理到这里。直接对公网暴露 8082 绝对不行。
PGADMIN_CONFIG_SERVER_MODE: "False"
PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: "False"
# pgAdmin 被 iframe 嵌入在 involutionhell.com 主站的 /admin/database 页里,
# 流量路径:involutionhell.com(Vercel 前端) → iframe → api.involutionhell.com/admin/pgadmin/*
# SCRIPT_NAME 让 pgAdmin 生成的所有 URL 自带 /admin/pgadmin 前缀,
# X_FRAME_OPTIONS 清空让 Caddy 层自己控制 CSP frame-ancestors。
SCRIPT_NAME: "/admin/pgadmin"
PGADMIN_CONFIG_X_FRAME_OPTIONS: "''"
PGADMIN_CONFIG_WTF_CSRF_SSL_STRICT: "False"
ports:
- "127.0.0.1:8082:80"
volumes:
- pgadmin-data:/var/lib/pgadmin
- ./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

Copilot AI Apr 17, 2026

Copy link

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.
# 备份卷挂到容器内 /backups(只读)。SERVER_MODE=True 下 /var/lib/pgadmin/
# storage/<email>/ 目录由 pgAdmin 运行时自建并校验 5050 所有权,把 root
# 所有的 pg-backups 挂进去会触发 "user does not have permission" 启动失败。
# 在 pgAdmin Restore 对话框里手填路径 /backups/daily/xxx.dump 即可。
- pg-backups:/backups:ro
depends_on:
postgres:
condition: service_healthy
networks:
- InvolutionHell-net

# 自动定时备份:每天 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}
# -Fc 产出 custom 格式(.dump),pgAdmin 右键 Restore 直接一键恢复;
# 镜像包装脚本默认会 gzip,这里必须显式设 BACKUP_SUFFIX=.dump 且不加 gzip
# 选项,让镜像按 custom 格式直接落盘。
POSTGRES_EXTRA_OPTS: "-Fc --blobs"
BACKUP_SUFFIX: ".dump"
BACKUP_COMPRESS: "none"
SCHEDULE: "@daily"
Comment on lines +122 to +140

Copilot AI Apr 17, 2026

Copy link

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.
BACKUP_KEEP_DAYS: 30
BACKUP_KEEP_WEEKS: 8
BACKUP_KEEP_MONTHS: 12
HEALTHCHECK_PORT: 8080
volumes:
- pg-backups:/backups
depends_on:
postgres:
condition: service_healthy
networks:
- InvolutionHell-net

networks:
InvolutionHell-net:
driver: bridge

volumes:
involution-postgres-data:
pgadmin-data:
pg-backups:
1 change: 1 addition & 0 deletions docker/pgadmin/pgpass.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
postgres:5432:*:involution:change_me

Copilot AI Apr 17, 2026

Copy link

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.
16 changes: 16 additions & 0 deletions docker/pgadmin/servers.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"Servers": {
"1": {
"Name": "InvolutionHell (local)",
"Group": "Servers",
"Host": "postgres",
"Port": 5432,
"MaintenanceDB": "involution_hell",
"Username": "neondb_owner",
Comment on lines +8 to +9

Copilot AI Apr 17, 2026

Copy link

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.
"PassFile": "/tmp/pgpass",
"PassFileReadonly": true,
"SSLMode": "prefer",
"Shared": false
}
}
}
232 changes: 232 additions & 0 deletions docs/database.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
# 数据库运维手册(自建 Docker PostgreSQL)

> 2026-04-17 起,生产/开发均从 Neon 迁到本机 compose 起的 `postgres:18-alpine`。
> 迁移动因:Neon 免费月度额度(100 CU-h)耗尽后计算节点被暂停,
> 所有业务请求报错。自建后无配额限制,数据和延迟都本地可控。

## 架构总览

```
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 月月备

Copilot AI Apr 17, 2026

Copy link

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.
pgadmin — Web GUI,http://<host>:8082,带完整 Backup/Restore 按钮
```

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

Copilot AI Apr 17, 2026

Copy link

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.

## 常用操作

### 登录 pgAdmin(关键:走 sa-token cookie 校验,不是 pgAdmin 自己的账号密码)

**生产环境**

pgAdmin 容器跑在 `SERVER_MODE=False`(desktop 模式,**自身无登录页**,进去就能操作 DB)。
安全把门**不在 pgAdmin**,而是外层 Caddy 的 `forward_auth` 钩子:

```
浏览器 → api.involutionhell.com/admin/pgadmin/*
│ 携带 cookie: satoken=xxx(Domain=.involutionhell.com)
Caddy handle /admin/pgadmin/* {
forward_auth 127.0.0.1:8080 {
uri /api/admin/pgadmin-check ← 后端接口带 @SaCheckRole("admin")
copy_headers Cookie ← satoken cookie 透传
}
reverse_proxy 127.0.0.1:8082 ← 只有 forward_auth 200 才到这里
}
```

**用户视角**:
1. 先在主站 `involutionhell.com` 用 GitHub OAuth 登录
2. 登录成功时前端会把 satoken **双写**:
- `localStorage.satoken`:给同源 fetch 手动附 header 用
- `cookie satoken=...; Domain=.involutionhell.com`:给 api 子域直连用
3. 随便哪条都能进 pgAdmin:
- 点主站 `/admin/database` 页面的 "打开 pgAdmin" 按钮
- 直接敲 `https://api.involutionhell.com/admin/pgadmin/`
4. 浏览器自动带 cookie → Caddy forward_auth → 后端看 cookie + 查角色 → admin 就放过
5. 非 admin 或未登录 → 401(浏览器看到 Cloudflare/Caddy 的 401 错误页)

**不需要也不应该**再输 pgAdmin 自己的 email/password——`.env` 里 `PGADMIN_EMAIL`/`PASSWORD`
只是 desktop 模式下 pgAdmin 容器初始化用的占位,用户侧感知不到。

**本机联调**:直接 `ssh -L 8082:127.0.0.1:8082 <server>` 然后浏览器开
`http://localhost:8082/admin/pgadmin/`。这条路径绕过 Caddy,也就没有 forward_auth
校验——管理员自己机器上专用,不对外。

左侧树都能看到预注册的 `InvolutionHell (local)`,双击即连。

### 反向代理 / forward_auth 架构

```
involutionhell.com(主站 / Vercel)
│ 登录成功 → 前端 lib/use-auth.tsx 写 cookie: satoken=xxx; Domain=.involutionhell.com
├─ /admin/database 页面 → Link target=_blank → api.involutionhell.com/admin/pgadmin/
└─ 或者用户直接在地址栏敲 api.involutionhell.com/admin/pgadmin/
│ 浏览器自动带 Domain=.involutionhell.com 的 satoken cookie
Caddy (global-caddy-gateway, host 网络)
│ forward_auth 127.0.0.1:8080 uri=/api/admin/pgadmin-check
│ ├─ 200 → 继续代理
│ └─ 非 200 → 拒绝
127.0.0.1:8082 (pgAdmin 容器, SERVER_MODE=False)
```

**pgAdmin 容器环境变量**:
- `SCRIPT_NAME=/admin/pgadmin`:让 pgAdmin 自生成的 URL 自带前缀(含登录跳转 / CSS)
- `PGADMIN_CONFIG_SERVER_MODE=False`:desktop / single-user,**不渲染登录页**。
安全由外层 forward_auth 把守
- `PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED=False`:不用 master password 二次加密
- `PGADMIN_CONFIG_X_FRAME_OPTIONS=''`:清空默认 DENY,Caddy 负责 CSP
- 容器端口只绑 `127.0.0.1:8082`,不对公网开,唯一公网入口是 Caddy

**Caddy 配置**(`/home/ubuntu/caddy-gateway/Caddyfile`,不在本仓库):

```caddy
api.involutionhell.com {
handle /admin/pgadmin/* {
forward_auth 127.0.0.1:8080 {
uri /api/admin/pgadmin-check
copy_headers Cookie
}
header {
-X-Frame-Options
Content-Security-Policy "frame-ancestors 'self' https://involutionhell.com https://*.involutionhell.com https://*.vercel.app http://localhost:3000 http://localhost:3010"
}
reverse_proxy 127.0.0.1:8082 {
header_up X-Script-Name /admin/pgadmin
header_up X-Scheme https
header_up X-Forwarded-Proto https
}
}
handle { reverse_proxy 127.0.0.1:8080 }
}
```

**后端端点**(`AdminInfraController.java`):

```java
@RestController
@RequestMapping("/api/admin")
public class AdminInfraController {
@GetMapping("/pgadmin-check")
@SaCheckRole("admin")
public ApiResponse<Void> pgadminCheck() { return ApiResponse.okMessage("authorized"); }
}
```

sa-token 默认从 header **和** cookie 同时读(`is-read-cookie=true` 默认开),
所以无论是同源 fetch 带 satoken header、还是跨子域的新标签页靠 cookie 自动带,
都能命中同一套角色校验。

**前端 cookie 同步**(`lib/use-auth.tsx` 的 `syncTokenCookie`):

登录成功 / 每次刷新有效 session 时把 `localStorage.satoken` 复制一份到 cookie,
`Domain=.involutionhell.com; Max-Age=2592000`。localhost 开发时不带 Domain。
登出 / token 失效时反向清 cookie。

### 手动备份(立刻打一个快照)

```bash
docker exec involution-pg-backup /backup.sh
# 产物:pg-backups 卷里 last/daily/weekly/monthly 各一份
# 文件名示例:involution_hell-20260417-201149.sql.gz(plain SQL + gzip)

Copilot AI Apr 17, 2026

Copy link

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.
```

### 查看历史备份

```bash
docker exec involution-pg-backup ls -lh /backups/daily/
docker exec involution-pg-backup ls -lh /backups/weekly/
docker exec involution-pg-backup ls -lh /backups/monthly/
```

或者在 pgAdmin 里:Tools → Storage Manager → `backups/`。

### 恢复到指定时间点(命令行)

```bash
# 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"
Comment on lines +156 to +169

Copilot AI Apr 17, 2026

Copy link

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.

# 5. 启回 backend
docker start involution-hell-backend
```

### 恢复到指定时间点(pgAdmin GUI)

pgAdmin 的 Restore 对话框默认只支持 custom/tar/directory 格式,
`.sql.gz` 不直接支持。解决方式二选一:

- **Query Tool 导入**:右键数据库 → Query Tool → 把 `.sql.gz` 解压后粘贴 SQL 执行
- **改用自定义格式备份**:见下方「切换备份格式」章节

### 切换备份格式到 pg_restore 兼容的 `-Fc`(可选)

在 `docker-compose.yml` 的 `pg-backup` 服务改:

```yaml
environment:
POSTGRES_EXTRA_OPTS: "-Fc"
BACKUP_SUFFIX: ".dump"
```
Comment on lines +183 to +191

Copilot AI Apr 17, 2026

Copy link

Choose a reason for hiding this comment

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

这一节说“切换备份格式到 -Fc(可选)”,但当前 docker-compose.ymlpg-backup 已经默认配置了 POSTGRES_EXTRA_OPTS: "-Fc --blobs"。这会让读者误以为默认是 .sql.gz,与前文/恢复命令示例产生冲突。建议要么把 compose 默认改回 plain SQL.gz,并保留这里作为可选切换;要么把这里改成“当前已使用 -Fc”并补全 BACKUP_SUFFIX/恢复命令的对应说明。

Copilot uses AI. Check for mistakes.

然后重建:`docker compose up -d --force-recreate pg-backup`。
之后 pgAdmin 右键数据库 → Restore → 文件类型选 Custom,直接点按钮即可。
代价:备份文件比 gzip 压缩的略大一点。

## 连接凭证

所有 DB 连接信息在根目录 `.env`:

| 变量 | 用途 |
|---|---|
| `POSTGRES_DB` / `POSTGRES_USER` / `POSTGRES_PASSWORD` | postgres 容器初始化 + backup 脚本 |
| `SPRING_DATASOURCE_URL` / `*_USERNAME` / `*_PASSWORD` | backend 的 JDBC 连接 |
| `PGHOST` / `PGPORT` / `PGUSER` / `PGPASSWORD` / `PGDATABASE` | psql 兼容环境变量(部分脚本依赖) |
| `PGADMIN_EMAIL` / `PGADMIN_PASSWORD` | pgAdmin Web 登录 |

> 账号沿用 `neondb_owner` 是为了最小化改动,不是生产建议。
> 后续可以跑 `ALTER USER neondb_owner RENAME TO involution;` 改得更整洁。

## 初始化 pgpass 文件

`docker/pgadmin/pgpass` 被 .gitignore,首次部署要在服务器上生成:

```bash
cd ~/involution-hell
set -a && . ./.env && set +a
printf 'postgres:5432:*:%s:%s\n' "$POSTGRES_USER" "$POSTGRES_PASSWORD" \
> docker/pgadmin/pgpass
# pgAdmin 容器内 UID=5050,必须把文件 owner 改成 5050,且权限 0600
sudo chown 5050:5050 docker/pgadmin/pgpass
sudo chmod 600 docker/pgadmin/pgpass
```

没这步 pgAdmin 会 restart loop,日志里是 `cp: can't open '/pgpass': Permission denied`。

## 数据迁移的历史记录

- 2026-04-17:Neon `involution-hell` 项目 → 本机 `involution-postgres` 容器
- dump 方法:`pg_dump -Fc` 从 pooler endpoint 拉出 14 张表、5487 行
- 验证:先导入 `involution_hell_test` 对过行数,再切流量
- `.env` 里 `PGHOST` 由 Neon endpoint 改为 `postgres`(compose 服务名)
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.involutionhell.backend.admin.controller;

import cn.dev33.satoken.annotation.SaCheckRole;
import com.involutionhell.backend.common.api.ApiResponse;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* 管理员基础设施(非业务)辅助接口。
*
* 目前只有一个:/api/admin/pgadmin-check —— 专门给 Caddy `forward_auth` 调用,
* 用来判断当前请求是否是 admin。通过就 200,否则 sa-token 自动抛 NotLogin /
* NotPermission 异常,全局异常处理器转成 401 / 403,Caddy 据此拒绝代理到 pgAdmin。
*
* 设计要点:
* - sa-token 默认从 header / cookie 两边读 token(sa-token.is-read-cookie=true 默认开)
* 配合前端在登录时把 satoken 同步写一份到 .involutionhell.com 域名 cookie,
* 浏览器直接访问 api 子域时也能带上,forward_auth 校验链才能成立
* - 响应体故意空壳,Caddy 只看状态码不看 body;保持最小负载
* - 单独放在 admin/controller 包下而不是塞进 events/controller:这是
* "基础设施级"鉴权桩,不属于任何业务域,放一起语义会误导
*/
@RestController
@RequestMapping("/api/admin")
public class AdminInfraController {

/**
* Caddy 的 forward_auth 目标。只要通过 @SaCheckRole("admin") 就返回 200。
*
* superadmin 的 roles 集合也包含 "admin"(由 sa-token 角色体系保证),
* 所以超管也能直接过,不用单独处理。
*/
@GetMapping("/pgadmin-check")
@SaCheckRole("admin")
public ApiResponse<Void> pgadminCheck() {
return ApiResponse.okMessage("authorized");
}
}