import { useState, useEffect, useRef } from 'react'; import { Card } from '../../shared/ui/Card'; import { Button } from '../../shared/ui/Button'; import { Input } from '../../shared/ui/Input'; import { apiClient } from '../../shared/api/client'; import { useAuth } from '../../shared/auth/AuthContext'; import { Save, Database, Server, CheckCircle2, AlertCircle, Key, RefreshCcw, ShieldAlert, Lock, Unlock, Clock } from 'lucide-react'; interface SystemSettings { session_timeout: number; encryption_key: string; subscriber_id: string; gitea_url: string; gitea_user: string; gitea_password: string; db_config: { host: string; user: string; password?: string; database: string; port: string; }; } export function BasicSettingsPage() { const { user: currentUser } = useAuth(); const [settings, setSettings] = useState({ session_timeout: 60, encryption_key: '', subscriber_id: '', gitea_url: '', gitea_user: '', gitea_password: '', db_config: { host: '', user: '', password: '', database: '', port: '3306' } }); const [testResult, setTestResult] = useState<{ success: boolean; message: string } | null>(null); const [isDbVerified, setIsDbVerified] = useState(false); const [saveResults, setSaveResults] = useState<{ [key: string]: { success: boolean; message: string } | null }>({ security: null, encryption: null, repository: null, database: null }); const [testing, setTesting] = useState(false); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [rotationStatus, setRotationStatus] = useState<{ currentKey: string; affectedCount: number } | null>(null); const [rotating, setRotating] = useState(false); // Supervisor Protection States const [isDbConfigUnlocked, setIsDbConfigUnlocked] = useState(false); const [isEncryptionUnlocked, setIsEncryptionUnlocked] = useState(false); const [isRepoUnlocked, setIsRepoUnlocked] = useState(false); const [verifyingTarget, setVerifyingTarget] = useState<'database' | 'encryption' | 'repository' | null>(null); const [showVerifyModal, setShowVerifyModal] = useState(false); const [verifyPassword, setVerifyPassword] = useState(''); const [verifying, setVerifying] = useState(false); const [verifyError, setVerifyError] = useState(''); const fetchedRef = useRef(false); useEffect(() => { if (fetchedRef.current) return; fetchedRef.current = true; fetchSettings(); fetchEncryptionStatus(); }, []); const fetchSettings = async () => { try { const res = await apiClient.get('/system/settings'); const data = res.data; if (!data.db_config) { data.db_config = { host: '', user: '', password: '', database: '', port: '3306' }; } setSettings(data); setIsDbVerified(true); } catch (error) { console.error('Failed to fetch settings', error); } finally { setLoading(false); } }; const fetchEncryptionStatus = async () => { try { const res = await apiClient.get('/system/encryption/status'); setRotationStatus({ currentKey: res.data.current_key, affectedCount: Object.values(res.data.affected_records as Record).reduce((a, b) => a + b, 0) }); } catch (error) { console.error('Failed to fetch encryption status', error); } }; const generateNewKey = () => { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()'; let key = 'smartims_'; for (let i = 0; i < 24; i++) { key += chars.charAt(Math.floor(Math.random() * chars.length)); } setSettings({ ...settings, encryption_key: key }); }; const handleRotateKey = async () => { if (!settings.encryption_key) { alert('새로운 암호화 키를 입력하거나 생성해 주세요.'); return; } const confirmMsg = `암호화 키 변경 및 데이터 마이그레이션을 시작합니다.\n- 대상 레코드: 약 ${rotationStatus?.affectedCount || 0}건\n- 소요 시간: 데이터 양에 따라 수 초가 걸릴 수 있습니다.\n\n주의: 절차 진행 중에는 서버 연결이 일시적으로 끊길 수 있으며, 중간에 브라우저를 닫지 마십시오.\n진행하시겠습니까?`; if (!confirm(confirmMsg)) return; setRotating(true); setSaveResults(prev => ({ ...prev, encryption: null })); try { await apiClient.post('/system/encryption/rotate', { new_key: settings.encryption_key }); setSaveResults(prev => ({ ...prev, encryption: { success: true, message: '암호화 키 변경 및 데이터 마이그레이션 완료!' } })); fetchSettings(); fetchEncryptionStatus(); setTimeout(() => setSaveResults(prev => ({ ...prev, encryption: null })), 5000); } catch (error: any) { setSaveResults(prev => ({ ...prev, encryption: { success: false, message: error.response?.data?.error || '재암호화 작업 중 오류가 발생했습니다.' } })); } finally { setRotating(false); } }; const handleTestConnection = async () => { setTesting(true); setTestResult(null); try { const res = await apiClient.post('/system/test-db', settings.db_config); setTestResult({ success: true, message: res.data.message }); setIsDbVerified(true); } catch (error: any) { setTestResult({ success: false, message: error.response?.data?.error || '접속 테스트에 실패했습니다.' }); setIsDbVerified(false); } finally { setTesting(false); } }; const handleOpenVerify = (target: 'database' | 'encryption' | 'repository') => { if (currentUser?.role !== 'supervisor') { alert('해당 권한이 없습니다. 최고관리자(Supervisor)만 접근 가능합니다.'); return; } setVerifyingTarget(target); setShowVerifyModal(true); }; const handleVerifySupervisor = async () => { setVerifying(true); setVerifyError(''); try { const res = await apiClient.post('/verify-supervisor', { password: verifyPassword }); if (res.data.success) { if (verifyingTarget === 'database') setIsDbConfigUnlocked(true); else if (verifyingTarget === 'encryption') setIsEncryptionUnlocked(true); else if (verifyingTarget === 'repository') setIsRepoUnlocked(true); setShowVerifyModal(false); setVerifyPassword(''); setVerifyingTarget(null); } } catch (error: any) { setVerifyError(error.response?.data?.message || '인증에 실패했습니다. 비밀번호를 확인해주세요.'); } finally { setVerifying(false); } }; const handleSaveSection = async (section: 'security' | 'encryption' | 'database' | 'repository') => { if (section === 'database' && !isDbVerified) { alert('DB 접속 정보가 변경되었습니다. 저장 전 반드시 [연결 테스트]를 수행하십시오.'); return; } let payload: any = {}; const successMessage = section === 'database' ? 'DB 설정이 저장되었습니다.' : '설정이 성공적으로 저장되었습니다.'; if (section === 'security') payload = { session_timeout: settings.session_timeout }; else if (section === 'encryption') payload = { encryption_key: settings.encryption_key }; else if (section === 'repository') payload = { gitea_url: settings.gitea_url, gitea_user: settings.gitea_user, gitea_password: settings.gitea_password }; else if (section === 'database') { if (!confirm('DB 설정을 저장하면 서버가 재시작되어 접속이 끊길 수 있습니다. 진행하시겠습니까?')) return; payload = { db_config: settings.db_config }; } setSaving(true); setSaveResults(prev => ({ ...prev, [section]: null })); try { await apiClient.post('/system/settings', payload); setSaveResults(prev => ({ ...prev, [section]: { success: true, message: successMessage } })); setTimeout(() => setSaveResults(prev => ({ ...prev, [section]: null })), 3000); if (section !== 'database') fetchSettings(); } catch (error: any) { setSaveResults(prev => ({ ...prev, [section]: { success: false, message: error.response?.data?.error || '저장 중 오류 발생' } })); } finally { setSaving(false); } }; if (loading) return
데이터를 불러오는 중...
; return (

시스템 관리 - 기본 설정

플랫폼의 보안 및 인프라 환경을 섹션별로 관리합니다.

{/* v0.4.1.0 Info: Session Timeout relocation notice */}

세션 관리 정책 변경 안내 (v0.4.1.0)

보안 강화 및 개인화 설정을 위해 전역 세션 로그아웃 설정이 개별 사용자 설정으로 이전되었습니다.
좌측 메뉴 하단의 [기본 설정] 메뉴 또는 [사용자 관리] 메뉴에서 사용자별로 시간을 설정하실 수 있습니다.

{/* Section 2: Encryption Master Key & Rotation (Supervisor Protected) */}

데이터 암호화 마스터 키 관리

{!isEncryptionUnlocked && ( )} {isEncryptionUnlocked && (
검증 완료: 최고관리자 모드
)}
{isEncryptionUnlocked ? (
현재 활성 키 {rotationStatus?.currentKey || '-'}
영향 레코드
{rotationStatus?.affectedCount || 0} 건
setSettings({ ...settings, encryption_key: e.target.value })} placeholder="새로운 키 입력" className="font-mono flex-1" />
{saveResults.encryption && (
{saveResults.encryption.success ? : } {saveResults.encryption.message}
)}
) : (

보안을 위해 암호화 설정은 최고관리자 인증 후 접근 가능합니다.

)}
{/* Section 2.5: Gitea Repository Update Settings */}

시스템 업데이트 저장소 설정 (Gitea)

{!isRepoUnlocked && ( )} {isRepoUnlocked && (
검증 완료: 최고관리자 모드
)}
{isRepoUnlocked ? (

원격 저장소(Gitea)에서 최신 업데이트 태그를 가져오기 위한 주소 및 인증 정보를 설정합니다.

setSettings({ ...settings, gitea_url: e.target.value })} placeholder="https://gitea.example.com/org/repo.git" />
setSettings({ ...settings, gitea_user: e.target.value })} placeholder="gitea_update_user" />
setSettings({ ...settings, gitea_password: e.target.value })} placeholder="••••••••" />
{saveResults.repository && (
{saveResults.repository.success ? : } {saveResults.repository.message}
)}
) : (

보안을 위해 저장소 설정은 최고관리자 인증 후 접근 가능합니다.

)}
{/* Section 3: Database Infrastructure (Supervisor Protected) */}

데이터베이스 인프라 구성

{!isDbConfigUnlocked && ( )} {isDbConfigUnlocked && (
검증 완료: 최고관리자 모드
)}
{isDbConfigUnlocked ? (

물리적 인프라 수동 관리 모드

이 섹션의 설정을 잘못 변경하면 플랫폼 전체 접속이 불가능해질 수 있습니다.
반드시 접속 테스트 성공을 확인한 후에 저장하십시오.

{ setSettings({ ...settings, db_config: { ...settings.db_config, host: e.target.value } }); setIsDbVerified(false); }} />
{ setSettings({ ...settings, db_config: { ...settings.db_config, user: e.target.value } }); setIsDbVerified(false); }} />
{ setSettings({ ...settings, db_config: { ...settings.db_config, password: e.target.value } }); setIsDbVerified(false); }} />
{ setSettings({ ...settings, db_config: { ...settings.db_config, database: e.target.value } }); setIsDbVerified(false); }} />
{ setSettings({ ...settings, db_config: { ...settings.db_config, port: e.target.value } }); setIsDbVerified(false); }} />
{testResult && ( {testResult.success ? : } {testResult.message} )} {!testResult && 변경 시 반드시 접속 테스트를 수행하십시오.}
{testResult && !testResult.success && ⚠️ 연결 실패 시 변경된 정보를 저장할 수 없습니다.} {saveResults.database && (
{saveResults.database.message}
)}
) : (

보안을 위해 인프라 설정은 최고관리자 인증 후 접근 가능합니다.

)}
{/* Verification Modal */} {showVerifyModal && (

최고관리자 권한 확인 ({verifyingTarget === 'encryption' ? '암호화 키' : verifyingTarget === 'repository' ? '저장소 설정' : '인프라 설정'})

민감 설정 접근을 위해 본인 확인이 필요합니다.

{verifyingTarget === 'encryption' ? "※ 경고: 암호화 키 변경은 시스템 내 모든 민감 데이터를 다시 암호화하는 중대한 작업입니다. 성공 시 기존 데이터를 새 키로 대체하며, 실패 시 데이터 유실의 위험이 있습니다." : verifyingTarget === 'repository' ? "※ 주의: 시스템 업데이트 저장소 정보 노출은 원본 소스 코드 유출로 이어질 수 있습니다." : "※ 경고: 인프라 설정은 데이터베이스 물리 접속 정보를 직접 수정하는 매우 위험한 작업입니다. 잘못된 입력은 시스템 중단으로 이어질 수 있습니다."}
setVerifyPassword(e.target.value)} placeholder="비밀번호를 입력하세요" onKeyDown={e => e.key === 'Enter' && handleVerifySupervisor()} autoFocus /> {verifyError &&

{verifyError}

}
)}
); }