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:
Hyungi Ahn
2025-08-18 15:24:01 +09:00
parent b752e56b94
commit 1e098999c1
9 changed files with 1352 additions and 18 deletions

View File

@@ -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();