"""인증 + 사용자 관리 API 로그인 → JWT 토큰 발급 → 대시보드 접근 관리자만 사용자 생성/수정/삭제 가능 """ import logging from datetime import datetime, timezone from typing import List, Optional from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel, Field from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.database import get_db from app.models import User, UserRole from app.services.auth import ( hash_password, verify_password, create_token, get_current_user, require_admin, ) logger = logging.getLogger(__name__) router = APIRouter(prefix="/auth", tags=["인증"]) # ── 스키마 ── class LoginRequest(BaseModel): username: str password: str class LoginResponse(BaseModel): access_token: str token_type: str = "bearer" user: dict class UserCreate(BaseModel): username: str = Field(..., min_length=3, max_length=64) password: str = Field(..., min_length=4) display_name: str = "" role: str = "viewer" class UserUpdate(BaseModel): display_name: Optional[str] = None role: Optional[str] = None is_active: Optional[bool] = None password: Optional[str] = None class UserOut(BaseModel): id: int username: str display_name: Optional[str] role: str is_active: bool last_login: Optional[datetime] created_at: datetime class Config: from_attributes = True # ── 로그인 ── @router.post("/login", response_model=LoginResponse) async def login( data: LoginRequest, db: AsyncSession = Depends(get_db), ): """로그인 → JWT 토큰 발급""" result = await db.execute( select(User).where(User.username == data.username) ) user = result.scalar_one_or_none() if not user or not verify_password(data.password, user.hashed_password): raise HTTPException(401, "아이디 또는 비밀번호가 올바르지 않습니다") if not user.is_active: raise HTTPException(403, "비활성화된 계정입니다") # 마지막 로그인 시간 갱신 user.last_login = datetime.now(timezone.utc) token = create_token(user.id, user.username, user.role) logger.info(f"로그인: {user.username} ({user.role})") return LoginResponse( access_token=token, user={ "id": user.id, "username": user.username, "display_name": user.display_name or user.username, "role": user.role, }, ) # ── 내 정보 ── @router.get("/me", response_model=UserOut) async def get_me(user: User = Depends(get_current_user)): """현재 로그인 사용자 정보""" return user @router.put("/me/password") async def change_my_password( old_password: str, new_password: str, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """비밀번호 변경""" if not verify_password(old_password, user.hashed_password): raise HTTPException(400, "현재 비밀번호가 틀립니다") user.hashed_password = hash_password(new_password) return {"message": "비밀번호 변경 완료"} # ── 사용자 관리 (관리자 전용) ── @router.get("/users", response_model=List[UserOut]) async def list_users( admin: User = Depends(require_admin), db: AsyncSession = Depends(get_db), ): """사용자 목록 (관리자 전용)""" result = await db.execute( select(User).order_by(User.created_at) ) return result.scalars().all() @router.post("/users", response_model=UserOut, status_code=201) async def create_user( data: UserCreate, admin: User = Depends(require_admin), db: AsyncSession = Depends(get_db), ): """사용자 생성 (관리자 전용)""" # 중복 확인 existing = await db.execute( select(User).where(User.username == data.username) ) if existing.scalar_one_or_none(): raise HTTPException(409, f"이미 존재하는 아이디: {data.username}") # 역할 검증 valid_roles = [r.value for r in UserRole] if data.role not in valid_roles: raise HTTPException(400, f"유효하지 않은 역할: {data.role} (가능: {valid_roles})") user = User( username=data.username, hashed_password=hash_password(data.password), display_name=data.display_name or data.username, role=data.role, ) db.add(user) await db.flush() await db.refresh(user) logger.info(f"사용자 생성: {user.username} ({user.role}) by {admin.username}") return user @router.put("/users/{user_id}", response_model=UserOut) async def update_user( user_id: int, data: UserUpdate, admin: User = Depends(require_admin), db: AsyncSession = Depends(get_db), ): """사용자 수정 (관리자 전용)""" result = await db.execute(select(User).where(User.id == user_id)) user = result.scalar_one_or_none() if not user: raise HTTPException(404, "사용자 없음") if data.display_name is not None: user.display_name = data.display_name if data.role is not None: user.role = data.role if data.is_active is not None: user.is_active = data.is_active if data.password is not None: user.hashed_password = hash_password(data.password) logger.info(f"사용자 수정: {user.username} by {admin.username}") await db.flush() await db.refresh(user) return user @router.delete("/users/{user_id}") async def delete_user( user_id: int, admin: User = Depends(require_admin), db: AsyncSession = Depends(get_db), ): """사용자 삭제 (관리자 전용)""" result = await db.execute(select(User).where(User.id == user_id)) user = result.scalar_one_or_none() if not user: raise HTTPException(404, "사용자 없음") if user.id == admin.id: raise HTTPException(400, "자기 자신은 삭제할 수 없습니다") await db.delete(user) logger.info(f"사용자 삭제: {user.username} by {admin.username}") return {"message": f"{user.username} 삭제 완료"}