diff --git a/server/app/domains/user/api/login_controller.py b/server/app/domains/user/api/login_controller.py index a733a39ef..2a71c5465 100644 --- a/server/app/domains/user/api/login_controller.py +++ b/server/app/domains/user/api/login_controller.py @@ -75,7 +75,9 @@ async def auto_login(db_session: Session = Depends(session)) -> LoginResponse: raise UserException(code.error, _("Failed to create default user")) logger.info("Auto login successful", extra={"user_id": user.id, "email": user.email}) - return LoginResponse(token=create_access_token(user.id), email=user.email) + return LoginResponse( + token=create_access_token(user.id), email=user.email, user_id=user.id + ) class RefreshTokenIn(BaseModel): diff --git a/server/app/model/user/user.py b/server/app/model/user/user.py index ed49fb489..955ea93ba 100644 --- a/server/app/model/user/user.py +++ b/server/app/model/user/user.py @@ -60,6 +60,7 @@ class LoginByPasswordIn(BaseModel): class LoginResponse(BaseModel): token: str email: EmailStr + user_id: int | None = None redirect_url: str | None = None diff --git a/src/store/authStore.ts b/src/store/authStore.ts index fa1421ee9..ec41f243d 100644 --- a/src/store/authStore.ts +++ b/src/store/authStore.ts @@ -46,7 +46,7 @@ interface AuthInfo { token: string; username: string; email: string; - user_id: number; + user_id?: number | null; } // auth state interface @@ -129,6 +129,23 @@ interface AuthState { checkAgentTool: (tool: string) => void; } +/** + * User id from the access token's payload. The local auto-login response + * carries no explicit user id, but the token it returns does; without an + * id every server-owned Space is filtered out client-side and the app + * degrades to an empty legacy-only workspace. + */ +const userIdFromToken = (token: string | null): number | null => { + if (!token) return null; + try { + const base64 = token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/'); + const payload = JSON.parse(atob(base64)); + return typeof payload.id === 'number' ? payload.id : null; + } catch { + return null; + } +}; + // random default model selection const getRandomDefaultModel = (): CloudModelType => { return LEGACY_DEFAULT_CLOUD_MODEL_ID; @@ -213,19 +230,20 @@ const authStore = create()( // auth related methods setAuth: ({ token, username, email, user_id }) => { + const resolvedUserId = user_id ?? userIdFromToken(token); const previousUserId = get().user_id; - if (previousUserId != null && previousUserId !== user_id) { + if (previousUserId != null && previousUserId !== resolvedUserId) { void clearAllCachedProjects(previousUserId); } - useSpaceStore.getState().resetForUser(user_id); + useSpaceStore.getState().resetForUser(resolvedUserId); set({ token, username, email, - user_id, + user_id: resolvedUserId, authEnvironmentKey: getAuthEnvironmentKey(), }); - hydrateSpacesForUser(user_id); + hydrateSpacesForUser(resolvedUserId); }, logout: () => { @@ -379,7 +397,9 @@ const authStore = create()( }), { name: 'auth-storage', - version: 10, + // Bump so migrate re-runs for existing sessions that still need the + // user-id repair; a matching version skips migrate and stays unrepaired. + version: 11, migrate: (persistedState, _version) => { const s = persistedState as | { @@ -403,7 +423,12 @@ const authStore = create()( const environmentMatches = s.authEnvironmentKey === currentEnvironmentKey; const authState = environmentMatches - ? {} + ? typeof s.token === 'string' && s.user_id == null + ? // Existing session persisted without a user id (the local + // auto-login response never included one): recover it from + // the token so owned Spaces are not filtered out. + { user_id: userIdFromToken(s.token) } + : {} : { token: null, username: null,