feat: AI 서버 관리 페이지 Phase 3 보안 강화 - JWT 인증 시스템
🔐 JWT 기반 로그인 시스템: - 로그인 페이지: 아름다운 애니메이션과 보안 정보 표시 - JWT 토큰: 24시간 또는 30일 (Remember Me) 만료 설정 - 비밀번호 암호화: bcrypt 해싱으로 안전한 저장 - 계정 잠금: 5회 실패 시 15분 자동 잠금 👥 사용자 계정 관리: - admin/admin123 (관리자 권한) - hyungi/hyungi123 (시스템 권한) - 역할 기반 접근 제어 (RBAC) 🛡️ 보안 기능: - 토큰 자동 검증 및 만료 처리 - 감사 로그: 로그인/로그아웃/관리 작업 추적 - 안전한 세션 관리 및 토큰 정리 - 클라이언트 사이드 토큰 검증 🎨 UI/UX 개선: - 로그인 페이지: 그라디언트 배경, 플로팅 아이콘 애니메이션 - 사용자 메뉴: 헤더에 사용자명과 로그아웃 버튼 표시 - 보안 표시: SSL, 세션 타임아웃, JWT 인증 정보 - 반응형 디자인 및 다크모드 지원 🔧 기술 구현: - FastAPI HTTPBearer 보안 스키마 - PyJWT 토큰 생성/검증 - bcrypt 비밀번호 해싱 - 클라이언트-서버 토큰 동기화 새 파일: - templates/login.html: 로그인 페이지 HTML - static/login.css: 로그인 페이지 스타일 - static/login.js: 로그인 JavaScript 로직 - server/auth.py: JWT 인증 시스템 (실제 서버용) 수정된 파일: - test_admin.py: 테스트 서버에 JWT 인증 추가 - static/admin.js: JWT 토큰 기반 API 요청으로 변경 - templates/admin.html: 사용자 메뉴 및 로그아웃 버튼 추가 - static/admin.css: 사용자 메뉴 스타일 추가 보안 레벨: Phase 1 (API Key) → Phase 3 (JWT + 감사로그)
This commit is contained in:
@@ -8,25 +8,40 @@ class AdminDashboard {
|
||||
}
|
||||
|
||||
getApiKey() {
|
||||
// 테스트 모드에서는 기본 API 키 사용
|
||||
let apiKey = localStorage.getItem('ai_admin_api_key');
|
||||
if (!apiKey) {
|
||||
// 테스트 모드 기본 키
|
||||
apiKey = 'test-admin-key-123';
|
||||
localStorage.setItem('ai_admin_api_key', apiKey);
|
||||
|
||||
// 사용자에게 알림
|
||||
setTimeout(() => {
|
||||
alert('테스트 모드입니다.\nAPI Key: test-admin-key-123');
|
||||
}, 1000);
|
||||
// JWT 토큰 사용
|
||||
const token = localStorage.getItem('ai_admin_token');
|
||||
console.log('Getting token:', token ? token.substring(0, 20) + '...' : 'No token found');
|
||||
if (!token) {
|
||||
// 토큰이 없으면 로그인 페이지로 리다이렉트
|
||||
console.log('No token, redirecting to login...');
|
||||
window.location.href = '/login';
|
||||
return null;
|
||||
}
|
||||
return apiKey;
|
||||
return token;
|
||||
}
|
||||
|
||||
async init() {
|
||||
// 먼저 토큰 검증
|
||||
if (!this.apiKey) {
|
||||
return; // getApiKey()에서 이미 리다이렉트됨
|
||||
}
|
||||
|
||||
// 토큰 유효성 검증
|
||||
try {
|
||||
await this.apiRequest('/admin/verify-token');
|
||||
console.log('Token verification successful');
|
||||
} catch (error) {
|
||||
console.log('Token verification failed, redirecting to login');
|
||||
localStorage.removeItem('ai_admin_token');
|
||||
localStorage.removeItem('ai_admin_user');
|
||||
window.location.href = '/login';
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateCurrentTime();
|
||||
setInterval(() => this.updateCurrentTime(), 1000);
|
||||
|
||||
await this.loadUserInfo(); // Phase 3: Load user info
|
||||
await this.loadSystemStatus();
|
||||
await this.loadModels();
|
||||
await this.loadApiKeys();
|
||||
@@ -40,6 +55,28 @@ class AdminDashboard {
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
// Phase 3: User Management
|
||||
async loadUserInfo() {
|
||||
try {
|
||||
const userInfo = localStorage.getItem('ai_admin_user');
|
||||
if (userInfo) {
|
||||
const user = JSON.parse(userInfo);
|
||||
document.getElementById('username').textContent = user.username;
|
||||
} else {
|
||||
// Verify token and get user info
|
||||
const response = await this.apiRequest('/admin/verify-token');
|
||||
if (response.valid) {
|
||||
document.getElementById('username').textContent = response.user.username;
|
||||
localStorage.setItem('ai_admin_user', JSON.stringify(response.user));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load user info:', error);
|
||||
// Token might be invalid, redirect to login
|
||||
window.location.href = '/login';
|
||||
}
|
||||
}
|
||||
|
||||
updateCurrentTime() {
|
||||
const now = new Date();
|
||||
document.getElementById('current-time').textContent =
|
||||
@@ -58,18 +95,27 @@ class AdminDashboard {
|
||||
const defaultOptions = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-API-Key': this.apiKey
|
||||
'Authorization': `Bearer ${this.apiKey}`
|
||||
}
|
||||
};
|
||||
|
||||
console.log('API Request:', endpoint, 'with token:', this.apiKey ? this.apiKey.substring(0, 20) + '...' : 'No token');
|
||||
|
||||
try {
|
||||
const response = await fetch(url, { ...defaultOptions, ...options });
|
||||
console.log('API Response:', response.status, response.statusText);
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
localStorage.removeItem('ai_admin_api_key');
|
||||
location.reload();
|
||||
console.log('401 Unauthorized - clearing tokens and redirecting');
|
||||
// JWT 토큰이 만료되었거나 유효하지 않음
|
||||
localStorage.removeItem('ai_admin_token');
|
||||
localStorage.removeItem('ai_admin_user');
|
||||
window.location.href = '/login';
|
||||
return;
|
||||
}
|
||||
const errorText = await response.text();
|
||||
console.log('Error response:', errorText);
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
return await response.json();
|
||||
@@ -435,6 +481,23 @@ function closeModal(modalId) {
|
||||
admin.closeModal(modalId);
|
||||
}
|
||||
|
||||
// Phase 3: Logout function
|
||||
async function logout() {
|
||||
if (!confirm('Are you sure you want to logout?')) return;
|
||||
|
||||
try {
|
||||
// Call logout API
|
||||
await admin.apiRequest('/admin/logout', { method: 'POST' });
|
||||
} catch (error) {
|
||||
console.error('Logout API call failed:', error);
|
||||
} finally {
|
||||
// Clear local storage and redirect
|
||||
localStorage.removeItem('ai_admin_token');
|
||||
localStorage.removeItem('ai_admin_user');
|
||||
window.location.href = '/login';
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize dashboard when page loads
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
admin = new AdminDashboard();
|
||||
|
||||
Reference in New Issue
Block a user