feat: 완전한 사용자 관리 및 로그 모니터링 시스템 구현
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
- 시스템 관리자/관리자 권한별 대시보드 기능 추가 - 사용자 관리 페이지: 계정 생성, 역할 변경, 사용자 삭제 - 시스템 로그 페이지: 로그인 로그, 시스템 오류 로그 조회 - 로그 모니터링 대시보드: 실시간 통계, 최근 활동, 오류 모니터링 - 프론트엔드 ErrorBoundary 및 오류 로깅 시스템 통합 - 계정 설정 페이지: 프로필 업데이트, 비밀번호 변경 - 3단계 권한 시스템 (system/admin/user) 완전 구현 - 시스템 관리자 계정 생성 기능 (hyungi/000000) - 로그인 페이지 테스트 계정 안내 제거 - API 오류 수정: CORS, 이메일 검증, User 모델 import 등
This commit is contained in:
@@ -6,6 +6,7 @@ TK-MP-Project 인증 시스템의 모든 컴포넌트를 노출
|
||||
from .jwt_service import jwt_service, JWTService
|
||||
from .auth_service import AuthService, get_auth_service
|
||||
from .auth_controller import router as auth_router
|
||||
from .setup_controller import router as setup_router
|
||||
from .middleware import (
|
||||
auth_middleware,
|
||||
get_current_user,
|
||||
@@ -39,6 +40,7 @@ __all__ = [
|
||||
|
||||
# 라우터
|
||||
'auth_router',
|
||||
'setup_router',
|
||||
|
||||
# 미들웨어 및 의존성
|
||||
'auth_middleware',
|
||||
|
||||
@@ -5,7 +5,7 @@ TK-FB-Project의 authController.js를 참고하여 FastAPI용으로 구현
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Request
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from sqlalchemy.orm import Session
|
||||
from pydantic import BaseModel, EmailStr
|
||||
from pydantic import BaseModel, EmailStr, validator
|
||||
from typing import Optional, List, Dict, Any
|
||||
|
||||
from ..database import get_db
|
||||
@@ -29,11 +29,21 @@ class RegisterRequest(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
name: str
|
||||
email: Optional[EmailStr] = None
|
||||
email: Optional[str] = None
|
||||
access_level: str = 'worker'
|
||||
department: Optional[str] = None
|
||||
position: Optional[str] = None
|
||||
phone: Optional[str] = None
|
||||
role: str = "user"
|
||||
|
||||
@validator('email', pre=True)
|
||||
def validate_email(cls, v):
|
||||
if v == '' or v is None:
|
||||
return None
|
||||
# 간단한 이메일 형식 검증
|
||||
if '@' not in v or '.' not in v.split('@')[-1]:
|
||||
raise ValueError('올바른 이메일 형식을 입력해주세요')
|
||||
return v
|
||||
|
||||
|
||||
class RefreshTokenRequest(BaseModel):
|
||||
@@ -112,27 +122,47 @@ async def login(
|
||||
@router.post("/register", response_model=RegisterResponse)
|
||||
async def register(
|
||||
register_data: RegisterRequest,
|
||||
credentials: HTTPAuthorizationCredentials = Depends(security),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
사용자 등록
|
||||
사용자 등록 (시스템 관리자만 가능)
|
||||
|
||||
Args:
|
||||
register_data: 등록 정보
|
||||
credentials: JWT 토큰 (시스템 관리자 권한 필요)
|
||||
db: 데이터베이스 세션
|
||||
|
||||
Returns:
|
||||
RegisterResponse: 등록 결과
|
||||
"""
|
||||
try:
|
||||
# 토큰 검증 및 권한 확인
|
||||
from .jwt_service import jwt_service
|
||||
payload = jwt_service.verify_access_token(credentials.credentials)
|
||||
|
||||
# 시스템 관리자 권한 확인
|
||||
if payload['role'] != 'system':
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="계정 생성은 시스템 관리자만 가능합니다"
|
||||
)
|
||||
|
||||
auth_service = get_auth_service(db)
|
||||
result = await auth_service.register(register_data.dict())
|
||||
|
||||
logger.info(f"User registered by system admin: {register_data.username} (created by: {payload['username']})")
|
||||
|
||||
return RegisterResponse(**result)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Register endpoint error: {str(e)}")
|
||||
raise
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="사용자 등록 중 오류가 발생했습니다"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/refresh", response_model=RefreshTokenResponse)
|
||||
@@ -307,7 +337,7 @@ async def get_all_users(
|
||||
if payload['role'] not in ['admin', 'system']:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="관리자 권한이 필요합니다"
|
||||
detail="관리자 이상의 권한이 필요합니다"
|
||||
)
|
||||
|
||||
# 사용자 목록 조회
|
||||
@@ -350,10 +380,10 @@ async def delete_user(
|
||||
try:
|
||||
# 토큰 검증 및 권한 확인
|
||||
payload = jwt_service.verify_access_token(credentials.credentials)
|
||||
if payload['role'] not in ['admin', 'system']:
|
||||
if payload['role'] != 'system':
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="관리자 권한이 필요합니다"
|
||||
detail="사용자 삭제는 시스템 관리자만 가능합니다"
|
||||
)
|
||||
|
||||
# 자기 자신 삭제 방지
|
||||
@@ -393,7 +423,453 @@ async def delete_user(
|
||||
)
|
||||
|
||||
|
||||
|
||||
# 로그 관리 API (관리자 이상)
|
||||
@router.get("/logs/login")
|
||||
async def get_login_logs(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
user_id: Optional[int] = None,
|
||||
status: Optional[str] = None,
|
||||
credentials: HTTPAuthorizationCredentials = Depends(security),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
로그인 로그 조회 (관리자 이상)
|
||||
|
||||
Args:
|
||||
skip: 건너뛸 레코드 수
|
||||
limit: 조회할 레코드 수
|
||||
user_id: 특정 사용자 ID 필터
|
||||
status: 로그인 상태 필터 (success/failed)
|
||||
credentials: JWT 토큰
|
||||
db: 데이터베이스 세션
|
||||
|
||||
Returns:
|
||||
Dict: 로그인 로그 목록
|
||||
"""
|
||||
try:
|
||||
# 토큰 검증 및 권한 확인
|
||||
from .jwt_service import jwt_service
|
||||
payload = jwt_service.verify_access_token(credentials.credentials)
|
||||
|
||||
if payload['role'] not in ['admin', 'system']:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="로그 조회는 관리자 이상의 권한이 필요합니다"
|
||||
)
|
||||
|
||||
# 로그인 로그 조회
|
||||
from .models import LoginLog, User
|
||||
query = db.query(LoginLog).join(User)
|
||||
|
||||
if user_id:
|
||||
query = query.filter(LoginLog.user_id == user_id)
|
||||
if status:
|
||||
query = query.filter(LoginLog.login_status == status)
|
||||
|
||||
logs = query.order_by(LoginLog.login_time.desc()).offset(skip).limit(limit).all()
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'logs': [
|
||||
{
|
||||
'log_id': log.log_id,
|
||||
'user_id': log.user_id,
|
||||
'username': log.user.username,
|
||||
'name': log.user.name,
|
||||
'login_time': log.login_time,
|
||||
'ip_address': log.ip_address,
|
||||
'user_agent': log.user_agent,
|
||||
'login_status': log.login_status,
|
||||
'failure_reason': log.failure_reason
|
||||
}
|
||||
for log in logs
|
||||
],
|
||||
'total_count': len(logs)
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Get login logs error: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="로그인 로그 조회 중 오류가 발생했습니다"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/logs/system")
|
||||
async def get_system_logs(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
level: Optional[str] = None,
|
||||
credentials: HTTPAuthorizationCredentials = Depends(security),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
시스템 로그 조회 (관리자 이상)
|
||||
|
||||
Args:
|
||||
skip: 건너뛸 레코드 수
|
||||
limit: 조회할 레코드 수
|
||||
level: 로그 레벨 필터 (ERROR, WARNING, INFO, DEBUG)
|
||||
credentials: JWT 토큰
|
||||
db: 데이터베이스 세션
|
||||
|
||||
Returns:
|
||||
Dict: 시스템 로그 목록
|
||||
"""
|
||||
try:
|
||||
# 토큰 검증 및 권한 확인
|
||||
from .jwt_service import jwt_service
|
||||
payload = jwt_service.verify_access_token(credentials.credentials)
|
||||
|
||||
if payload['role'] not in ['admin', 'system']:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="시스템 로그 조회는 관리자 이상의 권한이 필요합니다"
|
||||
)
|
||||
|
||||
# 로그 파일에서 최근 로그 읽기 (임시 구현)
|
||||
import os
|
||||
from ..config import get_settings
|
||||
|
||||
settings = get_settings()
|
||||
log_file_path = settings.logging.file_path
|
||||
|
||||
logs = []
|
||||
if os.path.exists(log_file_path):
|
||||
try:
|
||||
with open(log_file_path, 'r', encoding='utf-8') as f:
|
||||
lines = f.readlines()
|
||||
|
||||
# 최근 로그부터 처리
|
||||
recent_lines = lines[-limit-skip:] if len(lines) > skip else lines
|
||||
|
||||
for line in reversed(recent_lines):
|
||||
if line.strip():
|
||||
# 간단한 로그 파싱 (실제로는 더 정교한 파싱 필요)
|
||||
parts = line.strip().split(' - ')
|
||||
if len(parts) >= 4:
|
||||
timestamp = parts[0]
|
||||
module = parts[1]
|
||||
log_level = parts[2]
|
||||
message = ' - '.join(parts[3:])
|
||||
|
||||
if not level or log_level == level:
|
||||
logs.append({
|
||||
'timestamp': timestamp,
|
||||
'module': module,
|
||||
'level': log_level,
|
||||
'message': message
|
||||
})
|
||||
|
||||
if len(logs) >= limit:
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to read log file: {str(e)}")
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'logs': logs,
|
||||
'total_count': len(logs)
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Get system logs error: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="시스템 로그 조회 중 오류가 발생했습니다"
|
||||
)
|
||||
|
||||
|
||||
# 사용자 역할 변경 API (시스템 관리자만)
|
||||
@router.put("/users/{user_id}/role")
|
||||
async def change_user_role(
|
||||
user_id: int,
|
||||
new_role: str,
|
||||
credentials: HTTPAuthorizationCredentials = Depends(security),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
사용자 역할 변경 (시스템 관리자만)
|
||||
|
||||
Args:
|
||||
user_id: 변경할 사용자 ID
|
||||
new_role: 새로운 역할 (system, admin, user)
|
||||
credentials: JWT 토큰
|
||||
db: 데이터베이스 세션
|
||||
|
||||
Returns:
|
||||
Dict: 변경 결과
|
||||
"""
|
||||
try:
|
||||
# 토큰 검증 및 권한 확인
|
||||
from .jwt_service import jwt_service
|
||||
payload = jwt_service.verify_access_token(credentials.credentials)
|
||||
|
||||
if payload['role'] != 'system':
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="사용자 역할 변경은 시스템 관리자만 가능합니다"
|
||||
)
|
||||
|
||||
# 유효한 역할인지 확인
|
||||
if new_role not in ['system', 'admin', 'user']:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="유효하지 않은 역할입니다. (system, admin, user 중 선택)"
|
||||
)
|
||||
|
||||
# 자기 자신의 역할 변경 방지
|
||||
if payload['user_id'] == user_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="자기 자신의 역할은 변경할 수 없습니다"
|
||||
)
|
||||
|
||||
# 사용자 조회 및 역할 변경
|
||||
from .models import UserRepository
|
||||
user_repo = UserRepository(db)
|
||||
user = user_repo.find_by_id(user_id)
|
||||
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="해당 사용자를 찾을 수 없습니다"
|
||||
)
|
||||
|
||||
old_role = user.role
|
||||
user.role = new_role
|
||||
user_repo.update_user(user)
|
||||
|
||||
logger.info(f"User role changed: {user.username} ({old_role} → {new_role}) by {payload['username']}")
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'message': f'사용자 역할이 변경되었습니다: {old_role} → {new_role}',
|
||||
'user_id': user_id,
|
||||
'old_role': old_role,
|
||||
'new_role': new_role
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Change user role error: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="사용자 역할 변경 중 오류가 발생했습니다"
|
||||
)
|
||||
|
||||
|
||||
# 프론트엔드 오류 로그 수집 API
|
||||
@router.post("/logs/frontend-error")
|
||||
async def log_frontend_error(
|
||||
error_data: dict,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
프론트엔드 오류 로그 수집
|
||||
|
||||
Args:
|
||||
error_data: 프론트엔드에서 전송한 오류 데이터
|
||||
request: FastAPI Request 객체
|
||||
db: 데이터베이스 세션
|
||||
|
||||
Returns:
|
||||
Dict: 로그 저장 결과
|
||||
"""
|
||||
try:
|
||||
from datetime import datetime
|
||||
|
||||
# 클라이언트 정보 추가
|
||||
client_ip = request.client.host
|
||||
user_agent = request.headers.get("user-agent", "")
|
||||
|
||||
# 오류 데이터 보강
|
||||
enhanced_error_data = {
|
||||
**error_data,
|
||||
'client_ip': client_ip,
|
||||
'server_timestamp': datetime.utcnow().isoformat(),
|
||||
'user_agent': user_agent
|
||||
}
|
||||
|
||||
# 로그로 기록
|
||||
logger.error(f"Frontend Error: {error_data.get('type', 'unknown')} - {error_data.get('message', 'No message')}",
|
||||
extra={'frontend_error': enhanced_error_data})
|
||||
|
||||
# 데이터베이스에 저장 (선택적)
|
||||
# TODO: 필요시 frontend_errors 테이블 생성하여 저장
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'message': '오류가 기록되었습니다',
|
||||
'timestamp': enhanced_error_data['server_timestamp']
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to log frontend error: {str(e)}")
|
||||
return {
|
||||
'success': False,
|
||||
'message': '오류 기록에 실패했습니다'
|
||||
}
|
||||
|
||||
|
||||
# 프로필 업데이트 API
|
||||
class ProfileUpdateRequest(BaseModel):
|
||||
name: str
|
||||
email: Optional[EmailStr] = None
|
||||
department: Optional[str] = None
|
||||
position: Optional[str] = None
|
||||
|
||||
|
||||
@router.put("/profile")
|
||||
async def update_profile(
|
||||
profile_data: ProfileUpdateRequest,
|
||||
credentials: HTTPAuthorizationCredentials = Depends(security),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
사용자 프로필 업데이트
|
||||
|
||||
Args:
|
||||
profile_data: 업데이트할 프로필 정보
|
||||
credentials: JWT 토큰
|
||||
db: 데이터베이스 세션
|
||||
|
||||
Returns:
|
||||
Dict: 업데이트 결과
|
||||
"""
|
||||
try:
|
||||
# 토큰 검증
|
||||
from .jwt_service import jwt_service
|
||||
payload = jwt_service.verify_access_token(credentials.credentials)
|
||||
user_id = payload['user_id']
|
||||
|
||||
# 사용자 조회
|
||||
from .models import UserRepository
|
||||
user_repo = UserRepository(db)
|
||||
user = user_repo.find_by_id(user_id)
|
||||
|
||||
if not user or not user.is_active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="사용자를 찾을 수 없습니다"
|
||||
)
|
||||
|
||||
# 이메일 중복 확인 (다른 사용자가 사용 중인지)
|
||||
if profile_data.email and profile_data.email != user.email:
|
||||
existing_email = user_repo.find_by_email(profile_data.email)
|
||||
if existing_email and existing_email.user_id != user_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="이미 사용 중인 이메일입니다"
|
||||
)
|
||||
|
||||
# 프로필 업데이트
|
||||
user.name = profile_data.name
|
||||
user.email = profile_data.email
|
||||
user.department = profile_data.department
|
||||
user.position = profile_data.position
|
||||
|
||||
user_repo.update_user(user)
|
||||
|
||||
logger.info(f"Profile updated for user: {user.username}")
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'message': '프로필이 업데이트되었습니다',
|
||||
'user': user.to_dict()
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Profile update error: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="프로필 업데이트 중 오류가 발생했습니다"
|
||||
)
|
||||
|
||||
|
||||
# 비밀번호 변경 API
|
||||
class ChangePasswordRequest(BaseModel):
|
||||
current_password: str
|
||||
new_password: str
|
||||
|
||||
|
||||
@router.put("/change-password")
|
||||
async def change_password(
|
||||
password_data: ChangePasswordRequest,
|
||||
credentials: HTTPAuthorizationCredentials = Depends(security),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
사용자 비밀번호 변경
|
||||
|
||||
Args:
|
||||
password_data: 현재 비밀번호와 새 비밀번호
|
||||
credentials: JWT 토큰
|
||||
db: 데이터베이스 세션
|
||||
|
||||
Returns:
|
||||
Dict: 변경 결과
|
||||
"""
|
||||
try:
|
||||
# 토큰 검증
|
||||
from .jwt_service import jwt_service
|
||||
payload = jwt_service.verify_access_token(credentials.credentials)
|
||||
user_id = payload['user_id']
|
||||
|
||||
# 사용자 조회
|
||||
from .models import UserRepository
|
||||
user_repo = UserRepository(db)
|
||||
user = user_repo.find_by_id(user_id)
|
||||
|
||||
if not user or not user.is_active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="사용자를 찾을 수 없습니다"
|
||||
)
|
||||
|
||||
# 현재 비밀번호 확인
|
||||
if not user.check_password(password_data.current_password):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="현재 비밀번호가 올바르지 않습니다"
|
||||
)
|
||||
|
||||
# 새 비밀번호 유효성 검사
|
||||
if len(password_data.new_password) < 8:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="새 비밀번호는 8자 이상이어야 합니다"
|
||||
)
|
||||
|
||||
# 비밀번호 변경
|
||||
user.set_password(password_data.new_password)
|
||||
user_repo.update_user(user)
|
||||
|
||||
logger.info(f"Password changed for user: {user.username}")
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'message': '비밀번호가 변경되었습니다'
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Password change error: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="비밀번호 변경 중 오류가 발생했습니다"
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -27,9 +27,9 @@ class User(Base):
|
||||
name = Column(String(100), nullable=False)
|
||||
email = Column(String(100), index=True)
|
||||
|
||||
# 권한 관리
|
||||
role = Column(String(20), default='user', nullable=False)
|
||||
access_level = Column(String(20), default='worker', nullable=False)
|
||||
# 권한 관리 - 3단계 시스템: system(제작자) > admin(관리자) > user(사용자)
|
||||
role = Column(String(20), default='user', nullable=False) # system, admin, user
|
||||
access_level = Column(String(20), default='worker', nullable=False) # 호환성 유지
|
||||
|
||||
# 계정 상태 관리
|
||||
is_active = Column(Boolean, default=True, nullable=False)
|
||||
@@ -118,6 +118,40 @@ class User(Base):
|
||||
def update_last_login(self):
|
||||
"""마지막 로그인 시간 업데이트"""
|
||||
self.last_login_at = datetime.utcnow()
|
||||
|
||||
# 권한 체크 메서드들
|
||||
def is_system(self) -> bool:
|
||||
"""시스템 관리자 권한 확인"""
|
||||
return self.role == 'system'
|
||||
|
||||
def is_admin(self) -> bool:
|
||||
"""관리자 권한 확인 (시스템 관리자 포함)"""
|
||||
return self.role in ['system', 'admin']
|
||||
|
||||
def is_user(self) -> bool:
|
||||
"""일반 사용자 권한 확인"""
|
||||
return self.role == 'user'
|
||||
|
||||
def can_create_users(self) -> bool:
|
||||
"""사용자 생성 권한 확인 (시스템 관리자만)"""
|
||||
return self.is_system()
|
||||
|
||||
def can_view_logs(self) -> bool:
|
||||
"""로그 조회 권한 확인 (관리자 이상)"""
|
||||
return self.is_admin()
|
||||
|
||||
def can_manage_system(self) -> bool:
|
||||
"""시스템 관리 권한 확인 (시스템 관리자만)"""
|
||||
return self.is_system()
|
||||
|
||||
def get_role_display_name(self) -> str:
|
||||
"""역할 표시명 반환"""
|
||||
role_names = {
|
||||
'system': '시스템 관리자',
|
||||
'admin': '관리자',
|
||||
'user': '사용자'
|
||||
}
|
||||
return role_names.get(self.role, '알 수 없음')
|
||||
|
||||
|
||||
class LoginLog(Base):
|
||||
|
||||
198
backend/app/auth/setup_controller.py
Normal file
198
backend/app/auth/setup_controller.py
Normal file
@@ -0,0 +1,198 @@
|
||||
"""
|
||||
초기 시스템 설정 컨트롤러
|
||||
배포 후 첫 실행 시 시스템 관리자 계정 생성
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from pydantic import BaseModel, EmailStr
|
||||
from typing import Optional
|
||||
|
||||
from ..database import get_db
|
||||
from .models import User, UserRepository
|
||||
from ..utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class SystemSetupRequest(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
name: str
|
||||
email: Optional[EmailStr] = None
|
||||
department: Optional[str] = None
|
||||
position: Optional[str] = None
|
||||
|
||||
|
||||
class SystemSetupResponse(BaseModel):
|
||||
success: bool
|
||||
message: str
|
||||
user_id: Optional[int] = None
|
||||
setup_completed: bool
|
||||
|
||||
|
||||
@router.get("/setup/status")
|
||||
async def get_setup_status(db: Session = Depends(get_db)):
|
||||
"""
|
||||
시스템 초기 설정 상태 확인
|
||||
|
||||
Returns:
|
||||
Dict: 설정 완료 여부
|
||||
"""
|
||||
try:
|
||||
user_repo = UserRepository(db)
|
||||
|
||||
# 시스템 관리자가 존재하는지 확인
|
||||
system_admin = db.query(User).filter(User.role == 'system').first()
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'setup_completed': system_admin is not None,
|
||||
'has_system_admin': system_admin is not None,
|
||||
'total_users': db.query(User).count()
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Setup status check error: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="설정 상태 확인 중 오류가 발생했습니다"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/setup/initialize", response_model=SystemSetupResponse)
|
||||
async def initialize_system(
|
||||
setup_data: SystemSetupRequest,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
시스템 초기화 및 첫 번째 시스템 관리자 생성
|
||||
|
||||
Args:
|
||||
setup_data: 시스템 관리자 계정 정보
|
||||
db: 데이터베이스 세션
|
||||
|
||||
Returns:
|
||||
SystemSetupResponse: 설정 결과
|
||||
"""
|
||||
try:
|
||||
user_repo = UserRepository(db)
|
||||
|
||||
# 이미 시스템 관리자가 존재하는지 확인
|
||||
existing_admin = db.query(User).filter(User.role == 'system').first()
|
||||
if existing_admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="시스템이 이미 초기화되었습니다"
|
||||
)
|
||||
|
||||
# 사용자명 중복 확인
|
||||
existing_user = user_repo.find_by_username(setup_data.username)
|
||||
if existing_user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="이미 존재하는 사용자명입니다"
|
||||
)
|
||||
|
||||
# 이메일 중복 확인 (이메일이 제공된 경우)
|
||||
if setup_data.email:
|
||||
existing_email = user_repo.find_by_email(setup_data.email)
|
||||
if existing_email:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="이미 존재하는 이메일입니다"
|
||||
)
|
||||
|
||||
# 시스템 관리자 계정 생성
|
||||
user = User(
|
||||
username=setup_data.username,
|
||||
name=setup_data.name,
|
||||
email=setup_data.email,
|
||||
role='system',
|
||||
access_level='system',
|
||||
department=setup_data.department,
|
||||
position=setup_data.position,
|
||||
is_active=True
|
||||
)
|
||||
|
||||
# 비밀번호 설정
|
||||
user.set_password(setup_data.password)
|
||||
|
||||
# 데이터베이스에 저장
|
||||
db.add(user)
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
|
||||
logger.info(f"System initialized with admin user: {user.username}")
|
||||
|
||||
return SystemSetupResponse(
|
||||
success=True,
|
||||
message="시스템이 성공적으로 초기화되었습니다",
|
||||
user_id=user.user_id,
|
||||
setup_completed=True
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"System initialization error: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="시스템 초기화 중 오류가 발생했습니다"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/setup/reset")
|
||||
async def reset_system_setup(
|
||||
confirm_reset: bool = False,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
시스템 설정 리셋 (개발/테스트 용도)
|
||||
|
||||
Args:
|
||||
confirm_reset: 리셋 확인
|
||||
db: 데이터베이스 세션
|
||||
|
||||
Returns:
|
||||
Dict: 리셋 결과
|
||||
"""
|
||||
try:
|
||||
if not confirm_reset:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="리셋을 확인해주세요 (confirm_reset=true)"
|
||||
)
|
||||
|
||||
# 개발 환경에서만 허용
|
||||
from ..config import get_settings
|
||||
settings = get_settings()
|
||||
|
||||
if settings.environment != 'development':
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="개발 환경에서만 시스템 리셋이 가능합니다"
|
||||
)
|
||||
|
||||
# 모든 사용자 삭제
|
||||
db.query(User).delete()
|
||||
db.commit()
|
||||
|
||||
logger.warning("System setup has been reset (development only)")
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'message': '시스템 설정이 리셋되었습니다',
|
||||
'setup_completed': False
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"System reset error: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="시스템 리셋 중 오류가 발생했습니다"
|
||||
)
|
||||
@@ -108,9 +108,11 @@ logger.info("파일 관리 API 라우터 비활성화됨 (files 라우터 사용
|
||||
|
||||
# 인증 API 라우터 등록
|
||||
try:
|
||||
from .auth import auth_router
|
||||
from .auth import auth_router, setup_router
|
||||
app.include_router(auth_router, prefix="/auth", tags=["authentication"])
|
||||
app.include_router(setup_router, prefix="/setup", tags=["system-setup"])
|
||||
logger.info("인증 API 라우터 등록 완료")
|
||||
logger.info("시스템 설정 API 라우터 등록 완료")
|
||||
except ImportError as e:
|
||||
logger.warning(f"인증 라우터를 찾을 수 없습니다: {e}")
|
||||
|
||||
|
||||
197
backend/scripts/create_system_admin.py
Executable file
197
backend/scripts/create_system_admin.py
Executable file
@@ -0,0 +1,197 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
시스템 관리자 계정 생성 스크립트
|
||||
최초 설치 시 시스템 관리자 계정을 생성합니다.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import getpass
|
||||
from datetime import datetime
|
||||
|
||||
# 프로젝트 루트를 Python 경로에 추가
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from sqlalchemy import create_engine, text
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from app.config import get_settings
|
||||
from app.auth.models import User, UserRepository
|
||||
from app.database import Base
|
||||
|
||||
def create_system_admin():
|
||||
"""시스템 관리자 계정 생성"""
|
||||
|
||||
print("=" * 60)
|
||||
print("🔧 TK-MP 시스템 관리자 계정 생성")
|
||||
print("=" * 60)
|
||||
|
||||
# 설정 로드
|
||||
settings = get_settings()
|
||||
|
||||
# 데이터베이스 연결
|
||||
try:
|
||||
engine = create_engine(settings.database_url)
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
db = SessionLocal()
|
||||
|
||||
print("✅ 데이터베이스 연결 성공")
|
||||
|
||||
# 테이블 생성 (필요한 경우)
|
||||
Base.metadata.create_all(bind=engine)
|
||||
print("✅ 데이터베이스 테이블 확인/생성 완료")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 데이터베이스 연결 실패: {str(e)}")
|
||||
return False
|
||||
|
||||
# 기존 시스템 관리자 확인
|
||||
try:
|
||||
user_repo = UserRepository(db)
|
||||
existing_admin = db.query(User).filter(User.role == 'system').first()
|
||||
|
||||
if existing_admin:
|
||||
print(f"⚠️ 시스템 관리자가 이미 존재합니다: {existing_admin.username}")
|
||||
response = input("새로운 시스템 관리자를 추가로 생성하시겠습니까? (y/N): ").lower()
|
||||
if response != 'y':
|
||||
print("❌ 작업이 취소되었습니다.")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 기존 관리자 확인 실패: {str(e)}")
|
||||
return False
|
||||
|
||||
# 사용자 입력 받기
|
||||
print("\n📝 시스템 관리자 정보를 입력해주세요:")
|
||||
print("-" * 40)
|
||||
|
||||
try:
|
||||
# 사용자명 입력
|
||||
while True:
|
||||
username = input("사용자명 (영문/숫자, 3-20자): ").strip()
|
||||
if not username:
|
||||
print("❌ 사용자명을 입력해주세요.")
|
||||
continue
|
||||
if len(username) < 3 or len(username) > 20:
|
||||
print("❌ 사용자명은 3-20자여야 합니다.")
|
||||
continue
|
||||
if not username.replace('_', '').isalnum():
|
||||
print("❌ 사용자명은 영문, 숫자, 언더스코어만 사용 가능합니다.")
|
||||
continue
|
||||
|
||||
# 중복 확인
|
||||
existing_user = user_repo.find_by_username(username)
|
||||
if existing_user:
|
||||
print("❌ 이미 존재하는 사용자명입니다.")
|
||||
continue
|
||||
|
||||
break
|
||||
|
||||
# 이름 입력
|
||||
while True:
|
||||
name = input("이름 (한글/영문, 2-50자): ").strip()
|
||||
if not name:
|
||||
print("❌ 이름을 입력해주세요.")
|
||||
continue
|
||||
if len(name) < 2 or len(name) > 50:
|
||||
print("❌ 이름은 2-50자여야 합니다.")
|
||||
continue
|
||||
break
|
||||
|
||||
# 이메일 입력 (선택사항)
|
||||
email = input("이메일 (선택사항): ").strip()
|
||||
if email and '@' not in email:
|
||||
print("⚠️ 올바르지 않은 이메일 형식입니다. 빈 값으로 설정합니다.")
|
||||
email = None
|
||||
|
||||
# 비밀번호 입력
|
||||
while True:
|
||||
password = getpass.getpass("비밀번호 (8자 이상): ")
|
||||
if len(password) < 8:
|
||||
print("❌ 비밀번호는 8자 이상이어야 합니다.")
|
||||
continue
|
||||
|
||||
password_confirm = getpass.getpass("비밀번호 확인: ")
|
||||
if password != password_confirm:
|
||||
print("❌ 비밀번호가 일치하지 않습니다.")
|
||||
continue
|
||||
|
||||
break
|
||||
|
||||
# 부서/직책 입력 (선택사항)
|
||||
department = input("부서 (선택사항): ").strip() or None
|
||||
position = input("직책 (선택사항): ").strip() or None
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n❌ 작업이 취소되었습니다.")
|
||||
return False
|
||||
|
||||
# 입력 정보 확인
|
||||
print("\n📋 입력된 정보:")
|
||||
print("-" * 40)
|
||||
print(f"사용자명: {username}")
|
||||
print(f"이름: {name}")
|
||||
print(f"이메일: {email or '(없음)'}")
|
||||
print(f"부서: {department or '(없음)'}")
|
||||
print(f"직책: {position or '(없음)'}")
|
||||
print(f"역할: 시스템 관리자")
|
||||
|
||||
response = input("\n위 정보로 시스템 관리자를 생성하시겠습니까? (y/N): ").lower()
|
||||
if response != 'y':
|
||||
print("❌ 작업이 취소되었습니다.")
|
||||
return False
|
||||
|
||||
# 사용자 생성
|
||||
try:
|
||||
# 사용자 객체 생성
|
||||
user = User(
|
||||
username=username,
|
||||
name=name,
|
||||
email=email,
|
||||
role='system',
|
||||
access_level='system',
|
||||
department=department,
|
||||
position=position,
|
||||
is_active=True
|
||||
)
|
||||
|
||||
# 비밀번호 설정
|
||||
user.set_password(password)
|
||||
|
||||
# 데이터베이스에 저장
|
||||
db.add(user)
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
|
||||
print(f"✅ 시스템 관리자 계정이 생성되었습니다!")
|
||||
print(f" - 사용자 ID: {user.user_id}")
|
||||
print(f" - 사용자명: {user.username}")
|
||||
print(f" - 이름: {user.name}")
|
||||
print(f" - 생성일시: {user.created_at}")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
print(f"❌ 사용자 생성 실패: {str(e)}")
|
||||
return False
|
||||
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def main():
|
||||
"""메인 함수"""
|
||||
try:
|
||||
success = create_system_admin()
|
||||
if success:
|
||||
print("\n🎉 시스템 관리자 계정 생성이 완료되었습니다!")
|
||||
print("이제 이 계정으로 로그인하여 다른 사용자를 관리할 수 있습니다.")
|
||||
else:
|
||||
print("\n❌ 시스템 관리자 계정 생성에 실패했습니다.")
|
||||
sys.exit(1)
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n💥 예상치 못한 오류가 발생했습니다: {str(e)}")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user