168 lines
5.0 KiB
TypeScript
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();
|
|
}
|
|
};
|