Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion server/app/domains/user/api/login_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
1 change: 1 addition & 0 deletions server/app/model/user/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ class LoginByPasswordIn(BaseModel):
class LoginResponse(BaseModel):
token: str
email: EmailStr
user_id: int | None = None
redirect_url: str | None = None


Expand Down
39 changes: 32 additions & 7 deletions src/store/authStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ interface AuthInfo {
token: string;
username: string;
email: string;
user_id: number;
user_id?: number | null;
}

// auth state interface
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -213,19 +230,20 @@ const authStore = create<AuthState>()(

// 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: () => {
Expand Down Expand Up @@ -379,7 +397,9 @@ const authStore = create<AuthState>()(
}),
{
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
| {
Expand All @@ -403,7 +423,12 @@ const authStore = create<AuthState>()(
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,
Expand Down