Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
- 파이프 수량 계산 로직 수정 (단관 개수가 아닌 실제 길이 기반 계산) - UI 전면 개편 (DevonThink 스타일의 간결하고 세련된 디자인) - 자재별 그룹핑 로직 개선: * 플랜지: 동일 사양별 그룹핑, WN 스케줄 표시, ORIFICE 풀네임 표시 * 피팅: 상세 타입 표시 (니플 길이, 엘보 각도/연결, 티 타입, 리듀서 타입 등) * 밸브: 동일 사양별 그룹핑, 타입/연결방식/압력 표시 * 볼트: 크기/재질/길이별 그룹핑 (8SET → 개별 집계) * 가스켓: 동일 사양별 그룹핑, 재질/상세내역/두께 분리 표시 * UNKNOWN: 원본 설명 전체 표시, 동일 항목 그룹핑 - 전체 카테고리 버튼 제거 (표시 복잡도 감소) - 카테고리별 동적 컬럼 헤더 및 레이아웃 적용
410 lines
10 KiB
Python
410 lines
10 KiB
Python
"""
|
|
인증 컨트롤러
|
|
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 typing import Optional, List, Dict, Any
|
|
|
|
from ..database import get_db
|
|
from .auth_service import get_auth_service
|
|
from .jwt_service import jwt_service
|
|
from .models import UserRepository
|
|
from ..utils.logger import get_logger
|
|
|
|
logger = get_logger(__name__)
|
|
router = APIRouter()
|
|
security = HTTPBearer()
|
|
|
|
|
|
# Pydantic 모델들
|
|
class LoginRequest(BaseModel):
|
|
username: str
|
|
password: str
|
|
|
|
|
|
class RegisterRequest(BaseModel):
|
|
username: str
|
|
password: str
|
|
name: str
|
|
email: Optional[EmailStr] = None
|
|
access_level: str = 'worker'
|
|
department: Optional[str] = None
|
|
position: Optional[str] = None
|
|
phone: Optional[str] = None
|
|
|
|
|
|
class RefreshTokenRequest(BaseModel):
|
|
refresh_token: str
|
|
|
|
|
|
class LoginResponse(BaseModel):
|
|
success: bool
|
|
access_token: str
|
|
refresh_token: str
|
|
token_type: str
|
|
expires_in: int
|
|
user: Dict[str, Any]
|
|
redirect_url: str
|
|
permissions: List[str]
|
|
|
|
|
|
class RefreshTokenResponse(BaseModel):
|
|
success: bool
|
|
access_token: str
|
|
token_type: str
|
|
expires_in: int
|
|
user: Dict[str, Any]
|
|
|
|
|
|
class RegisterResponse(BaseModel):
|
|
success: bool
|
|
message: str
|
|
user_id: int
|
|
username: str
|
|
|
|
|
|
class LogoutResponse(BaseModel):
|
|
success: bool
|
|
message: str
|
|
|
|
|
|
class UserInfoResponse(BaseModel):
|
|
success: bool
|
|
user: Dict[str, Any]
|
|
permissions: List[str]
|
|
|
|
|
|
@router.post("/login", response_model=LoginResponse)
|
|
async def login(
|
|
login_data: LoginRequest,
|
|
request: Request,
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
사용자 로그인
|
|
|
|
Args:
|
|
login_data: 로그인 정보 (사용자명, 비밀번호)
|
|
request: FastAPI Request 객체
|
|
db: 데이터베이스 세션
|
|
|
|
Returns:
|
|
LoginResponse: 로그인 결과 (토큰, 사용자 정보 등)
|
|
"""
|
|
try:
|
|
auth_service = get_auth_service(db)
|
|
result = await auth_service.login(
|
|
username=login_data.username,
|
|
password=login_data.password,
|
|
request=request
|
|
)
|
|
|
|
return LoginResponse(**result)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Login endpoint error: {str(e)}")
|
|
raise
|
|
|
|
|
|
@router.post("/register", response_model=RegisterResponse)
|
|
async def register(
|
|
register_data: RegisterRequest,
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
사용자 등록
|
|
|
|
Args:
|
|
register_data: 등록 정보
|
|
db: 데이터베이스 세션
|
|
|
|
Returns:
|
|
RegisterResponse: 등록 결과
|
|
"""
|
|
try:
|
|
auth_service = get_auth_service(db)
|
|
result = await auth_service.register(register_data.dict())
|
|
|
|
return RegisterResponse(**result)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Register endpoint error: {str(e)}")
|
|
raise
|
|
|
|
|
|
@router.post("/refresh", response_model=RefreshTokenResponse)
|
|
async def refresh_token(
|
|
refresh_data: RefreshTokenRequest,
|
|
request: Request,
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
토큰 갱신
|
|
|
|
Args:
|
|
refresh_data: 리프레시 토큰 정보
|
|
request: FastAPI Request 객체
|
|
db: 데이터베이스 세션
|
|
|
|
Returns:
|
|
RefreshTokenResponse: 새로운 토큰 정보
|
|
"""
|
|
try:
|
|
auth_service = get_auth_service(db)
|
|
result = await auth_service.refresh_token(
|
|
refresh_token=refresh_data.refresh_token,
|
|
request=request
|
|
)
|
|
|
|
return RefreshTokenResponse(**result)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Refresh token endpoint error: {str(e)}")
|
|
raise
|
|
|
|
|
|
@router.post("/logout", response_model=LogoutResponse)
|
|
async def logout(
|
|
refresh_data: RefreshTokenRequest,
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
로그아웃
|
|
|
|
Args:
|
|
refresh_data: 리프레시 토큰 정보
|
|
db: 데이터베이스 세션
|
|
|
|
Returns:
|
|
LogoutResponse: 로그아웃 결과
|
|
"""
|
|
try:
|
|
auth_service = get_auth_service(db)
|
|
result = await auth_service.logout(refresh_data.refresh_token)
|
|
|
|
return LogoutResponse(**result)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Logout endpoint error: {str(e)}")
|
|
raise
|
|
|
|
|
|
@router.get("/me", response_model=UserInfoResponse)
|
|
async def get_current_user_info(
|
|
credentials: HTTPAuthorizationCredentials = Depends(security),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
현재 사용자 정보 조회
|
|
|
|
Args:
|
|
credentials: JWT 토큰
|
|
db: 데이터베이스 세션
|
|
|
|
Returns:
|
|
UserInfoResponse: 사용자 정보 및 권한
|
|
"""
|
|
try:
|
|
# 토큰 검증
|
|
payload = jwt_service.verify_access_token(credentials.credentials)
|
|
user_id = payload['user_id']
|
|
|
|
# 사용자 정보 조회
|
|
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_401_UNAUTHORIZED,
|
|
detail="사용자를 찾을 수 없거나 비활성화된 계정입니다"
|
|
)
|
|
|
|
# 권한 정보 조회
|
|
permissions = user_repo.get_user_permissions(user.role)
|
|
|
|
return UserInfoResponse(
|
|
success=True,
|
|
user=user.to_dict(),
|
|
permissions=permissions
|
|
)
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Get current user info error: {str(e)}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail="사용자 정보 조회 중 오류가 발생했습니다"
|
|
)
|
|
|
|
|
|
@router.get("/verify")
|
|
async def verify_token(
|
|
credentials: HTTPAuthorizationCredentials = Depends(security)
|
|
):
|
|
"""
|
|
토큰 검증
|
|
|
|
Args:
|
|
credentials: JWT 토큰
|
|
|
|
Returns:
|
|
Dict: 토큰 검증 결과
|
|
"""
|
|
try:
|
|
payload = jwt_service.verify_access_token(credentials.credentials)
|
|
|
|
return {
|
|
'success': True,
|
|
'valid': True,
|
|
'user_id': payload['user_id'],
|
|
'username': payload['username'],
|
|
'role': payload['role'],
|
|
'expires_at': payload.get('exp')
|
|
}
|
|
|
|
except HTTPException as e:
|
|
return {
|
|
'success': False,
|
|
'valid': False,
|
|
'error': e.detail
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Token verification error: {str(e)}")
|
|
return {
|
|
'success': False,
|
|
'valid': False,
|
|
'error': '토큰 검증 중 오류가 발생했습니다'
|
|
}
|
|
|
|
|
|
# 관리자 전용 엔드포인트들
|
|
@router.get("/users")
|
|
async def get_all_users(
|
|
skip: int = 0,
|
|
limit: int = 100,
|
|
credentials: HTTPAuthorizationCredentials = Depends(security),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
모든 사용자 목록 조회 (관리자 전용)
|
|
|
|
Args:
|
|
skip: 건너뛸 레코드 수
|
|
limit: 조회할 레코드 수
|
|
credentials: JWT 토큰
|
|
db: 데이터베이스 세션
|
|
|
|
Returns:
|
|
Dict: 사용자 목록
|
|
"""
|
|
try:
|
|
# 토큰 검증 및 권한 확인
|
|
payload = jwt_service.verify_access_token(credentials.credentials)
|
|
if payload['role'] not in ['admin', 'system']:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="관리자 권한이 필요합니다"
|
|
)
|
|
|
|
# 사용자 목록 조회
|
|
user_repo = UserRepository(db)
|
|
users = user_repo.get_all_users(skip=skip, limit=limit)
|
|
|
|
return {
|
|
'success': True,
|
|
'users': [user.to_dict() for user in users],
|
|
'total_count': len(users)
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Get all users error: {str(e)}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail="사용자 목록 조회 중 오류가 발생했습니다"
|
|
)
|
|
|
|
|
|
@router.delete("/users/{user_id}")
|
|
async def delete_user(
|
|
user_id: int,
|
|
credentials: HTTPAuthorizationCredentials = Depends(security),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
사용자 삭제 (관리자 전용)
|
|
|
|
Args:
|
|
user_id: 삭제할 사용자 ID
|
|
credentials: JWT 토큰
|
|
db: 데이터베이스 세션
|
|
|
|
Returns:
|
|
Dict: 삭제 결과
|
|
"""
|
|
try:
|
|
# 토큰 검증 및 권한 확인
|
|
payload = jwt_service.verify_access_token(credentials.credentials)
|
|
if payload['role'] not in ['admin', 'system']:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="관리자 권한이 필요합니다"
|
|
)
|
|
|
|
# 자기 자신 삭제 방지
|
|
if payload['user_id'] == user_id:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="자기 자신은 삭제할 수 없습니다"
|
|
)
|
|
|
|
# 사용자 조회 및 삭제
|
|
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="해당 사용자를 찾을 수 없습니다"
|
|
)
|
|
|
|
user_repo.delete_user(user)
|
|
|
|
logger.info(f"User deleted by admin: {user.username} (deleted by: {payload['username']})")
|
|
|
|
return {
|
|
'success': True,
|
|
'message': '사용자가 삭제되었습니다',
|
|
'deleted_user_id': user_id
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Delete user error: {str(e)}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail="사용자 삭제 중 오류가 발생했습니다"
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|