Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
- H/F/I/O SS304/GRAPHITE/CS/CS 패턴에서 4개 구성요소 모두 표시 - 기존 SS304 + GRAPHITE → SS304/GRAPHITE/CS/CS로 완전한 구성 표시 - 외부링/필러/내부링/추가구성 모든 정보 포함 - 구매수량 계산 모달에서 정확한 재질 정보 확인 가능
373 lines
14 KiB
Python
373 lines
14 KiB
Python
"""
|
|
인증 서비스
|
|
TK-FB-Project의 auth.service.js를 참고하여 FastAPI용으로 구현
|
|
"""
|
|
from typing import Dict, Any, Optional, Tuple
|
|
from datetime import datetime, timedelta
|
|
from fastapi import HTTPException, status, Request
|
|
from sqlalchemy.orm import Session
|
|
|
|
from .models import User, UserRepository
|
|
from .jwt_service import jwt_service
|
|
from ..utils.logger import get_logger
|
|
from ..utils.error_handlers import TKMPException
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
|
|
class AuthService:
|
|
"""인증 서비스 클래스"""
|
|
|
|
def __init__(self, db: Session):
|
|
self.db = db
|
|
self.user_repo = UserRepository(db)
|
|
|
|
async def login(self, username: str, password: str, request: Request) -> Dict[str, Any]:
|
|
"""
|
|
사용자 로그인
|
|
|
|
Args:
|
|
username: 사용자명
|
|
password: 비밀번호
|
|
request: FastAPI Request 객체
|
|
|
|
Returns:
|
|
Dict[str, Any]: 로그인 결과 (토큰, 사용자 정보 등)
|
|
|
|
Raises:
|
|
TKMPException: 로그인 실패 시
|
|
"""
|
|
try:
|
|
# 클라이언트 정보 추출
|
|
ip_address = self._get_client_ip(request)
|
|
user_agent = request.headers.get('user-agent', 'unknown')
|
|
|
|
logger.info(f"Login attempt for username: {username} from IP: {ip_address}")
|
|
|
|
# 입력 검증
|
|
if not username or not password:
|
|
await self._record_login_failure(None, ip_address, user_agent, 'missing_credentials')
|
|
raise TKMPException(
|
|
message="사용자명과 비밀번호를 입력해주세요",
|
|
status_code=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
# 사용자 조회
|
|
user = self.user_repo.find_by_username(username)
|
|
if not user:
|
|
await self._record_login_failure(None, ip_address, user_agent, 'user_not_found')
|
|
logger.warning(f"Login failed - user not found: {username}")
|
|
raise TKMPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
message="아이디 또는 비밀번호가 올바르지 않습니다"
|
|
)
|
|
|
|
# 계정 활성화 상태 확인
|
|
if not user.is_active:
|
|
await self._record_login_failure(user.user_id, ip_address, user_agent, 'account_disabled')
|
|
logger.warning(f"Login failed - account disabled: {username}")
|
|
raise TKMPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
message="비활성화된 계정입니다. 관리자에게 문의하세요"
|
|
)
|
|
|
|
# 계정 잠금 상태 확인
|
|
if user.is_locked():
|
|
remaining_time = int((user.locked_until - datetime.utcnow()).total_seconds() / 60)
|
|
await self._record_login_failure(user.user_id, ip_address, user_agent, 'account_locked')
|
|
logger.warning(f"Login failed - account locked: {username}")
|
|
raise TKMPException(
|
|
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
|
|
message=f"계정이 잠겨있습니다. {remaining_time}분 후에 다시 시도하세요"
|
|
)
|
|
|
|
# 비밀번호 확인
|
|
if not user.check_password(password):
|
|
# 로그인 실패 처리
|
|
user.increment_failed_attempts()
|
|
self.user_repo.update_user(user)
|
|
|
|
await self._record_login_failure(user.user_id, ip_address, user_agent, 'invalid_password')
|
|
logger.warning(f"Login failed - invalid password: {username}")
|
|
|
|
# 계정 잠금 확인
|
|
if user.failed_login_attempts >= 5:
|
|
logger.warning(f"Account locked due to failed attempts: {username}")
|
|
|
|
raise TKMPException(
|
|
message="아이디 또는 비밀번호가 올바르지 않습니다",
|
|
status_code=status.HTTP_401_UNAUTHORIZED
|
|
)
|
|
|
|
# 로그인 성공 처리
|
|
user.reset_failed_attempts()
|
|
user.update_last_login()
|
|
self.user_repo.update_user(user)
|
|
|
|
# 토큰 생성
|
|
user_data = user.to_dict()
|
|
access_token = jwt_service.create_access_token(user_data)
|
|
refresh_token = jwt_service.create_refresh_token(user.user_id)
|
|
|
|
# 세션 생성
|
|
expires_at = datetime.utcnow() + timedelta(days=7)
|
|
session = self.user_repo.create_session(
|
|
user_id=user.user_id,
|
|
refresh_token=refresh_token,
|
|
expires_at=expires_at,
|
|
ip_address=ip_address,
|
|
user_agent=user_agent
|
|
)
|
|
|
|
# 로그인 성공 기록
|
|
self.user_repo.record_login_log(
|
|
user_id=user.user_id,
|
|
ip_address=ip_address,
|
|
user_agent=user_agent,
|
|
status='success'
|
|
)
|
|
|
|
# 리디렉션 URL 결정
|
|
redirect_url = self._get_redirect_url(user.role)
|
|
|
|
logger.info(f"Login successful for user: {username} (role: {user.role})")
|
|
|
|
return {
|
|
'success': True,
|
|
'access_token': access_token,
|
|
'refresh_token': refresh_token,
|
|
'token_type': 'bearer',
|
|
'expires_in': 24 * 3600, # 24시간 (초)
|
|
'user': user_data,
|
|
'redirect_url': redirect_url,
|
|
'permissions': self.user_repo.get_user_permissions(user.role)
|
|
}
|
|
|
|
except TKMPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Login service error for {username}: {str(e)}")
|
|
raise TKMPException(
|
|
message="로그인 처리 중 서버 오류가 발생했습니다",
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
)
|
|
|
|
async def refresh_token(self, refresh_token: str, request: Request) -> Dict[str, Any]:
|
|
"""
|
|
토큰 갱신
|
|
|
|
Args:
|
|
refresh_token: 리프레시 토큰
|
|
request: FastAPI Request 객체
|
|
|
|
Returns:
|
|
Dict[str, Any]: 새로운 토큰 정보
|
|
"""
|
|
try:
|
|
# 리프레시 토큰 검증
|
|
payload = jwt_service.verify_refresh_token(refresh_token)
|
|
user_id = payload['user_id']
|
|
|
|
# 세션 확인
|
|
session = self.user_repo.find_session_by_token(refresh_token)
|
|
if not session or session.is_expired() or not session.is_active:
|
|
logger.warning(f"Invalid or expired refresh token for user_id: {user_id}")
|
|
raise TKMPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
message="유효하지 않거나 만료된 리프레시 토큰입니다"
|
|
)
|
|
|
|
# 사용자 조회
|
|
user = self.user_repo.find_by_id(user_id)
|
|
if not user or not user.is_active:
|
|
logger.warning(f"User not found or inactive for token refresh: {user_id}")
|
|
raise TKMPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
message="사용자를 찾을 수 없거나 비활성화된 계정입니다"
|
|
)
|
|
|
|
# 새 액세스 토큰 생성
|
|
user_data = user.to_dict()
|
|
new_access_token = jwt_service.create_access_token(user_data)
|
|
|
|
logger.info(f"Token refreshed for user: {user.username}")
|
|
|
|
return {
|
|
'success': True,
|
|
'access_token': new_access_token,
|
|
'token_type': 'bearer',
|
|
'expires_in': 24 * 3600,
|
|
'user': user_data
|
|
}
|
|
|
|
except TKMPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Token refresh error: {str(e)}")
|
|
raise TKMPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
message="토큰 갱신 중 서버 오류가 발생했습니다"
|
|
)
|
|
|
|
async def logout(self, refresh_token: str) -> Dict[str, Any]:
|
|
"""
|
|
로그아웃
|
|
|
|
Args:
|
|
refresh_token: 리프레시 토큰
|
|
|
|
Returns:
|
|
Dict[str, Any]: 로그아웃 결과
|
|
"""
|
|
try:
|
|
# 세션 찾기 및 비활성화
|
|
session = self.user_repo.find_session_by_token(refresh_token)
|
|
if session:
|
|
session.deactivate()
|
|
self.user_repo.update_user(session.user)
|
|
logger.info(f"User logged out: user_id {session.user_id}")
|
|
|
|
return {
|
|
'success': True,
|
|
'message': '로그아웃되었습니다'
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Logout error: {str(e)}")
|
|
raise TKMPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
message="로그아웃 처리 중 오류가 발생했습니다"
|
|
)
|
|
|
|
async def register(self, user_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""
|
|
사용자 등록
|
|
|
|
Args:
|
|
user_data: 사용자 정보
|
|
|
|
Returns:
|
|
Dict[str, Any]: 등록 결과
|
|
"""
|
|
try:
|
|
# 필수 필드 검증
|
|
required_fields = ['username', 'password', 'name']
|
|
for field in required_fields:
|
|
if not user_data.get(field):
|
|
raise TKMPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
message=f"{field}는 필수 입력 항목입니다"
|
|
)
|
|
|
|
# 중복 사용자명 확인
|
|
existing_user = self.user_repo.find_by_username(user_data['username'])
|
|
if existing_user:
|
|
raise TKMPException(
|
|
status_code=status.HTTP_409_CONFLICT,
|
|
message="이미 존재하는 사용자명입니다"
|
|
)
|
|
|
|
# 이메일 중복 확인 (이메일이 제공된 경우)
|
|
if user_data.get('email'):
|
|
existing_email = self.user_repo.find_by_email(user_data['email'])
|
|
if existing_email:
|
|
raise TKMPException(
|
|
status_code=status.HTTP_409_CONFLICT,
|
|
message="이미 사용 중인 이메일입니다"
|
|
)
|
|
|
|
# 역할 매핑
|
|
role_map = {
|
|
'admin': 'admin',
|
|
'system': 'system',
|
|
'group_leader': 'leader',
|
|
'support_team': 'support',
|
|
'worker': 'user'
|
|
}
|
|
access_level = user_data.get('access_level', 'worker')
|
|
role = role_map.get(access_level, 'user')
|
|
|
|
# 사용자 생성
|
|
new_user_data = {
|
|
'username': user_data['username'],
|
|
'name': user_data['name'],
|
|
'email': user_data.get('email'),
|
|
'role': role,
|
|
'access_level': access_level,
|
|
'department': user_data.get('department'),
|
|
'position': user_data.get('position'),
|
|
'phone': user_data.get('phone')
|
|
}
|
|
|
|
user = User(**new_user_data)
|
|
user.set_password(user_data['password'])
|
|
|
|
self.db.add(user)
|
|
self.db.commit()
|
|
self.db.refresh(user)
|
|
|
|
logger.info(f"User registered successfully: {user.username}")
|
|
|
|
return {
|
|
'success': True,
|
|
'message': '사용자 등록이 완료되었습니다',
|
|
'user_id': user.user_id,
|
|
'username': user.username
|
|
}
|
|
|
|
except TKMPException:
|
|
raise
|
|
except Exception as e:
|
|
self.db.rollback()
|
|
logger.error(f"User registration error: {str(e)}")
|
|
raise TKMPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
message="사용자 등록 중 서버 오류가 발생했습니다"
|
|
)
|
|
|
|
def _get_client_ip(self, request: Request) -> str:
|
|
"""클라이언트 IP 주소 추출"""
|
|
# X-Forwarded-For 헤더 확인 (프록시 환경)
|
|
forwarded_for = request.headers.get('x-forwarded-for')
|
|
if forwarded_for:
|
|
return forwarded_for.split(',')[0].strip()
|
|
|
|
# X-Real-IP 헤더 확인
|
|
real_ip = request.headers.get('x-real-ip')
|
|
if real_ip:
|
|
return real_ip
|
|
|
|
# 직접 연결된 클라이언트 IP
|
|
return request.client.host if request.client else 'unknown'
|
|
|
|
def _get_redirect_url(self, role: str) -> str:
|
|
"""역할에 따른 리디렉션 URL 결정"""
|
|
redirect_urls = {
|
|
'system': '/admin/system',
|
|
'admin': '/admin/dashboard',
|
|
'leader': '/dashboard/leader',
|
|
'support': '/dashboard/support',
|
|
'user': '/dashboard'
|
|
}
|
|
return redirect_urls.get(role, '/dashboard')
|
|
|
|
async def _record_login_failure(self, user_id: Optional[int], ip_address: str,
|
|
user_agent: str, failure_reason: str):
|
|
"""로그인 실패 기록"""
|
|
try:
|
|
if user_id:
|
|
self.user_repo.record_login_log(
|
|
user_id=user_id,
|
|
ip_address=ip_address,
|
|
user_agent=user_agent,
|
|
status='failed',
|
|
failure_reason=failure_reason
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Failed to record login failure: {str(e)}")
|
|
|
|
|
|
def get_auth_service(db: Session) -> AuthService:
|
|
"""인증 서비스 팩토리 함수"""
|
|
return AuthService(db)
|