Files
ev-charging-backend/app/routers/auth.py
2026-04-18 05:59:31 +09:00

220 lines
6.0 KiB
Python

"""인증 + 사용자 관리 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} 삭제 완료"}