feat: OAuth 2.0 백엔드 시스템 구현 완료

Phase 1 & 2 완료:
- 프로젝트 기본 구조 설정
- Docker Compose 환경 구성 (MongoDB, Redis, Backend, Frontend)
- FastAPI 기반 OAuth 2.0 백엔드 구현

주요 기능:
- JWT 기반 인증 시스템
- 3단계 권한 체계 (System Admin/Group Admin/User)
- 사용자 관리 CRUD API
- 애플리케이션 관리 CRUD API
- OAuth 2.0 Authorization Code Flow
- Refresh Token 관리
- 인증 히스토리 추적

API 엔드포인트:
- /auth/* - 인증 관련 (register, login, logout, refresh)
- /users/* - 사용자 관리
- /applications/* - 애플리케이션 관리
- /oauth/* - OAuth 2.0 플로우

보안 기능:
- bcrypt 비밀번호 해싱
- JWT 토큰 인증
- CORS 설정
- Rate limiting 준비

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
jungwoo choi
2025-09-05 14:56:02 +09:00
parent abdcc31245
commit 6c21809a24
25 changed files with 2012 additions and 45 deletions

View File

@ -0,0 +1,156 @@
"""Users management router"""
from fastapi import APIRouter, Depends, HTTPException, status, Query
from typing import List, Optional
from bson import ObjectId
from app.models.user import User, UserUpdate, UserRole
from app.routers.auth import get_current_user
from app.utils.database import get_database
from app.utils.security import hash_password
from datetime import datetime
router = APIRouter(prefix="/users", tags=["Users"])
def require_admin(current_user: User = Depends(get_current_user)) -> User:
"""Require admin role"""
if current_user.role not in [UserRole.SYSTEM_ADMIN, UserRole.GROUP_ADMIN]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions"
)
return current_user
@router.get("/", response_model=List[User])
async def get_users(
skip: int = Query(0, ge=0),
limit: int = Query(10, ge=1, le=100),
current_user: User = Depends(require_admin)
):
"""Get all users (admin only)"""
db = get_database()
users = await db.users.find().skip(skip).limit(limit).to_list(limit)
return [User(**user) for user in users]
@router.get("/{user_id}", response_model=User)
async def get_user(user_id: str, current_user: User = Depends(get_current_user)):
"""Get user by ID"""
# Users can only view their own profile unless they're admin
if str(current_user.id) != user_id and current_user.role not in [UserRole.SYSTEM_ADMIN, UserRole.GROUP_ADMIN]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions"
)
db = get_database()
try:
user = await db.users.find_one({"_id": ObjectId(user_id)})
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
return User(**user)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid user ID"
)
@router.put("/{user_id}", response_model=User)
async def update_user(
user_id: str,
user_update: UserUpdate,
current_user: User = Depends(get_current_user)
):
"""Update user"""
# Users can only update their own profile unless they're admin
if str(current_user.id) != user_id and current_user.role not in [UserRole.SYSTEM_ADMIN, UserRole.GROUP_ADMIN]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions"
)
# Only system admin can change roles
if user_update.role and current_user.role != UserRole.SYSTEM_ADMIN:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Only system admin can change user roles"
)
db = get_database()
try:
# Prepare update data
update_data = user_update.model_dump(exclude_unset=True)
# Hash password if provided
if "password" in update_data:
update_data["hashed_password"] = hash_password(update_data.pop("password"))
# Update timestamp
update_data["updated_at"] = datetime.utcnow()
# Update user
result = await db.users.update_one(
{"_id": ObjectId(user_id)},
{"$set": update_data}
)
if result.modified_count == 0:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
# Return updated user
user = await db.users.find_one({"_id": ObjectId(user_id)})
return User(**user)
except Exception as e:
if isinstance(e, HTTPException):
raise
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
@router.delete("/{user_id}")
async def delete_user(
user_id: str,
current_user: User = Depends(require_admin)
):
"""Delete user (admin only)"""
# Prevent self-deletion
if str(current_user.id) == user_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Cannot delete your own account"
)
db = get_database()
try:
result = await db.users.delete_one({"_id": ObjectId(user_id)})
if result.deleted_count == 0:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
return {"message": "User deleted successfully"}
except Exception as e:
if isinstance(e, HTTPException):
raise
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid user ID"
)