fix(auth): rehydrate session store on refresh (empty workspaces broke event-scoped screens)
Token lives in sessionStorage but user/workspaces existed only in the zustand store, so any full reload left workspaces=[] and every event-scoped screen fell to 'select an event' with an empty selector. RequireAuth now restores /api/auth/me + /api/auth/workspaces once when authenticated with an empty store, and logs out on 401. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
ecc00f8739
commit
3860e7e88a
@ -1,4 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Navigate, Outlet, Route, Routes, useNavigate } from 'react-router-dom';
|
||||
import { authApi } from './api/endpoints';
|
||||
import { useAuthStore } from './store/authStore';
|
||||
import { landingPathFor } from './lib/roleTrack';
|
||||
import { LoginPage } from './screens/login/LoginPage';
|
||||
@ -95,10 +97,45 @@ function DocsMilestoneRoute() {
|
||||
return <DocsMilestonePage onOpenAuthoring={() => navigate('/docs/authoring')} />;
|
||||
}
|
||||
|
||||
/** 인증 가드 — 미인증 시 로그인으로. (역할별 라우팅은 화면 추가 시 확장) */
|
||||
/**
|
||||
* 인증 가드 — 미인증 시 로그인으로. (역할별 라우팅은 화면 추가 시 확장)
|
||||
* 새로고침 재수화: 토큰(sessionStorage)은 살아있는데 스토어가 빈 상태(user=null·workspaces=[])면
|
||||
* /api/auth/me + /api/auth/workspaces 로 복원한다 — 없으면 행사스코프 화면 전체가
|
||||
* "행사를 선택해 주세요"로 깨진다(2026-07-14 소유자 제보). 401이면 토큰 무효 → 로그아웃.
|
||||
*/
|
||||
function RequireAuth({ children }: { children: React.ReactNode }) {
|
||||
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
|
||||
const user = useAuthStore((s) => s.user);
|
||||
const applySession = useAuthStore((s) => s.applySession);
|
||||
const logout = useAuthStore((s) => s.logout);
|
||||
const needsHydration = isAuthenticated && user == null;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!needsHydration) return;
|
||||
let cancelled = false;
|
||||
void Promise.all([authApi.me(), authApi.workspaces()])
|
||||
.then(([me, workspaces]) => {
|
||||
if (cancelled) return;
|
||||
applySession(
|
||||
{
|
||||
userId: me.userId,
|
||||
displayName: me.displayName,
|
||||
hallManager: me.hallManager,
|
||||
roleCode: me.roleCode ?? null,
|
||||
},
|
||||
workspaces,
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) logout();
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [needsHydration, applySession, logout]);
|
||||
|
||||
if (!isAuthenticated) return <Navigate to="/login" replace />;
|
||||
if (needsHydration) return null; // 복원 완료까지 렌더 보류(빈 workspaces로 화면 오판 방지)
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
|
||||
@ -13,6 +13,8 @@ interface AuthState {
|
||||
currentEventId: string | null;
|
||||
isAuthenticated: boolean;
|
||||
applyLogin: (res: LoginResponse) => void;
|
||||
/** 새로고침 세션 재수화 — 토큰은 살아있지만 스토어가 빈 상태(user=null)일 때 /me·/workspaces 응답으로 복원. */
|
||||
applySession: (user: AuthUser, workspaces: WorkspaceDto[]) => void;
|
||||
selectEvent: (eventId: string) => void;
|
||||
logout: () => void;
|
||||
currentWorkspace: () => WorkspaceDto | null;
|
||||
@ -38,6 +40,8 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
});
|
||||
},
|
||||
|
||||
applySession: (user, workspaces) => set({ user, workspaces, isAuthenticated: true }),
|
||||
|
||||
selectEvent: (eventId) => set({ currentEventId: eventId }),
|
||||
|
||||
logout: () => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user