- app.json: version 0.2.0 / versionCode 2, drop expo-screen-capture plugin - add lib/i18n (ko/en/ja/zh), lib/roleTrack, LanguageContext + LanguageSelector - profile/security hardening across screens (login/register/forgot/profile/tabs) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
118 lines
3.7 KiB
TypeScript
118 lines
3.7 KiB
TypeScript
/*
|
|
* 인증 컨텍스트 — JWT를 secure-store에 영속, 앱 부팅 시 복원.
|
|
* 활성 워크스페이스(행사) 선택 상태 보관. 비밀번호는 저장하지 않는다.
|
|
*/
|
|
import React, {
|
|
createContext,
|
|
useCallback,
|
|
useContext,
|
|
useEffect,
|
|
useMemo,
|
|
useState,
|
|
} from 'react';
|
|
import { setAccessToken } from '../lib/api';
|
|
import { landingPathFor } from '../lib/roleTrack';
|
|
import { secureDelete, secureGet, secureSet } from '../lib/secureStore';
|
|
import type { AuthUser, LoginResponse, WorkspaceDto } from '../lib/types';
|
|
|
|
const TOKEN_KEY = 'kintex.accessToken';
|
|
const USER_KEY = 'kintex.user';
|
|
const LANDING_KEY = 'kintex.landing';
|
|
|
|
interface AuthState {
|
|
ready: boolean;
|
|
token: string | null;
|
|
user: AuthUser | null;
|
|
workspaces: WorkspaceDto[];
|
|
activeEventId: string | null;
|
|
/** 로그인 시 역할로 계산한 랜딩 라우트(콜드 부팅 시 workspaces 미복원 문제 회피용으로 지속). */
|
|
landingPath: string | null;
|
|
}
|
|
|
|
interface AuthContextValue extends AuthState {
|
|
activeWorkspace: WorkspaceDto | null;
|
|
signIn: (res: LoginResponse) => Promise<void>;
|
|
signOut: () => Promise<void>;
|
|
selectWorkspace: (eventId: string) => void;
|
|
}
|
|
|
|
const AuthContext = createContext<AuthContextValue | undefined>(undefined);
|
|
|
|
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|
const [state, setState] = useState<AuthState>({
|
|
ready: false,
|
|
token: null,
|
|
user: null,
|
|
workspaces: [],
|
|
activeEventId: null,
|
|
landingPath: null,
|
|
});
|
|
|
|
// 부팅 시 토큰 복원
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
(async () => {
|
|
const token = await secureGet(TOKEN_KEY);
|
|
const userRaw = await secureGet(USER_KEY);
|
|
const landingPath = await secureGet(LANDING_KEY);
|
|
if (cancelled) return;
|
|
const user = userRaw ? (JSON.parse(userRaw) as AuthUser) : null;
|
|
if (token) setAccessToken(token);
|
|
setState((s) => ({ ...s, ready: true, token, user, landingPath }));
|
|
})();
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, []);
|
|
|
|
const signIn = useCallback(async (res: LoginResponse) => {
|
|
setAccessToken(res.accessToken);
|
|
// 역할 우선순위로 랜딩 경로를 계산·지속(콜드 부팅 시 재사용 — workspaces 미복원 misroute 방지).
|
|
const landingPath = landingPathFor({ user: res.user, workspaces: res.workspaces });
|
|
await secureSet(TOKEN_KEY, res.accessToken);
|
|
await secureSet(USER_KEY, JSON.stringify(res.user));
|
|
await secureSet(LANDING_KEY, landingPath);
|
|
setState((s) => ({
|
|
...s,
|
|
token: res.accessToken,
|
|
user: res.user,
|
|
workspaces: res.workspaces ?? [],
|
|
activeEventId: res.workspaces?.[0]?.eventId ?? null,
|
|
landingPath,
|
|
}));
|
|
}, []);
|
|
|
|
const signOut = useCallback(async () => {
|
|
setAccessToken(null);
|
|
await secureDelete(TOKEN_KEY);
|
|
await secureDelete(USER_KEY);
|
|
await secureDelete(LANDING_KEY);
|
|
setState((s) => ({
|
|
...s,
|
|
token: null,
|
|
user: null,
|
|
workspaces: [],
|
|
activeEventId: null,
|
|
landingPath: null,
|
|
}));
|
|
}, []);
|
|
|
|
const selectWorkspace = useCallback((eventId: string) => {
|
|
setState((s) => ({ ...s, activeEventId: eventId }));
|
|
}, []);
|
|
|
|
const value = useMemo<AuthContextValue>(() => {
|
|
const activeWorkspace =
|
|
state.workspaces.find((w) => w.eventId === state.activeEventId) ?? null;
|
|
return { ...state, activeWorkspace, signIn, signOut, selectWorkspace };
|
|
}, [state, signIn, signOut, selectWorkspace]);
|
|
|
|
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
|
}
|
|
|
|
export function useAuth(): AuthContextValue {
|
|
const ctx = useContext(AuthContext);
|
|
if (!ctx) throw new Error('useAuth must be used within AuthProvider');
|
|
return ctx;
|
|
}
|