refactor(auth): replace dashboard sessions with stateless tokens and session control - #6329
Conversation
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
|
Important Review skippedToo many files! This PR contains 1152 files, which is 852 over the limit of 300. To get a review, narrow the scope: Usage-priced reviews support at most 300 files. ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (18)
📒 Files selected for processing (1587)
You can disable this status message by setting the WalkthroughThe pull request introduces token-based authentication, database-backed login sessions, one-time auth flows, security proofs, auth-version cache fencing, fixed-window rate limiting, OAuth/Passkey/Telegram flow updates, frontend session coordination, login-session management UI, expanded API documentation, and related configuration guidance. ChangesAuthentication and session platform
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant Browser
participant AuthAPI
participant AuthSessionService
participant UserSessionStore
participant Redis
Browser->>AuthAPI: POST login or refresh
AuthAPI->>AuthSessionService: create or rotate auth session
AuthSessionService->>UserSessionStore: validate, persist, or revoke session
AuthSessionService->>Redis: publish session cache or deny fence
AuthAPI-->>Browser: auth bundle and refresh-cookie state
Possibly related PRs
Suggested labels: Suggested reviewers: Poem
✨ Finishing Touches🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
Actionable comments posted: 9
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
middleware/auth.go (1)
70-83: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
TryUserAuthshould fall back to anonymous on auth errors
/api/oauth/state,/api/oauth/:provider, and the header-nav optional-auth paths all use this middleware. If a client sends a stale or unrelatedAuthorizationtoken,authenticateDashboardRequestaborts the request instead of continuing anonymously, which breaks public OAuth flows. Treat auth failures here as “no auth” and only abort on clearly dashboard-authenticated requests.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@middleware/auth.go` around lines 70 - 83, The TryUserAuth middleware should treat authentication failures from authenticateDashboardRequest as anonymous access for optional-auth routes. Update the err handling in TryUserAuth so it only writes the dashboard auth error and returns when the request is clearly intended to use dashboard authentication; otherwise continue to c.Next() without setting auth context. Preserve successful authentication behavior.web/default/src/features/auth/sign-in/components/user-auth-form.tsx (1)
168-189: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winBare
catchswallows the newly-added local validation errors.The two new
throw new Error(...)calls (missingflow_token, invalid auth bundle) are local, synchronous throws — not Axios/network errors — so the global response interceptor never sees them. The barecatch { // Errors are handled by global interceptor }discards them silently: the user gets no toast and no navigation, just a reset spinner.handlePasskeyLogin's catch block (below, lines 291-299) already shows the correct pattern for surfacing these.🐛 Proposed fix: surface local throws like the passkey handler does
- } catch { - // Errors are handled by global interceptor - } finally { + } catch (error) { + if (getServerErrorMessageKey(error)) return + if (error instanceof Error) { + toast.error(error.message) + } + } finally { setIsLoading(false) }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/default/src/features/auth/sign-in/components/user-auth-form.tsx` around lines 168 - 189, Update the catch block in the sign-in handler around isAuthBundle and handleLoginSuccess so locally thrown validation errors for a missing flow_token or invalid auth bundle are surfaced to the user, following the existing error-handling pattern in handlePasskeyLogin. Preserve global interceptor handling for request errors while ensuring these local failures produce the same toast or navigation behavior as the passkey flow.web/default/src/features/auth/hooks/use-auth-redirect.ts (1)
25-40: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
getSavedLanguageignores object-typeduser.setting.
AuthUser.settingis typed asRecord<string, unknown> | string, but this function only readssetting.languagewhensettingis a string; if the backend ever returnssettingas an already-parsed object, saved-language restoration silently fails.🐛 Proposed fix: handle both shapes
- if (typeof user.setting !== 'string') { - return undefined - } - - try { - const setting = JSON.parse(user.setting) as { language?: unknown } - return typeof setting.language === 'string' ? setting.language : undefined - } catch { - return undefined - } + if (typeof user.setting === 'string') { + try { + const setting = JSON.parse(user.setting) as { language?: unknown } + return typeof setting.language === 'string' ? setting.language : undefined + } catch { + return undefined + } + } + + if (user.setting && typeof user.setting === 'object') { + const language = (user.setting as Record<string, unknown>).language + return typeof language === 'string' ? language : undefined + } + + return undefined🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/default/src/features/auth/hooks/use-auth-redirect.ts` around lines 25 - 40, Update getSavedLanguage to support both AuthUser.setting shapes: continue parsing string values as JSON, but also read language directly from object-valued settings. Return the language only when it is a string, and preserve undefined for missing, invalid, or unsupported values.
🧹 Nitpick comments (4)
web/default/src/routes/oauth/$provider.tsx (1)
149-161: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoffFallback bypasses SPA routing via direct
window.location.replace.Per path instructions for
web/default/src/routes/**/*.tsx, navigation should go throughuseNavigate/Link, notwindow.location. This fallback triggers a full-page reload/history bypass whenever the router doesn't visibly commit within 100ms, which can mask a real navigation bug rather than fix it. Worth confirming why the fallback is needed here and whether a router-native check (e.g. inspectingrouter.state) is preferable.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/default/src/routes/oauth/`$provider.tsx around lines 149 - 161, The safeNavigate fallback in the OAuth route bypasses SPA routing with window.location.replace. Remove the timeout-based direct reload and keep navigation exclusively through the existing navigate function, or replace the fallback with a router-native state check if fallback behavior is required; update safeNavigate accordingly without using window.location.Source: Path instructions
web/default/src/routes/__root.tsx (1)
83-86: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse router navigation instead of
window.location.replacefor the sign-out redirect.
window.location.replace('/sign-in')performs a hard navigation that bypasses TanStack Router. PreferuseNavigate()(or a redirect) so route state, guards, and in-app transitions behave consistently. A full reload is understandable for the cross-tab reset on Line 79, but the/sign-inredirect is a normal route change.As per coding guidelines: "For TanStack Router UI routes, use
useNavigateorLinkfor navigation instead of manipulatingwindow.locationdirectly."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/default/src/routes/__root.tsx` around lines 83 - 86, Replace the hard navigation in the currentSID sign-out branch with TanStack Router navigation via useNavigate, while preserving the existing clearAuthentication(false) behavior and redirecting to /sign-in. Leave the separate cross-tab reset handling unchanged.Source: Coding guidelines
web/default/src/lib/http-client.ts (1)
44-50: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winConsider adding a request timeout to the shared Axios instance.
No
timeoutis configured, so a stalled request could hang indefinitely for the user with no automatic recovery.export const api = axios.create({ baseURL: '', withCredentials: true, + timeout: 30000, headers: { 'Cache-Control': 'no-store', }, })🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/default/src/lib/http-client.ts` around lines 44 - 50, Add a finite timeout setting to the shared Axios instance created by api, using the project's established timeout configuration or a reasonable default, so stalled requests terminate automatically while preserving the existing credentials and headers.web/default/src/features/auth/secure-verification/api.ts (1)
108-121: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winType the
/api/verifyresponse instead of relying on an implicitany.
api.post('/api/verify', ...)has no generic type argument, sores.datais effectivelyany; the finalreturn res.data.data as SecurityProofis an unchecked cast. Typing the call (e.g.api.post<ApiResponse<SecurityProof>>(...)) removes the need for the cast and gives compile-time safety consistent withverifyPasskey's typed helpers.As per coding guidelines, `web/default/**/*.{ts,tsx}`: "Avoid `any`; prefer concrete types or `unknown`, explicitly type parameters and return values".♻️ Proposed fix
- const res = await api.post('/api/verify', { + const res = await api.post<{ success: boolean; message?: string; data?: SecurityProof }>('/api/verify', { method: '2fa', code: trimmed, scope, }) if (!res.data?.success) { throw new Error(res.data?.message || i18next.t('Verification failed')) } - if (!res.data.data?.proof_token) { + if (!res.data.data?.proof_token) { throw new Error(i18next.t('Verification proof was not returned')) } - return res.data.data as SecurityProof + return res.data.data🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/default/src/features/auth/secure-verification/api.ts` around lines 108 - 121, Update the api.post call in the verification function to provide the appropriate ApiResponse<SecurityProof> generic, then return the typed response data directly without the unchecked SecurityProof cast. Preserve the existing success, error-message, and proof_token validation behavior.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@middleware/model-rate-limit.go`:
- Around line 45-51: Update the nowTime construction in the rate-limit
comparison flow to format the current time in UTC before parsing it with
modelRateLimitTimeFormat. Preserve the existing parsing and nowTime.Sub(oldTime)
behavior, ensuring generated timestamps are timezone-independent across service
instances.
In `@model/user_session.go`:
- Around line 47-49: Change the GORM type for PreviousRefreshHash in the session
model from char(64) to varchar(64), preserving its existing nullable and JSON
configuration so empty-string sentinel checks remain consistent across
databases.
In `@web/default/src/components/sign-out-dialog.tsx`:
- Around line 47-49: Update the sign-out dialog component to use TanStack Router
navigation: import and initialize useNavigate, then replace the
window.location.replace call with the router navigate action targeting /sign-in
while preserving the existing sign-out flow.
- Around line 47-49: Replace the direct window.location.replace call in the
sign-out dialog component with TanStack Router navigation. Import and invoke
useNavigate within the component, then navigate to /sign-in through the returned
navigate function while preserving the existing sign-out flow.
In
`@web/default/src/features/auth/secure-verification/hooks/use-secure-verification.ts`:
- Around line 151-153: Update the missing-scope error in the secure verification
hook to use the existing i18next translation mechanism, passing the translated
message to Error so executeVerification’s toast displays localized text. Follow
the hook’s established useTranslation/t pattern and preserve the current error
condition.
In `@web/default/src/i18n/locales/fr.json`:
- Line 4555: Update the French translation for “This session will lose access
immediately and must sign in again.” so the user is the subject of
re-authentication, using wording equivalent to “Cette session perdra
immédiatement l’accès ; vous devrez vous reconnecter.”
In `@web/default/src/i18n/locales/ru.json`:
- Line 3875: Update the Russian translation for “Review and sign out devices
currently using your account.” to use explicit session sign-out/termination
terminology instead of wording that implies disabling or disconnecting the
physical devices.
In `@web/default/src/routes/oauth/`$provider.tsx:
- Around line 193-206: In the OAuth error catch block, update the condition
around toast.error so a mapped messageKey displays i18next.t(messageKey),
matching the equivalent branch above; retain the existing responseMessage and
Error-message fallbacks only when no mapped key is available.
- Around line 81-96: Update the Telegram branch in the OAuth route to handle
telegram_bind values other than “success” explicitly, posting a
TELEGRAM_BIND_RESULT_MESSAGE with success false and the available failure
details before closing the window and returning. Keep the existing
success-message behavior unchanged, and prevent Telegram bind failures from
falling through to the generic OAuth callback path.
---
Outside diff comments:
In `@middleware/auth.go`:
- Around line 70-83: The TryUserAuth middleware should treat authentication
failures from authenticateDashboardRequest as anonymous access for optional-auth
routes. Update the err handling in TryUserAuth so it only writes the dashboard
auth error and returns when the request is clearly intended to use dashboard
authentication; otherwise continue to c.Next() without setting auth context.
Preserve successful authentication behavior.
In `@web/default/src/features/auth/hooks/use-auth-redirect.ts`:
- Around line 25-40: Update getSavedLanguage to support both AuthUser.setting
shapes: continue parsing string values as JSON, but also read language directly
from object-valued settings. Return the language only when it is a string, and
preserve undefined for missing, invalid, or unsupported values.
In `@web/default/src/features/auth/sign-in/components/user-auth-form.tsx`:
- Around line 168-189: Update the catch block in the sign-in handler around
isAuthBundle and handleLoginSuccess so locally thrown validation errors for a
missing flow_token or invalid auth bundle are surfaced to the user, following
the existing error-handling pattern in handlePasskeyLogin. Preserve global
interceptor handling for request errors while ensuring these local failures
produce the same toast or navigation behavior as the passkey flow.
---
Nitpick comments:
In `@web/default/src/features/auth/secure-verification/api.ts`:
- Around line 108-121: Update the api.post call in the verification function to
provide the appropriate ApiResponse<SecurityProof> generic, then return the
typed response data directly without the unchecked SecurityProof cast. Preserve
the existing success, error-message, and proof_token validation behavior.
In `@web/default/src/lib/http-client.ts`:
- Around line 44-50: Add a finite timeout setting to the shared Axios instance
created by api, using the project's established timeout configuration or a
reasonable default, so stalled requests terminate automatically while preserving
the existing credentials and headers.
In `@web/default/src/routes/__root.tsx`:
- Around line 83-86: Replace the hard navigation in the currentSID sign-out
branch with TanStack Router navigation via useNavigate, while preserving the
existing clearAuthentication(false) behavior and redirecting to /sign-in. Leave
the separate cross-tab reset handling unchanged.
In `@web/default/src/routes/oauth/`$provider.tsx:
- Around line 149-161: The safeNavigate fallback in the OAuth route bypasses SPA
routing with window.location.replace. Remove the timeout-based direct reload and
keep navigation exclusively through the existing navigate function, or replace
the fallback with a router-native state check if fallback behavior is required;
update safeNavigate accordingly without using window.location.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: cb1416c9-cd85-41d5-85a8-723d9dc80f3c
⛔ Files ignored due to path filters (1)
go.sumis excluded by!**/*.sum
📒 Files selected for processing (135)
.env.exampleREADME.en.mdREADME.fr.mdREADME.ja.mdREADME.mdREADME.zh_CN.mdREADME.zh_TW.mdTHIRD-PARTY-LICENSES.mdcommon/constants.gocommon/init.gocommon/session_cookie.gocommon/sys_log.gocommon/url_validator_test.gocommon/user_session_test.gocontroller/auth_flow_test.gocontroller/auth_session.gocontroller/auth_session_test.gocontroller/model_list_test.gocontroller/oauth.gocontroller/passkey.gocontroller/passkey_test.gocontroller/secure_verification.gocontroller/telegram.gocontroller/telegram_test.gocontroller/twofa.gocontroller/user.gocontroller/user_manage_test.gocontroller/wechat.godocker-compose.dev.ymldocker-compose.ymldocs/authentication.mddocs/openapi/api.jsondocs/openapi/relay.jsongo.modmain.gomiddleware/auth.gomiddleware/auth_origin.gomiddleware/auth_origin_test.gomiddleware/auth_test.gomiddleware/email-verification-rate-limit.gomiddleware/header_nav_test.gomiddleware/model-rate-limit.gomiddleware/rate-limit.gomiddleware/rate_limit_test.gomiddleware/secure_verification.gomiddleware/turnstile-check.gomodel/auth_flow.gomodel/auth_flow_test.gomodel/errors.gomodel/external_identity_claim.gomodel/external_identity_claim_test.gomodel/main.gomodel/passkey.gomodel/subscription.gomodel/subscription_auth_test.gomodel/task_cas_test.gomodel/twofa.gomodel/user.gomodel/user_auth_cache.gomodel/user_authentication_test.gomodel/user_cache.gomodel/user_cache_auth_version_test.gomodel/user_session.gomodel/user_session_test.gorouter/api-router.goservice/auth_cleanup.goservice/auth_session.goservice/auth_session_test.goservice/auth_token.goservice/auth_token_test.goservice/passkey/service.goservice/passkey/session.gotrusted_proxies.gotrusted_proxies_test.goweb/default/src/components/sign-out-dialog.tsxweb/default/src/features/auth/api.test.tsweb/default/src/features/auth/api.tsweb/default/src/features/auth/constants.tsweb/default/src/features/auth/hooks/use-auth-redirect.tsweb/default/src/features/auth/hooks/use-oauth-login.tsweb/default/src/features/auth/index.tsweb/default/src/features/auth/lib/oauth-bind-window.test.tsweb/default/src/features/auth/lib/oauth-bind-window.tsweb/default/src/features/auth/lib/storage.tsweb/default/src/features/auth/otp/components/otp-form.tsxweb/default/src/features/auth/passkey/api.tsweb/default/src/features/auth/passkey/hooks/use-passkey-management.tsweb/default/src/features/auth/passkey/types.tsweb/default/src/features/auth/secure-verification/api.tsweb/default/src/features/auth/secure-verification/hooks/use-secure-verification.tsweb/default/src/features/auth/secure-verification/types.tsweb/default/src/features/auth/sign-in/components/user-auth-form.tsxweb/default/src/features/auth/sign-up/components/sign-up-form.tsxweb/default/src/features/auth/types.tsweb/default/src/features/channels/api.tsweb/default/src/features/channels/components/dialogs/ollama-models-dialog.tsxweb/default/src/features/channels/components/drawers/channel-mutate-drawer.tsxweb/default/src/features/playground/hooks/use-chat-handler.tsweb/default/src/features/playground/hooks/use-stream-request.test.tsweb/default/src/features/playground/hooks/use-stream-request.tsweb/default/src/features/profile/api.tsweb/default/src/features/profile/components/dialogs/delete-account-dialog.tsxweb/default/src/features/profile/components/dialogs/telegram-bind-dialog.tsxweb/default/src/features/profile/components/login-session-dialogs.tsxweb/default/src/features/profile/components/login-session-item.tsxweb/default/src/features/profile/components/login-session-utils.test.tsweb/default/src/features/profile/components/login-session-utils.tsweb/default/src/features/profile/components/login-sessions-card.tsxweb/default/src/features/profile/components/passkey-card.tsxweb/default/src/features/profile/components/tabs/account-bindings-tab.tsxweb/default/src/features/profile/index.tsxweb/default/src/i18n/locales/en.jsonweb/default/src/i18n/locales/fr.jsonweb/default/src/i18n/locales/ja.jsonweb/default/src/i18n/locales/ru.jsonweb/default/src/i18n/locales/vi.jsonweb/default/src/i18n/locales/zh-TW.jsonweb/default/src/i18n/locales/zh.jsonweb/default/src/i18n/static-keys.tsweb/default/src/lib/api.tsweb/default/src/lib/auth-session-sync.tsweb/default/src/lib/auth-session.test.tsweb/default/src/lib/auth-session.tsweb/default/src/lib/handle-server-error.tsweb/default/src/lib/http-client.tsweb/default/src/lib/oauth.tsweb/default/src/lib/secure-verification.tsweb/default/src/lib/server-error-message.test.tsweb/default/src/lib/server-error-message.tsweb/default/src/main.tsxweb/default/src/routes/(auth)/oauth.tsxweb/default/src/routes/__root.tsxweb/default/src/routes/_authenticated/route.tsxweb/default/src/routes/oauth/$provider.tsxweb/default/src/stores/auth-store.ts
💤 Files with no reviewable changes (3)
- web/default/src/features/auth/lib/storage.ts
- service/passkey/service.go
- web/default/src/lib/oauth.ts
| } catch (error: unknown) { | ||
| const messageKey = getServerErrorMessageKey(error) | ||
| const responseMessage = ( | ||
| error as { response?: { data?: { message?: string } } } | ||
| ).response?.data?.message | ||
| if (!messageKey) { | ||
| toast.error( | ||
| responseMessage || | ||
| (error instanceof Error | ||
| ? error.message | ||
| : i18next.t('OAuth failed')) | ||
| ) | ||
| } | ||
| await handleLoginFailure(message) | ||
| return | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Inverted condition suppresses the toast exactly when a mapped error message is available.
if (!messageKey) means that whenever getServerErrorMessageKey returns a valid key — the most informative case — no toast.error is shown at all; the user is silently redirected to sign-in. This contradicts the equivalent (correct) branch above at Line 187-192, which shows i18next.t(messageKey) when messageKey is present.
🐛 Proposed fix
} catch (error: unknown) {
const messageKey = getServerErrorMessageKey(error)
const responseMessage = (
error as { response?: { data?: { message?: string } } }
).response?.data?.message
- if (!messageKey) {
- toast.error(
- responseMessage ||
- (error instanceof Error
- ? error.message
- : i18next.t('OAuth failed'))
- )
- }
+ toast.error(
+ messageKey
+ ? i18next.t(messageKey)
+ : responseMessage ||
+ (error instanceof Error
+ ? error.message
+ : i18next.t('OAuth failed'))
+ )
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| } catch (error: unknown) { | |
| const messageKey = getServerErrorMessageKey(error) | |
| const responseMessage = ( | |
| error as { response?: { data?: { message?: string } } } | |
| ).response?.data?.message | |
| if (!messageKey) { | |
| toast.error( | |
| responseMessage || | |
| (error instanceof Error | |
| ? error.message | |
| : i18next.t('OAuth failed')) | |
| ) | |
| } | |
| await handleLoginFailure(message) | |
| return | |
| } | |
| } catch (error: unknown) { | |
| const messageKey = getServerErrorMessageKey(error) | |
| const responseMessage = ( | |
| error as { response?: { data?: { message?: string } } } | |
| ).response?.data?.message | |
| toast.error( | |
| messageKey | |
| ? i18next.t(messageKey) | |
| : responseMessage || | |
| (error instanceof Error | |
| ? error.message | |
| : i18next.t('OAuth failed')) | |
| ) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@web/default/src/routes/oauth/`$provider.tsx around lines 193 - 206, In the
OAuth error catch block, update the condition around toast.error so a mapped
messageKey displays i18next.t(messageKey), matching the equivalent branch above;
retain the existing responseMessage and Error-message fallbacks only when no
mapped key is available.
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@model/user_session_migration_test.go`:
- Around line 70-72: Update
TestUserSessionPreviousRefreshHashSchemaUsesNullableVarchar to initialize a
local in-memory SQLite database and use it when constructing the gorm.Statement,
replacing the global DB dependency while preserving the existing schema
assertion.
In `@web/default/src/features/auth/lib/auth-redirect.test.ts`:
- Around line 19-20: Replace the node:assert/strict and node:test usage in
web/default/src/features/auth/lib/auth-redirect.test.ts lines 19-20 with Vitest
imports or globals, and convert its assertions to expect. In
web/default/src/features/auth/lib/oauth-bind-window.test.ts lines 53-55, migrate
test blocks to Vitest and replace assert.deepEqual calls with
expect(...).toEqual(...).
In `@web/default/src/i18n/locales/vi.json`:
- Line 4502: Update the Vietnamese translation for the key "The Telegram
authorization request is invalid or expired." to use “ủy quyền” for
authorization instead of “xác thực”, while preserving the existing
invalid-or-expired meaning.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: a53d0345-7489-4f9b-a7a7-94bb2d0773f0
📒 Files selected for processing (40)
controller/telegram.gocontroller/telegram_test.godocs/authentication.mddocs/openapi/api.jsonmiddleware/auth.gomiddleware/auth_test.gomiddleware/header_nav_test.gomiddleware/model-rate-limit.gomiddleware/model_rate_limit_test.gomodel/user_session.gomodel/user_session_migration_test.gomodel/user_session_test.goweb/default/src/components/sign-out-dialog.tsxweb/default/src/features/auth/hooks/use-auth-redirect.tsweb/default/src/features/auth/lib/auth-redirect.test.tsweb/default/src/features/auth/lib/auth-redirect.tsweb/default/src/features/auth/lib/oauth-bind-window.test.tsweb/default/src/features/auth/lib/oauth-bind-window.tsweb/default/src/features/auth/secure-verification/api.tsweb/default/src/features/auth/secure-verification/hooks/use-secure-verification.tsweb/default/src/features/auth/sign-in/components/user-auth-form.tsxweb/default/src/features/profile/components/dialogs/telegram-bind-dialog.tsxweb/default/src/features/profile/components/login-sessions-card.tsxweb/default/src/i18n/locales/en.jsonweb/default/src/i18n/locales/fr.jsonweb/default/src/i18n/locales/ja.jsonweb/default/src/i18n/locales/ru.jsonweb/default/src/i18n/locales/vi.jsonweb/default/src/i18n/locales/zh-TW.jsonweb/default/src/i18n/locales/zh.jsonweb/default/src/i18n/static-keys.tsweb/default/src/lib/api.tsweb/default/src/lib/auth-session.test.tsweb/default/src/lib/auth-session.tsweb/default/src/lib/server-error-message.test.tsweb/default/src/lib/server-error-message.tsweb/default/src/routes/(auth)/oauth.tsxweb/default/src/routes/(auth)/sign-in.tsxweb/default/src/routes/__root.tsxweb/default/src/routes/oauth/$provider.tsx
🚧 Files skipped from review as they are similar to previous changes (24)
- web/default/src/lib/server-error-message.ts
- web/default/src/lib/server-error-message.test.ts
- web/default/src/components/sign-out-dialog.tsx
- middleware/header_nav_test.go
- middleware/model-rate-limit.go
- web/default/src/features/profile/components/login-sessions-card.tsx
- web/default/src/features/auth/secure-verification/hooks/use-secure-verification.ts
- web/default/src/lib/auth-session.test.ts
- web/default/src/features/auth/hooks/use-auth-redirect.ts
- web/default/src/features/auth/sign-in/components/user-auth-form.tsx
- web/default/src/routes/(auth)/oauth.tsx
- web/default/src/features/profile/components/dialogs/telegram-bind-dialog.tsx
- web/default/src/lib/auth-session.ts
- web/default/src/routes/__root.tsx
- docs/authentication.md
- web/default/src/lib/api.ts
- web/default/src/i18n/locales/en.json
- model/user_session_test.go
- web/default/src/routes/oauth/$provider.tsx
- web/default/src/i18n/locales/fr.json
- web/default/src/features/auth/secure-verification/api.ts
- controller/telegram.go
- docs/openapi/api.json
- model/user_session.go
| func TestUserSessionPreviousRefreshHashSchemaUsesNullableVarchar(t *testing.T) { | ||
| statement := &gorm.Statement{DB: DB} | ||
| require.NoError(t, statement.Parse(&UserSession{})) |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Initialize a local dummy database to avoid nil pointer panics.
Using the global DB variable directly can cause a nil pointer dereference panic during statement.Parse() if this test is executed in isolation or runs before DB is initialized by other tests.
Initialize a local, in-memory SQLite database to decouple this schema test from global state.
🐛 Proposed fix
func TestUserSessionPreviousRefreshHashSchemaUsesNullableVarchar(t *testing.T) {
- statement := &gorm.Statement{DB: DB}
+ dummyDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
+ require.NoError(t, err)
+ statement := &gorm.Statement{DB: dummyDB}
require.NoError(t, statement.Parse(&UserSession{}))📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| func TestUserSessionPreviousRefreshHashSchemaUsesNullableVarchar(t *testing.T) { | |
| statement := &gorm.Statement{DB: DB} | |
| require.NoError(t, statement.Parse(&UserSession{})) | |
| func TestUserSessionPreviousRefreshHashSchemaUsesNullableVarchar(t *testing.T) { | |
| dummyDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) | |
| require.NoError(t, err) | |
| statement := &gorm.Statement{DB: dummyDB} | |
| require.NoError(t, statement.Parse(&UserSession{})) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@model/user_session_migration_test.go` around lines 70 - 72, Update
TestUserSessionPreviousRefreshHashSchemaUsesNullableVarchar to initialize a
local in-memory SQLite database and use it when constructing the gorm.Statement,
replacing the global DB dependency while preserving the existing schema
assertion.
| import assert from 'node:assert/strict' | ||
| import { describe, test } from 'node:test' |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Use Vitest for frontend unit tests.
Both test files violate the coding guideline by relying on Node's built-in node:test and node:assert modules instead of Vitest. As per coding guidelines, "Write unit tests for utility functions and pure logic with Vitest."
web/default/src/features/auth/lib/auth-redirect.test.ts#L19-L20: Replacenode:assert/strictandnode:testimports withvitest(or rely on Vitest globals) and update assertions to useexpect.web/default/src/features/auth/lib/oauth-bind-window.test.ts#L53-L55: Migratetestblocks andassert.deepEqualcalls to Vitest'stestandexpect(...).toEqual(...).
📍 Affects 2 files
web/default/src/features/auth/lib/auth-redirect.test.ts#L19-L20(this comment)web/default/src/features/auth/lib/oauth-bind-window.test.ts#L53-L55
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@web/default/src/features/auth/lib/auth-redirect.test.ts` around lines 19 -
20, Replace the node:assert/strict and node:test usage in
web/default/src/features/auth/lib/auth-redirect.test.ts lines 19-20 with Vitest
imports or globals, and convert its assertions to expect. In
web/default/src/features/auth/lib/oauth-bind-window.test.ts lines 53-55, migrate
test blocks to Vitest and replace assert.deepEqual calls with
expect(...).toEqual(...).
Source: Coding guidelines
| @@ -4463,6 +4499,7 @@ | |||
| "The site is not available at the moment.": "Trang web hiện không khả dụng.", | |||
| "The slug is appended to the URL:": "Slug được gắn vào URL:", | |||
| "The sync will fetch missing models and vendors from the selected source. Existing records are updated only when you approve conflicts.": "Đồng bộ hóa sẽ tìm nạp các mẫu và nhà cung cấp còn thiếu từ nguồn đã chọn. Các bản ghi hiện có chỉ được cập nhật khi bạn chấp thuận các xung đột.", | |||
| "The Telegram authorization request is invalid or expired.": "Yêu cầu xác thực Telegram không hợp lệ hoặc đã hết hạn.", | |||
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Translate “authorization” as “ủy quyền”.
This message concerns a Telegram authorization request, not user authentication or verification. Using “xác thực” can mislead users about the failed flow.
Proposed fix
- "The Telegram authorization request is invalid or expired.": "Yêu cầu xác thực Telegram không hợp lệ hoặc đã hết hạn.",
+ "The Telegram authorization request is invalid or expired.": "Yêu cầu ủy quyền Telegram không hợp lệ hoặc đã hết hạn.",📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "The Telegram authorization request is invalid or expired.": "Yêu cầu xác thực Telegram không hợp lệ hoặc đã hết hạn.", | |
| "The Telegram authorization request is invalid or expired.": "Yêu cầu ủy quyền Telegram không hợp lệ hoặc đã hết hạn.", |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@web/default/src/i18n/locales/vi.json` at line 4502, Update the Vietnamese
translation for the key "The Telegram authorization request is invalid or
expired." to use “ủy quyền” for authorization instead of “xác thực”, while
preserving the existing invalid-or-expired meaning.
同步上游 10 个提交(至 1721144),重点整合: - QuantumNous#6329 鉴权重构:dashboard 会话全面改为无状态 token(access/refresh + 版本栅栏 + 会话管理),gin session 全部移除。fork 侧适配: - turnstile 一次性消费改为按 token 键控内存缓存(兼容发码+注册两步流) - TRUSTED_PROXY_CIDRS 作为 TRUSTED_PROXIES 的兼容别名保留 - UserBase/ToBaseUser 保留 ParentId(子号计费),补 AuthVersion/CacheSchema - OAuth 绑定改 flow_token 流,保留 GitHub 账号年龄门禁(消费 flow 后校验) - RecordUserIP 反欺诈埋点移入 setupLoginAtAuthVersion - 子号/代理鉴权门(SubPermission/RejectSubAccount/AgentAuth)原样保留 - profile 嫁接上游 LoginSessionsCard(会话管理 UI),绑定卡接入 popup+postMessage 新绑定机制 - 2FA/OAuth/微信登录后 redirect 目标经 handleLoginSuccess 传递恢复 - web/default → web/ 扁平化 + 删除 classic 主题:fork 全部前端定制 (agent/supplier/detector/sub-account 等 180+ 文件)迁移至新路径, 保留 fork 的 i18n 按需加载、每表分页记忆、主题调校与设计系统 - QuantumNous#6157 渠道代理客户端重构(别名缓存+失效清理),保留 fork 全局代理 与 RELAY_DISABLE_HTTP2;QuantumNous#6074 suno CAS 防重复退款;QuantumNous#6163 playground 自动分组;QuantumNous#6224 无限额度密钥显示已用量;QuantumNous#6032 realtime GA 去 beta 头 - 语言文件三方合并:fork ~6280 键 + 上游新增 55 键鉴权文案 - fork 刻意删除的组件与 workflows 维持删除(上次合并曾误恢复) 验证:go build/test 全绿,前端 tsgo 类型检查与 rsbuild 构建通过。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
采用上游 QuantumNous#6329 的两栏结构:主列(minmax(0,1fr))放绑定/通知/存储/ 语言/安全/登录会话,右侧 360px 粘性侧栏放签到与侧栏配置,长页滚动 时侧栏保持可见;外层接入 CardStagger 结构与上游对齐。签到与侧栏 配置都关闭时退化为单列,避免空轨占位。fork 自有卡片全部保留 (Passkey/2FA 在安全卡内,故侧栏不再单列这两项)。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- 恢复上游 PasskeyCard/TwoFACard(适配本库 StatusBadge children API, danger→destructive)及其依赖 ui/icon-badge,置于个人页右侧粘性侧栏, 与上游 QuantumNous#6329 布局一致;安全卡随之移除内嵌的 Passkey/2FA 行, 删除不再使用的 passkey-row/two-fa-row - 移动端密钥卡改为上游行式版式(名称+状态 / 密钥+行操作 / 额度), 并因此补上了移动端此前缺失的行操作菜单;外层容器仍走统一的 DataTable mobile-card-list(含禁用置灰) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
合并 QuantumNous/new-api 至 1721144(QuantumNous#6329 无状态鉴权/session control、 代理客户端缓存生命周期、Suno CAS 退款、playground/UI 修复等)。 冲突处理: - Dockerfile 跟随上游单前端构建;移除 classic 主题 - web/default -> web/src 路径重排后保留定制功能(社区监控/同步/签到机器人、 维护响应、响应过滤、intercept/server monitor) - i18n 语言包双向合并;routeTree 重新生成 - 保留本地 Claude Responses->Chat usage 终态修复 验证:go test relay/channel/openai + middleware + controller 通过; web bun build:check 通过;go build 通过。service 包既有定制测试失败未在 本次合并引入修复范围内。
Sync 14 upstream commits. The dominant change is structural: upstream promoted the frontend from web/default to the web/ root and deleted the classic theme entirely, so most of the diff is renames and deletions. Notable upstream work: - refactor(auth): stateless dashboard tokens replacing sessions (QuantumNous#6329) - feat(channel): upstream model discovery for Codex and advanced custom channels (QuantumNous#6184, QuantumNous#5971) - fix: CAS status update prevents duplicate suno task refunds (QuantumNous#6074) - fix: no duplicate tool calls in Responses-to-Chat streaming (QuantumNous#6225) Fork-side resolutions: - Drop the classic theme, following upstream. electron/ and the release/electron-build workflows stay deleted as this fork already removed them; GHCR publishing continues via docker-build.yml. - UserBase keeps the fork's per-user Ratio alongside upstream's new Role, AuthVersion and CacheSchema fields. GetUserCache adopts upstream's cache-population path, which returns ToBaseUser() and so still carries Ratio. - web-router keeps the fork's dynamic index injector (SystemName/Logo templating) on top of upstream's renamed frontendFS. serveIndex collapses to the single-frontend WebAssets now that classic is gone. - Restore the fork's invoices feature (5 files), which git's rename detection dropped during the web/default -> web/ move, and re-register its route in routeTree.gen.ts. Backend builds and the full Go test suite passes. The frontend is not yet type-checked or built locally. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
…session control (QuantumNous#6329) * refactor(auth): replace dashboard sessions with stateless tokens * feat(auth): harden session issuance and distributed enforcement * fix(proxy): preserve trusted proxy compatibility defaults * refactor: address dashboard auth review feedback * refactor: remove classic frontend and flatten web app
The upstream QuantumNous#6329 auth-cache rewrite replaced RedisHSetObj with a Lua script that enumerates hash fields explicitly, silently dropping the fork-only Ratio field. Cache hits then read a nil Ratio (neutral 1.0), so billing flapped between groupRatio and groupRatio*userRatio depending on Redis cache hit/miss. Write Ratio through the same script, using the empty-string sentinel that RedisHGetObj already maps back to a nil pointer. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
#68) Users with 2FA could not log in on dev: the 2FA step failed with 'session expired'. rc.22 (QuantumNous#6329) replaced the server-side 2FA login session with a stateless AuthFlow token — the password step now returns data.flow_token and /api/user/login/2fa requires it in the body. The aurora frontend was still built for the old session-based flow and sent only { code }, so the backend's GetAuthFlow lookup failed. - Capture data.flow_token from the login response and stash it in sessionStorage before navigating to the OTP page. - Send it back as flow_token on login2fa (covers both TOTP and backup code, same endpoint); clear it once verification succeeds. - Type flow_token on LoginResponse.data and TwoFAPayload. aurora build + typecheck clean (6 pre-existing warnings, no new).
Users could not stay logged in after rc.22: QuantumNous#6329 replaced the dashboard session cookie with a stateless Bearer access-token + httpOnly refresh cookie, and the backend now authenticates dashboard requests ONLY via 'Authorization: Bearer <access_token>' (no cookie fallback). Aurora still authenticated by cookie, so every post-login request was unauthenticated. Port the upstream web/ scheme into aurora: - auth-store: hold accessToken/accessExpiresAt/session + setBundle/reset. - lib/auth-session.ts (new): single-flight refresh via /api/user/auth/refresh on a dedicated interceptor-free client (no recursion), app-boot bootstrap from the refresh cookie, and clearAuthentication. Transient (5xx/429/network) errors don't force sign-out. - lib/api.ts: request interceptor injects Bearer only when we hold a dashboard token and the request has no explicit Authorization (relay/API-key calls untouched); response interceptor does single-flight refresh + one retry on 401, else clears state and redirects to /sign-in. getCommonHeaders (SSE/ playground) also carries the Bearer token. - Capture the login bundle on every path: password, 2FA (keeps flow_token), passkey, OAuth, WeChat. - __root beforeLoad awaits bootstrapAuthentication so guards see the restored session on reload. Backend session JSON verified to match the frontend bundle parser. aurora build + typecheck clean (6 pre-existing warnings, no new).
方向:整体对齐上游 QuantumNous/new-api(含 QuantumNous#6329 无状态鉴权),删除经典前端, default 作为唯一 UI;保留 fork 的业务功能。 前端: - 删除 web/classic(492 文件),仅保留 web/default 作为单一前端 - 后端服务层改为单 UI:main.go / web-router.go 嵌入并服务 web/default/dist - 不引入上游的 web/src 重构 鉴权(采纳上游 QuantumNous#6329 无状态 token): - 移除 cookie session 中间件与 store;configureTrustedProxies(补回部署代理 IP) - secure_verification / passkey 二次校验:采用上游无状态实现(非删除), 清理 fork 遗留的 session 版实现与测试 - model.User / UserBase:合并上游 AuthVersion/Role/CacheSchema + 保留 fork GroupRatios - 限流:采用上游固定窗口原子脚本 + Retry-After 保留 fork 业务功能: - 渠道成本系数(CostRatio)、渠道名删除快照 - 充值:Infini / Stripe 动态币种 - 员工归属 / 提成统计、任务退款记账(叠加上游防重复退款) - console 迁移路由、分组倍率覆盖 采纳上游:service/group、任务对账、模型/渠道修复等其余 22 个提交 测试:移除与 fork 测试 harness 不兼容的上游 SQLite/truncateTables auth 测试; 清理单 UI 后失效的主题测试。go build ./... 通过,全部测试包编译通过。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
生产 bug 修复: - GroupRatios 缓存丢失:上游 QuantumNous#6329 的 writeUserCache Lua 脚本按显式字段写 Redis hash,未包含 fork 新增的 GroupRatios,导致用户专属分组倍率在缓存下 永远读空 → 计费倍率错误。脚本补写 GroupRatios(model/user_auth_cache.go)。 - SumUsedQuota / SumEmployeeCustomerUsedQuota 统计为 0:GORM v1.25.12 起 Scan 会清零目标结构体未匹配字段,两次 Scan 复用同一 stat 时 rpm/tpm 查询把已取到 的 quota 覆盖为 0。改为独立结构体扫描再合并(model/log.go)。 - go mod tidy 误将 gorm 从 v1.25.2 升到 v1.25.12(上游与 fork 均用 v1.25.2), 1.25.12 在 MySQL 上重复 AutoMigrate 会 panic(Can't DROP uni_tokens_key)。 回退 gorm 到 v1.25.2,同时修复上面的 Scan 行为(代码修复对两版本都健壮)。 测试适配 QuantumNous#6329(fork 测试重建的旧 API → 上游无状态 API): - model harness 补迁移 UserSession/AuthFlow/ExternalIdentityClaim;service harness 补 UserSubscription;controller harness 补员工/订阅/客户相关表。 - passkey 测试改用 UpsertPasskeyCredentialWithAuthVersion/DeletePasskeyByUserIDWithAuthVersion; 删除已被上游移除的 ApplyValidatedCredential 测试;删除测的是旧 session API 的 passkey session_test / user_session 测试 / 旧 2FA(twofa_test)。 - BatchDeleteChannels 改双返回值;DeleteOldLog 已随 classic 移除,测试改用 DeleteOldLogBatch 循环;user_cache group 缓存改 RefreshUserGroupCache。 - header_nav 测试改用 users.access_token(Bearer)模拟已登录(QuantumNous#6329 无状态鉴权)。 - retired_frontend 测试:fork 保留 console 迁移路由,断言改为 True。 - session_cookie 测试:无 host URL 的错误文案对齐上游。 验证:go build ./... 通过;完整套件 87 包通过,仅 service 包剩 2 个预存在失败 (负 TTFT 钳制未实现、Rankings 快照——pre-merge 亦失败,与本次合并无关)。
上游 31d70fc (QuantumNous#6329) 引入的登录会话卡片在 18c7b25 等三次合并中被误删, 后端 /api/user/sessions 三个端点一直可用但前端无入口。"活跃会话数超限" 的报错 文案指引用户去打开 "登录会话" 页面,而该页面不存在,用户只能靠改密码或等 30 天 过期来解套。 从 upstream/main 取回四个组件与其单元测试(含 iPad UA 识别修复 b27b2b1), 补回 api.ts 的三个请求函数、profile 页的挂载点,以及七个语言各 23 条翻译。 OAuth 与日期格式串上游本就未翻译,保持回退到 key 本身的行为。
Brings in the stateless dashboard auth refactor (QuantumNous#6329), which replaces gin sessions with access tokens plus a refresh cookie, moves OAuth state into the auth_flows table with an explicit login/bind intent, adds TRUSTED_PROXIES, changes SESSION_COOKIE_SECURE semantics, and removes the classic frontend (web/default -> web). Only .gitignore conflicted; kept both the upstream go.work entries and our production deploy artifact rules. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…session control (QuantumNous#6329) * refactor(auth): replace dashboard sessions with stateless tokens * feat(auth): harden session issuance and distributed enforcement * fix(proxy): preserve trusted proxy compatibility defaults * refactor: address dashboard auth review feedback * refactor: remove classic frontend and flatten web app
Important
📝 变更描述 / Description
本次重构将面板鉴权从 Gin Session 迁移为短期 Access JWT、HttpOnly Refresh Cookie 和数据库登录会话控制面:
user_sessions作为会话状态权威来源,支持查看登录设备、撤销单个会话和撤销其他会话。auth_flows,敏感操作使用与用户、会话和用途绑定的 Security Proof。New-Api-User。SYNC_FREQUENCY的短期缓存。共享 Redis 可即时传播撤销;独立 Redis 节点会在缓存到期后回源数据库收敛。升级后旧面板 Session 会失效,用户需要重新登录。反向代理部署需要配置
TRUSTED_PROXIES。本次未修改 relay CORS 行为。🚀 变更类型 / Type of change
🔗 关联任务 / Related Issue
✅ 提交前检查项 / Checklist
Bug fix。📸 运行证明 / Proof of Work