-
Notifications
You must be signed in to change notification settings - Fork 45
Frontend Auth And Admin
登录、会话、权限、管理员后台的前端侧设计。对应 PR:
配合的后端运维文档在后端仓 docs/database.md(DB 自托管 + pgAdmin + forward_auth)。
-
登录:GitHub OAuth → 后端
/oauth/render/github→ GitHub → 后端/api/auth/callback/github→ 302 回/#token=xxx -
会话:satoken(UUID)存两份 ——
localStorage.satoken给同源 fetch;cookiesatoken; Domain=.involutionhell.com给跨子域直连(pgAdmin) -
权限:后端
/auth/me返回roles: string[](user/admin/superadmin),前端据此决定 UI / 路由可见性;真正的安全兜底在后端@SaCheckRole - 数据库:与前端组件关系是"管理员 UI 入口",不是"前端直连" —— 从 2026-04-17 起前端运行时零 Prisma 依赖
在 lib/use-auth.tsx 的 AuthProvider 里。
登录回跳时 URL 带 #token=xxx(fragment 而不是 query,不会进 server log / Referer):
https://involutionhell.com/#token=abc123...
AuthProvider 首屏解析 fragment → localStorage.setItem("satoken", urlToken) → replaceState 清掉 fragment。
紧跟着调 syncTokenCookie(urlToken),把同一个 token 写到 cookie:
satoken=<token>; Path=/; Domain=.involutionhell.com; Max-Age=2592000; SameSite=Lax; Secure
为什么需要 cookie 这份副本:业务 API 走 Next.js rewrite,同源 fetch 可以手动附 satoken header;但用户直连 api.involutionhell.com/admin/pgadmin/*(新标签页点 pgAdmin)时浏览器不会主动发自定义 header,只能靠 cookie 自动携带。Caddy 在 api 子域前放了 forward_auth 钩子调后端校验这个 cookie,不带就 401。
Domain 属性关键:.involutionhell.com(主域通配,所有子域共享)。localhost 开发要留空——浏览器默认绑当前 host,写 .localhost 会被拒绝。
SameSite=Lax:顶层导航 + GET 子资源都会带,跨站 POST 不带。够用——pgAdmin 自己的 CSRF 有独立 cookie,我们不依赖 satoken 处理 POST。
为什么 30 天 TTL:与 sa-token.timeout=2592000 后端配置保持一致,到期浏览器自动弃。
const { user, status, logout } = useAuth();
// status: "loading" | "authenticated" | "unauthenticated"
// user: UserView | nullServer-side(API Route / Server Component)从 request header 取:
import { resolveUserId } from "@/lib/server-auth";
const userId = await resolveUserId(req); // bigint | null,走 /auth/me 校验logout() 或 token 无效时:
localStorage.removeItem("satoken");
syncTokenCookie(null); // Max-Age=0 + 同 Domain/Path,浏览器才认是"同一条"才能清掉两边必须都清干净——单清 localStorage 会留下 stale cookie 让 forward_auth 误判,反过来也一样。
后端 user_accounts.roles(TEXT 逗号分隔,例 admin,user)。这张表不是 users —— users 是 2026-03-25 之前 NextAuth 时代留下的历史数据,当前系统不写。
给某人发 admin 角色唯一的正规姿势:
- 该人已经 GitHub OAuth 登录过一次(
user_accounts里有github_<gh_id>行) - superadmin 在
/admin/users页面勾 checkbox(调PUT /api/admin/users/{id}/admin)
不要手工 INSERT 一行 username=longsizhuo 挂 admin —— OAuth 登录按 github_<id> 查找,找不到人类 username 的行。这条坑踩过一次,参考 git history 里的事后数据迁移。
app/admin/events/AdminGuard.tsx:
<AdminGuard required="admin"> {/* 或 "superadmin" */}
<AdminOnlyContent />
</AdminGuard>行为:
-
status === "unauthenticated"→ 跳/login - 已登录但
roles不含required→ 403 提示页 - 通过 → 渲染 children
这只是 UX 保护。真正的安全在后端 @SaCheckRole 注解,绕 UI 也拿不到数据。
个人主页 /u/[username] 的"管理员界面"按钮:只有当前用户是 admin 且正在看自己主页时渲染。避免其他人看到这个按钮产生社交困惑。
| 路径 | 最低权限 | 后端对应 |
|---|---|---|
/admin |
admin | — |
/admin/events + /admin/events/[id]/edit + /admin/events/new
|
admin | /api/admin/events/** |
/admin/users |
superadmin | /api/admin/users/** |
/admin/database |
admin |
/api/admin/pgadmin-check(Caddy forward_auth 目标) |
不 iframe。历史上试过两种 iframe 嵌法都不行:
-
跨域嵌(iframe src =
api.involutionhell.com/admin/pgadmin/):pgAdmin 的 session / CSRF cookie 走SameSite=Lax,子域 iframe 发 POST 不带 cookie,登录永远报CSRF session token is missing -
同源代理嵌(Next.js rewrite 把 pgAdmin 代到 localhost:3010 下):pgAdmin 返回绝对 URL 的重定向(host 是容器自己以为的值),浏览器跟着跳
http://localhost:8082→ERR_CONNECTION_REFUSED
现在的实现是一个按钮 target="_blank" 打开 pgAdmin,让 pgAdmin 在自己的 origin 里管自己的 session / CSRF,省心。管理员不高频用 DB,视觉一致性排最后(用户原话"管理员不配享受好 UI")。
覆盖 URL 给本地开发:
NEXT_PUBLIC_PGADMIN_URL=http://localhost:8082/admin/pgadmin/ pnpm dev -p 3010
# 需要先 ssh -L 8082:127.0.0.1:8082 <server>app/api/chat/route.ts 的 streamText.onFinish 早期直连 Prisma,在 Neon 上写 Chat / Message 表。2026-04-17 起不再:
- 后端自建 Docker PG 替代 Neon 以后,Prisma 还指向 Neon 会写到旧库,和后端读的自建 PG 分叉出脏数据
- 改调后端
POST /api/chat/sessions/save,单次请求带{ chatId, userMessage, assistantMessage }由后端@Transactional原子落库
Vercel AI SDK 的流式路径完全没动,streamText / convertToModelMessages / result.toUIMessageStreamResponse() 一字没改,UX 无感知。改动只替换 onFinish 里的 3 次 prisma 调用为 1 次 fetch。
scripts/ 下的三个 Prisma 脚本(test.mjs / backfill-contributors.mjs / generate-leaderboard.mjs)暂未迁移——它们是离线 CI 脚本,需要重定向 DATABASE_URL 到自建 PG 或改走后端 API,属于 P2 工作。
大部分业务 API 通过 next.config.mjs 的 rewrites() 代理到后端,浏览器只见同源 localhost:3000 / involutionhell.com,Next.js 服务端再转发给后端 BACKEND_URL:
/auth/:path* → ${backendUrl}/auth/:path*
/oauth/:path* → ${backendUrl}/oauth/:path*
/analytics/:path* → ${backendUrl}/analytics/:path*
/api/user-center/:path* → ${backendUrl}/api/user-center/:path*
/api/docs/history → ${backendUrl}/api/docs/history
/api/events + /api/events/* → ${backendUrl}/...
/api/admin/events + /:path* → ${backendUrl}/...
/api/admin/users + /:path* → ${backendUrl}/...
/api/auth/callback/github → ${backendUrl}/...
特意不 rewrite 的:/admin/pgadmin/*。原因见"数据库管理页"一节——同源代理嵌踩坑。pgAdmin 入口就用绝对 URL 打到 api.involutionhell.com/admin/pgadmin/,让它留在自己的 origin。
Q:我新加了一个后端接口,为什么前端 fetch 直接用 /api/xxx 报 404?
A:Next.js rewrites 是白名单制——没列进去的路径就走 Next.js 自己的 route,不会转发给后端。去 next.config.mjs 加一条 rewrite。
Q:本地跑 pnpm dev,登录后刷新页面又退出了?
A:检查 BACKEND_URL env 是否指向你起着的后端(默认 http://localhost:8080;memory 里 IH 约定是 8081)。前端 fetchCurrentUser 调 /auth/me 失败就会清掉 token。
Q:我想加一个新的 admin 页面,权限控制怎么做?
A:前端包 <AdminGuard>,路径前缀 /admin/*;后端对应的 controller 方法加 @SaCheckRole("admin");next.config.mjs 把 /api/admin/your-feature/:path* rewrite 到后端。三件套。
Q:为什么个人主页 URL 是 /u/<github_id> 而不是 /u/<username>?
A:username 可能带特殊字符 / 后期改名;GitHub ID 是数字稳定的。AdminLinkIfOwnerAdmin 同时接受 user.githubId === ownerGithubId 或 user.username === ownerUsername 做匹配兜底。