import type { AuthAppRole, AuthSession } from '../types/auth'; const AUTH_BASE_URL = (import.meta.env.VITE_ATNMY_AUTH_URL ?? import.meta.env.VITE_ADS_IDE_AUTH_URL ?? 'https://auth.autonomystudio.net').replace(/\/+$/, ''); const ADMIN_SESSION_STORAGE_KEY = 'atnmy.lms.admin.auth.session'; const INSTRUCTOR_ROLES = new Set(['INSTRUCTOR', 'LMS_ADMIN']); export const isInstructorSession = (session: AuthSession): boolean => { const roles = new Set(getSessionRoles(session)); return [...INSTRUCTOR_ROLES].some((role) => roles.has(role)); }; const getSessionRoles = (session: AuthSession): string[] => { return [ session.user.role, ...(session.user.roles ?? []), ...(session.roles ?? []), ...getAppRoleCodes(session.user.appRoles), ...getAppRoleCodes(session.user.app_roles), ...getAppRoleCodes(session.appRoles), ...getAppRoleCodes(session.app_roles), ] .filter((role): role is string => Boolean(role)) .map((role) => role.toUpperCase()); }; const getAppRoleCodes = (roles: AuthAppRole[] | undefined): string[] => { if (!Array.isArray(roles)) return []; return roles .filter((role) => { const appKey = role.appKey ?? role.app_key; return !appKey || appKey.toUpperCase() === 'LMS'; }) .map((role) => role.roleCode ?? role.role_code ?? role.role ?? role.code) .filter((role): role is string => Boolean(role)); }; export const loadStoredSession = (): AuthSession | null => { try { const raw = window.localStorage.getItem(ADMIN_SESSION_STORAGE_KEY); if (!raw) return null; const session = JSON.parse(raw) as AuthSession; if (!isInstructorSession(session) || isStoredSessionExpired(session)) { clearStoredSession(); return null; } return session; } catch { return null; } }; export const storeSession = (session: AuthSession): void => { window.localStorage.setItem(ADMIN_SESSION_STORAGE_KEY, JSON.stringify({ ...session, obtainedAt: session.obtainedAt ?? Date.now(), })); }; export const clearStoredSession = (): void => { window.localStorage.removeItem(ADMIN_SESSION_STORAGE_KEY); }; export const isStoredSessionExpired = (session: AuthSession): boolean => { const obtainedAt = session.obtainedAt ?? 0; if (!obtainedAt) return false; if (session.refreshToken && session.refreshTokenExpiresIn) { return Date.now() >= obtainedAt + session.refreshTokenExpiresIn * 1000; } return Date.now() >= obtainedAt + (session.accessTokenExpiresIn ?? 0) * 1000; }; export const loginInstructor = async (email: string, password: string): Promise => { const response = await fetch(`${AUTH_BASE_URL}/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ email, password }) }); if (!response.ok) { throw new Error(response.status === 429 ? '로그인 시도가 너무 많습니다. 잠시 후 다시 시도해주세요.' : '이메일 또는 비밀번호를 확인해주세요.'); } const session = { ...(await response.json()) as AuthSession, obtainedAt: Date.now(), }; if (!isInstructorSession(session)) { clearStoredSession(); throw new Error('강사 계정만 로그인할 수 있습니다.'); } storeSession(session); return session; }; export const refreshInstructorSession = async (session: AuthSession): Promise => { if (!session.refreshToken) { throw new Error('리프레시 토큰이 없습니다.'); } const response = await fetch(`${AUTH_BASE_URL}/auth/refresh`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ refreshToken: session.refreshToken }) }); if (!response.ok) { throw new Error('세션을 갱신하지 못했습니다.'); } const nextSession = { ...(await response.json()) as AuthSession, obtainedAt: Date.now(), }; if (!isInstructorSession(nextSession)) { clearStoredSession(); throw new Error('강사 계정만 로그인할 수 있습니다.'); } storeSession(nextSession); return nextSession; }; export const refreshCurrentUser = async (session: AuthSession): Promise => { const response = await fetch(`${AUTH_BASE_URL}/auth/me`, { headers: { Authorization: `Bearer ${session.accessToken}` } }); if (!response.ok) { throw new Error('사용자 정보를 다시 가져오지 못했습니다.'); } const payload = (await response.json()) as { user?: AuthSession['user'] }; const nextSession = { ...session, user: payload.user ?? session.user }; if (!isInstructorSession(nextSession)) { clearStoredSession(); throw new Error('강사 계정만 로그인할 수 있습니다.'); } storeSession(nextSession); return nextSession; }; export const logoutInstructor = async (): Promise => { try { await fetch(`${AUTH_BASE_URL}/auth/logout`, { method: 'POST', credentials: 'include' }); } finally { clearStoredSession(); } };