EV 충전 플랫폼 초기 백업
This commit is contained in:
219
app/routers/auth.py
Normal file
219
app/routers/auth.py
Normal file
@@ -0,0 +1,219 @@
|
||||
"""인증 + 사용자 관리 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} 삭제 완료"}
|
||||
Reference in New Issue
Block a user