smart_ims/src/shared/auth/AuthContext.tsx
choibk 8b2589b6fa feat: 플랫폼 보안 강화, 권한 계층 시스템 도입 및 버전 관리 통합 (v0.2.5)
- 최고관리자(Supervisor) 전용 2중 보안 잠금 시스템 및 인증 UI 적용
- 데이터베이스 인프라 및 암호화 마스터 키 자가 관리 기능 구축
- 권한 계층(Supervisor > Admin > User) 기반의 메뉴 노출 및 접근 제어 로직 강화
- 시스템 버전 정보 페이지 신규 추가 및 패키지 버전 자동 연동 (v0.2.5)
- 사용자 관리 UI 디자인 개선 및 폰트/스타일 일원화
2026-01-24 17:17:33 +09:00

147 lines
4.8 KiB
TypeScript

import { createContext, useContext, useState, type ReactNode, useEffect } from 'react';
import { apiClient, setCsrfToken } from '../api/client';
export interface User {
id: string;
name: string;
role: 'supervisor' | 'admin' | 'user';
department?: string;
position?: string;
phone?: string;
last_login?: string;
}
interface AuthContextType {
user: User | null;
login: (id: string, password: string) => Promise<boolean>;
logout: () => void;
isAuthenticated: boolean;
isLoading: boolean;
}
const AuthContext = createContext<AuthContextType | null>(null);
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
// Check for existing session on mount and periodically
useEffect(() => {
let lastActivity = Date.now();
let timeoutMs = 3600000; // Default 1 hour
const checkSession = async () => {
try {
const response = await apiClient.get('/check');
if (response.data.isAuthenticated) {
setUser(response.data.user);
if (response.data.csrfToken) setCsrfToken(response.data.csrfToken);
if (response.data.sessionTimeout) timeoutMs = response.data.sessionTimeout;
} else {
// Safety: only redirect if we were previously logged in
setUser(prev => {
if (prev) {
setCsrfToken(null);
window.location.href = '/login?expired=true';
return null;
}
return prev;
});
}
} catch (error) {
setUser(prev => {
if (prev) {
setCsrfToken(null);
window.location.href = '/login?expired=true';
return null;
}
return prev;
});
} finally {
setIsLoading(false);
}
};
const updateActivity = () => {
lastActivity = Date.now();
};
// Activity Listeners
window.addEventListener('mousemove', updateActivity);
window.addEventListener('keydown', updateActivity);
window.addEventListener('scroll', updateActivity);
window.addEventListener('click', updateActivity);
checkSession();
// Check activity status every 30 seconds
const activityInterval = setInterval(() => {
// Functional check to avoid stale user closure
setUser(current => {
if (current) {
const idleTime = Date.now() - lastActivity;
if (idleTime >= timeoutMs) {
console.log('Idle timeout reached. Checking session...');
checkSession();
}
}
return current;
});
}, 30000);
// Fallback polling every 5 minutes (for secondary tabs etc)
const pollInterval = setInterval(checkSession, 300000);
return () => {
window.removeEventListener('mousemove', updateActivity);
window.removeEventListener('keydown', updateActivity);
window.removeEventListener('scroll', updateActivity);
window.removeEventListener('click', updateActivity);
clearInterval(activityInterval);
clearInterval(pollInterval);
};
}, []); // Removed [user] to prevent infinite loop
const login = async (id: string, password: string) => {
try {
const response = await apiClient.post('/login', { id, password });
if (response.data.success) {
setUser(response.data.user);
if (response.data.csrfToken) {
setCsrfToken(response.data.csrfToken);
}
return true;
}
return false;
} catch (error) {
console.error('Login failed:', error);
return false;
}
};
const logout = async () => {
try {
await apiClient.post('/logout');
} catch (error) {
console.error('Logout failed:', error);
} finally {
setUser(null);
setCsrfToken(null);
}
};
return (
<AuthContext.Provider value={{ user, login, logout, isAuthenticated: !!user, isLoading }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}