@@ -36,6 +36,37 @@ function getStoredToken(): string | null {
3636 return localStorage . getItem ( "satoken" ) ;
3737}
3838
39+ /**
40+ * 把 satoken 同步写一份到 `.involutionhell.com` 域名的 cookie。
41+ *
42+ * 为什么需要:直接访问 api.involutionhell.com/admin/pgadmin/*(新标签页打开 pgAdmin)
43+ * 时浏览器不会主动发 `satoken` header——业务 API 是走 Next.js rewrite 同源的,所以
44+ * 前端能手动附 header;但新标签直连 api 子域就只能靠 cookie 自动带。
45+ *
46+ * Caddy 在 api 子域前放了 forward_auth 钩子调后端 /api/admin/pgadmin-check,
47+ * sa-token 默认从 cookie 读 token(sa-token.is-read-cookie=true 默认开),
48+ * 只要 cookie 存在且对应 satoken 在后端会话库里且拥有 admin 角色就放行。
49+ *
50+ * 本地开发(localhost)的 Domain 属性要留空,浏览器会默认绑当前 host;
51+ * 生产(involutionhell.com + api.involutionhell.com)要显式写 `.involutionhell.com`
52+ * 否则两个子域 cookie 各存各的。
53+ */
54+ function syncTokenCookie ( token : string | null ) {
55+ if ( typeof document === "undefined" ) return ;
56+ const isLocalhost =
57+ window . location . hostname === "localhost" ||
58+ window . location . hostname === "127.0.0.1" ;
59+ const domainAttr = isLocalhost ? "" : "; Domain=.involutionhell.com" ;
60+ const secureAttr = window . location . protocol === "https:" ? "; Secure" : "" ;
61+ if ( token ) {
62+ // 30 天 TTL 和 sa-token 服务端配置保持一致(application.properties 里 2592000)
63+ document . cookie = `satoken=${ encodeURIComponent ( token ) } ; Path=/${ domainAttr } ; Max-Age=2592000; SameSite=Lax${ secureAttr } ` ;
64+ } else {
65+ // 清除:设空值并 Max-Age=0;Domain / Path 必须与写入时一致浏览器才认这是"同一条"
66+ document . cookie = `satoken=; Path=/${ domainAttr } ; Max-Age=0; SameSite=Lax${ secureAttr } ` ;
67+ }
68+ }
69+
3970// 调用后端 /auth/me 验证 token 并获取用户信息
4071// 走 Next.js rewrite(/auth/* → 后端),浏览器无跨域问题
4172async function fetchCurrentUser ( token : string ) : Promise < UserView | null > {
@@ -63,8 +94,9 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
6394 const urlToken = hashParams . get ( "token" ) ;
6495
6596 if ( urlToken ) {
66- // 存入 localStorage
97+ // 存入 localStorage + 同步写 cookie(供 api 子域直连场景使用,比如 pgAdmin)
6798 localStorage . setItem ( "satoken" , urlToken ) ;
99+ syncTokenCookie ( urlToken ) ;
68100 // 用 replaceState 清除 URL 中的 fragment,避免刷新或分享时 token 泄露
69101 hashParams . delete ( "token" ) ;
70102 const newHash = hashParams . toString ( ) ;
@@ -87,9 +119,12 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
87119 if ( u ) {
88120 setUser ( u ) ;
89121 setStatus ( "authenticated" ) ;
122+ // 已登录用户每次刷新也重写 cookie,覆盖掉可能过期 / 丢失的副本
123+ syncTokenCookie ( token ) ;
90124 } else {
91- // token 无效或已过期,清除
125+ // token 无效或已过期,localStorage + cookie 都清
92126 localStorage . removeItem ( "satoken" ) ;
127+ syncTokenCookie ( null ) ;
93128 setStatus ( "unauthenticated" ) ;
94129 }
95130 } ) ;
@@ -110,6 +145,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
110145 }
111146 localStorage . removeItem ( "satoken" ) ;
112147 }
148+ syncTokenCookie ( null ) ;
113149 setUser ( null ) ;
114150 setStatus ( "unauthenticated" ) ;
115151 } ;
0 commit comments