feat(auth): add the frontend auth session layer - #163
Conversation
Business authentication failures arrive in successful HTTP envelopes. Add a scoped recovery provider and replay eligible requests once with the latest token. Authentication clients can opt out to prevent recursive refresh attempts.
Authentication endpoints expose backend DTOs that do not match frontend naming. Map all eight operations and validate successful user and token payloads explicitly. Callers receive a stable user contract while authentication requests avoid recursive recovery.
Feature modules consume entities through the repository public barrel. Expose user models, API types, the factory, and the lazy default instance. Authentication features can depend on the user entity without bypassing layer boundaries.
Refresh tokens need durable storage without making browser storage a login prerequisite. Add the contracted key with an in-memory fallback and failure tombstones. Storage errors now preserve the current tab session and cannot resurrect stale credentials.
Authentication state must survive refreshes while access tokens remain memory-only. Add session transitions, scheduled and reactive renewal, and cross-tab synchronization. Generation gates and per-token deduplication prevent stale refreshes from corrupting newer sessions.
The application root needs the real backend authentication session. Wrap App with AuthSessionProvider and inject the lazy user API instance. Runtime requests now share the mounted access-token and recovery lifecycle.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
@fennoai PR #163 / features/auth-session/index.tsx 的 rotateLatestTokens(122–152 行): 它在成功和失败两条路径上都做了同一件事——如果发现别的标签页写入了更新的 refresh token,就改用那个重新续期。成功路径里这次重新续期写在同一个 try 中,失败了会掉进下面的 catch;而 catch 判断「有没有没试过的 token」时,比较的始终是最初传入的 attemptedToken,于是它会把刚刚失败的那个 token 再试一遍。 复现:标签页已登录持有 A → 请求返回 401 触发续期 → 续期途中另一标签页写入 B → A 续期成功,但 B 在服务端被拒。实测 refresh 调用三次,参数依次是 A、B、B;A 换回来的有效令牌被丢弃,会话转 guest。 触发条件不宽,失败方向也安全(只会登出,不会串号),但会丢掉一套本来能用的会话。 请你判断:是修这条路径,还是这层「改用更新的 token 重试」本身就该去掉,改成让这种竞争不发生的做法。 |
Done
结论:保留“跟随更新 token 再续期”的策略,不建议删掉这层机制。这个竞争本身是现实存在的,PR 里也已经在处理跨标签页轮换、挂起恢复和单次重放;直接移除只会让这些场景更容易把会话打回 guest。 这里该修的是 所以我的判断是:修这条路径,别移除策略。这条策略的目标是正确的,问题只在“失败后追最新 token”的判定变量用错了。 我没在这里提交代码修改;另外本地 |
The rotation follow-up compared candidate tokens against the token the call started with, so a failure while following a newer cross-tab token fell through to the outer handler, which read that same failed token as untried and refreshed with it a second time. Track the tokens tried within a call, and fall back to the pair the successful refresh already produced instead of discarding it. Following a rejected newer token no longer signs the user out while a usable session is in hand. Refs 1024XEngineer#158
补齐前端认证 API 与全局会话状态,让业务请求在 access token 失效后能自行续期重放,为后续的账号界面与路由守卫提供统一登录态。
Closes #158
Why
后端鉴权中间件的白名单只放行
/auth/*与文档、健康检查,其余接口一律要求有效的 access token。前端目前没有任何模块持有会话,shared/api预留的registerApiAccessTokenProvider至今无人注册,所有需要鉴权的请求都会被拒。会话层不能只有一条定时续期路径。标签页被挂起时定时器不会准时触发,醒来后 access token 已过期,应用却仍停留在已登录状态,用户看到的是一连串失败的请求。
/auth/refresh又是轮换式的,两个标签页各自续期会互相使对方的 refresh token 失效。这两种情况在日常使用中都很常见。Change Description
entities/user:八个认证端点的接口契约与 HTTP 适配器,以及前端领域层的稳定用户模型。features/auth-session:booting、guest、authenticated 三态会话 Provider,覆盖启动恢复、登录、注册、登出、改密后失效与提前续期。localStorage;存储不可用时降级为仅内存会话。code=401时尝试续期并最多重放一次;认证接口自身关闭该机制,避免 refresh 的 401 递归。AuthSessionProvider。本条不含任何界面与路由守卫。Implementation Approach
实现参考已关闭的 PR #147,会话分层与存储思路来自该实现,原作者 xyh202131。本分支没有 cherry-pick 其提交,而是按当前后端契约重写:实体契约与端点调用形状大量沿用,会话核心与存储层重新实现,并补齐响应式续期、跨标签页同步与并发保护。
唯一需要单独说明的判断依据:登录失效看的是响应封套里的
code === 401,不是 HTTP 状态码——后端的 401 与 429 都以 HTTP 200 返回。请求层只在 HTTP 200 且业务码为 401 时调用恢复器,重放至多一次;恢复失败、请求体不可重放、或第二次仍是 401,都保留原始错误。其余的并发去重、generation 防陈旧与跨标签页策略见代码内注释。Screenshots
本条不含可见 UI,合入后界面没有变化。可见部分从 #159 开始。
Testing
Node.js v24.18.0,在
frontend/下依次执行:npm run format:checknpm run lintnpm run typechecknpm run testnpm run build跨标签页路径由 jsdom 的
StorageEvent覆盖,没有做双标签页的真实浏览器验证;后端接口以本地 fixture 模拟,没有对接真实服务。Follow-ups
/auth/login-by-code自动建号时响应无标志位,feat: 实现登录与注册界面(Refs #157) #159 的告知只能靠条件推断。ProjectPageQuery.ownerId属于 PR feat(project-create): add the project creation page #142 的清理范围,本分支未碰entities/project。Related
#157(产品口径)、PR #75(后端)、PR #147(起点)、#153 与 #156(后端待修)。后续 #159、#160、#161 依赖本条。