import { createContext, useCallback, useContext, useEffect, useMemo, useState, type ReactNode, } from "react"; import { AUTH_EXPIRED_EVENT, api, apiUrl, authApi, type MeResponse } from "./api"; export type Role = "learner" | "teacher" | "admin"; export type AccountStatus = "pending" | "approved" | "suspended"; export type DesignRole = "learner" | "instructor" | "admin"; export interface AuthUser { userId: string; email: string; name: string; role: Role; adminAccess: boolean; superAdmin: boolean; accountStatus: AccountStatus; approvalRequired: boolean; cohortIds: string[]; consentAt: number | null; onboardingCompletedAt: number | null; nickname: string; selfIntroduction: string; avatarUrl: string; } export interface AuthContextValue { user: AuthUser | null; role: Role | null; loading: boolean; login: (role: Role, opts?: { email?: string; displayName?: string }) => Promise; logout: () => Promise; refresh: () => Promise; acceptConsent: () => Promise; withdrawConsent: () => Promise; } export function designRoleOf(role: Role): DesignRole { return role === "teacher" ? "instructor" : role; } export function roleLabel(role: Role): string { switch (role) { case "learner": return "학습자 공간"; case "teacher": return "교수 콘솔"; case "admin": return "운영 콘솔"; } } export function roleHomePath(role: Role): string { switch (role) { case "learner": return "/learn"; case "teacher": return "/teach"; case "admin": return "/admin"; } } export function canAccessRole(user: AuthUser, role: Role): boolean { if (user.role === role) return true; if (user.superAdmin) return true; return role === "admin" && user.adminAccess; } const DEV_EMAIL_BY_ROLE: Record = { learner: "learner@hs.ac.kr", teacher: "teacher@hs.ac.kr", admin: "admin@twentyoz.kr", }; const DEV_NAME_BY_ROLE: Record = { learner: "학습자", teacher: "교수자", admin: "관리자", }; const AuthContext = createContext(null); function userFromMe(me: MeResponse): AuthUser { const onboarding = me as MeResponse & { onboarding_completed_at?: number | null }; const nickname = (me.nickname ?? "").trim(); const avatarUrl = (me.avatar_url ?? "").trim(); return { userId: me.user_id, email: me.email, name: nickname || me.display_name || me.email || me.user_id, role: (me.role as Role) ?? "learner", adminAccess: Boolean(me.admin_access), superAdmin: Boolean(me.super_admin), accountStatus: (me.account_status as AccountStatus | undefined) ?? "approved", approvalRequired: me.approval_required ?? false, cohortIds: me.cohort_ids ?? [], consentAt: me.consent_at ?? null, onboardingCompletedAt: onboarding.onboarding_completed_at ?? null, nickname, selfIntroduction: (me.self_introduction ?? "").trim(), avatarUrl: avatarUrl ? apiUrl(avatarUrl) : "", }; } export function AuthProvider({ children }: { children: ReactNode }) { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { const handleAuthExpired = () => { setUser(null); setLoading(false); }; window.addEventListener(AUTH_EXPIRED_EVENT, handleAuthExpired); return () => { window.removeEventListener(AUTH_EXPIRED_EVENT, handleAuthExpired); }; }, []); useEffect(() => { let alive = true; (async () => { try { const me = await api.get("/auth/me"); if (alive) setUser(userFromMe(me)); } catch { if (alive) setUser(null); } finally { if (alive) setLoading(false); } })(); return () => { alive = false; }; }, []); useEffect(() => { const body = document.body; if (user) body.setAttribute("data-role", designRoleOf(user.role)); else body.removeAttribute("data-role"); }, [user]); const login = useCallback(async (role, opts) => { const me = await api.post("/auth/dev-login", { email: opts?.email ?? DEV_EMAIL_BY_ROLE[role], role, display_name: opts?.displayName ?? DEV_NAME_BY_ROLE[role], }); const next = userFromMe(me); setUser(next); return next; }, []); const refresh = useCallback(async () => { try { const me = await api.get("/auth/me"); const next = userFromMe(me); setUser(next); return next; } catch { setUser(null); return null; } }, []); const logout = useCallback(async () => { try { await api.post("/auth/logout"); } finally { setUser(null); } }, []); const acceptConsent = useCallback(async () => { const response = await authApi.acceptConsent(); setUser((current) => current ? { ...current, consentAt: response.consent_at ?? null } : current, ); }, []); const withdrawConsent = useCallback(async () => { await authApi.withdrawConsent(); setUser((current) => (current ? { ...current, consentAt: null } : current)); }, []); const value = useMemo( () => ({ user, role: user?.role ?? null, loading, login, logout, refresh, acceptConsent, withdrawConsent, }), [user, loading, login, logout, refresh, acceptConsent, withdrawConsent], ); return {children}; } export function useAuth(): AuthContextValue { const ctx = useContext(AuthContext); if (!ctx) { throw new Error("useAuth must be used inside AuthProvider"); } return ctx; }