- 최고관리자(Supervisor) 전용 2중 보안 잠금 시스템 및 인증 UI 적용 - 데이터베이스 인프라 및 암호화 마스터 키 자가 관리 기능 구축 - 권한 계층(Supervisor > Admin > User) 기반의 메뉴 노출 및 접근 제어 로직 강화 - 시스템 버전 정보 페이지 신규 추가 및 패키지 버전 자동 연동 (v0.2.5) - 사용자 관리 UI 디자인 개선 및 폰트/스타일 일원화
147 lines
4.8 KiB
TypeScript
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;
|
|
}
|