Files
lms-adm-fe/src/lib/auth.ts
T

168 lines
5.0 KiB
TypeScript

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<AuthSession> => {
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<AuthSession> => {
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<AuthSession> => {
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<void> => {
try {
await fetch(`${AUTH_BASE_URL}/auth/logout`, {
method: 'POST',
credentials: 'include'
});
} finally {
clearStoredSession();
}
};