feat: Complete backend implementation - Users, Applications, Monitoring
Phase 1 Backend 100% 완료: ✅ UserService (312 lines): - 인증 시스템 (authenticate_user, JWT 토큰 생성) - CRUD 전체 기능 (get, create, update, delete) - 권한 기반 필터링 (role, disabled, search) - 비밀번호 관리 (change_password, hash 검증) - 상태 토글 및 통계 조회 ✅ ApplicationService (254 lines): - OAuth2 클라이언트 관리 - Client ID/Secret 자동 생성 - Secret 재생성 기능 - 소유권 검증 (ownership check) - 통계 조회 (grant types별) ✅ MonitoringService (309 lines): - 시스템 헬스 체크 (MongoDB, pipelines) - 시스템 메트릭 (keywords, pipelines, users, apps) - 활동 로그 조회 (필터링, 날짜 범위) - 데이터베이스 통계 (크기, 컬렉션, 인덱스) - 파이프라인 성능 분석 - 에러 요약 ✅ Users API (11 endpoints + OAuth2 로그인): - POST /login - OAuth2 password flow - GET /me - 현재 사용자 정보 - GET / - 사용자 목록 (admin only) - GET /stats - 사용자 통계 (admin only) - GET /{id} - 사용자 조회 (자신 or admin) - POST / - 사용자 생성 (admin only) - PUT /{id} - 사용자 수정 (권한 검증) - DELETE /{id} - 사용자 삭제 (admin only, 자기 삭제 방지) - POST /{id}/toggle - 상태 토글 (admin only) - POST /change-password - 비밀번호 변경 ✅ Applications API (7 endpoints): - GET / - 애플리케이션 목록 (admin: 전체, user: 자신 것만) - GET /stats - 통계 (admin only) - GET /{id} - 조회 (소유자 or admin) - POST / - 생성 (client_secret 1회만 표시) - PUT /{id} - 수정 (소유자 or admin) - DELETE /{id} - 삭제 (소유자 or admin) - POST /{id}/regenerate-secret - Secret 재생성 ✅ Monitoring API (8 endpoints): - GET /health - 시스템 헬스 상태 - GET /metrics - 시스템 메트릭 - GET /logs - 활동 로그 (필터링 지원) - GET /database/stats - DB 통계 (admin only) - GET /database/collections - 컬렉션 통계 (admin only) - GET /pipelines/performance - 파이프라인 성능 - GET /errors/summary - 에러 요약 주요 특징: - 🔐 역할 기반 접근 제어 (RBAC: admin/editor/viewer) - 🔒 OAuth2 Password Flow 인증 - 🛡️ 소유권 검증 (자신의 리소스만 수정) - 🚫 안전 장치 (자기 삭제 방지, 자기 비활성화 방지) - 📊 종합적인 모니터링 및 통계 - 🔑 안전한 Secret 관리 (1회만 표시) - ✅ 완전한 에러 핸들링 Backend API 총 45개 엔드포인트 완성! - Keywords: 8 - Pipelines: 11 - Users: 11 - Applications: 7 - Monitoring: 8 다음 단계: - Frontend 구현 (React + TypeScript + Material-UI) - Docker & Kubernetes 배포 - Redis 통합 - 테스트 작성 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@ -1,14 +1,284 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from typing import List
|
||||
|
||||
from app.core.auth import get_current_active_user, User
|
||||
from app.core.database import get_database
|
||||
from app.services.application_service import ApplicationService
|
||||
from app.services.user_service import UserService
|
||||
from app.schemas.application import (
|
||||
ApplicationCreate,
|
||||
ApplicationUpdate,
|
||||
ApplicationResponse,
|
||||
ApplicationWithSecret
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/")
|
||||
async def get_applications(current_user: User = Depends(get_current_active_user)):
|
||||
"""Get all OAuth2 applications"""
|
||||
return {"applications": [], "total": 0}
|
||||
|
||||
@router.post("/")
|
||||
async def create_application(app_data: dict, current_user: User = Depends(get_current_active_user)):
|
||||
"""Create new OAuth2 application"""
|
||||
return {"message": "Application created"}
|
||||
def get_application_service(db=Depends(get_database)) -> ApplicationService:
|
||||
"""Dependency to get application service"""
|
||||
return ApplicationService(db)
|
||||
|
||||
|
||||
def get_user_service(db=Depends(get_database)) -> UserService:
|
||||
"""Dependency to get user service"""
|
||||
return UserService(db)
|
||||
|
||||
|
||||
@router.get("/", response_model=List[ApplicationResponse])
|
||||
async def get_applications(
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
app_service: ApplicationService = Depends(get_application_service),
|
||||
user_service: UserService = Depends(get_user_service)
|
||||
):
|
||||
"""
|
||||
Get all OAuth2 applications
|
||||
|
||||
- Admins can see all applications
|
||||
- Regular users can only see their own applications
|
||||
"""
|
||||
# Get current user from database
|
||||
user = await user_service.get_user_by_username(current_user.username)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="User not found"
|
||||
)
|
||||
|
||||
# Admins can see all, others only their own
|
||||
if current_user.role == "admin":
|
||||
applications = await app_service.get_applications()
|
||||
else:
|
||||
applications = await app_service.get_applications(owner_id=str(user.id))
|
||||
|
||||
return [
|
||||
ApplicationResponse(
|
||||
_id=str(app.id),
|
||||
name=app.name,
|
||||
client_id=app.client_id,
|
||||
redirect_uris=app.redirect_uris,
|
||||
grant_types=app.grant_types,
|
||||
scopes=app.scopes,
|
||||
owner_id=app.owner_id,
|
||||
created_at=app.created_at,
|
||||
updated_at=app.updated_at
|
||||
)
|
||||
for app in applications
|
||||
]
|
||||
|
||||
|
||||
@router.get("/stats")
|
||||
async def get_application_stats(
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
app_service: ApplicationService = Depends(get_application_service)
|
||||
):
|
||||
"""Get application statistics (admin only)"""
|
||||
if current_user.role != "admin":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Only admins can view application statistics"
|
||||
)
|
||||
|
||||
stats = await app_service.get_application_stats()
|
||||
return stats
|
||||
|
||||
|
||||
@router.get("/{app_id}", response_model=ApplicationResponse)
|
||||
async def get_application(
|
||||
app_id: str,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
app_service: ApplicationService = Depends(get_application_service),
|
||||
user_service: UserService = Depends(get_user_service)
|
||||
):
|
||||
"""Get an application by ID"""
|
||||
app = await app_service.get_application_by_id(app_id)
|
||||
if not app:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Application with ID {app_id} not found"
|
||||
)
|
||||
|
||||
# Check ownership (admins can view all)
|
||||
user = await user_service.get_user_by_username(current_user.username)
|
||||
if current_user.role != "admin" and app.owner_id != str(user.id):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Not authorized to view this application"
|
||||
)
|
||||
|
||||
return ApplicationResponse(
|
||||
_id=str(app.id),
|
||||
name=app.name,
|
||||
client_id=app.client_id,
|
||||
redirect_uris=app.redirect_uris,
|
||||
grant_types=app.grant_types,
|
||||
scopes=app.scopes,
|
||||
owner_id=app.owner_id,
|
||||
created_at=app.created_at,
|
||||
updated_at=app.updated_at
|
||||
)
|
||||
|
||||
|
||||
@router.post("/", response_model=ApplicationWithSecret, status_code=status.HTTP_201_CREATED)
|
||||
async def create_application(
|
||||
app_data: ApplicationCreate,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
app_service: ApplicationService = Depends(get_application_service),
|
||||
user_service: UserService = Depends(get_user_service)
|
||||
):
|
||||
"""
|
||||
Create a new OAuth2 application
|
||||
|
||||
Returns the application with the client_secret (only shown once!)
|
||||
"""
|
||||
# Get current user from database
|
||||
user = await user_service.get_user_by_username(current_user.username)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="User not found"
|
||||
)
|
||||
|
||||
app, client_secret = await app_service.create_application(
|
||||
app_data=app_data,
|
||||
owner_id=str(user.id)
|
||||
)
|
||||
|
||||
return ApplicationWithSecret(
|
||||
_id=str(app.id),
|
||||
name=app.name,
|
||||
client_id=app.client_id,
|
||||
client_secret=client_secret, # Plain text secret (only shown once)
|
||||
redirect_uris=app.redirect_uris,
|
||||
grant_types=app.grant_types,
|
||||
scopes=app.scopes,
|
||||
owner_id=app.owner_id,
|
||||
created_at=app.created_at,
|
||||
updated_at=app.updated_at
|
||||
)
|
||||
|
||||
|
||||
@router.put("/{app_id}", response_model=ApplicationResponse)
|
||||
async def update_application(
|
||||
app_id: str,
|
||||
app_data: ApplicationUpdate,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
app_service: ApplicationService = Depends(get_application_service),
|
||||
user_service: UserService = Depends(get_user_service)
|
||||
):
|
||||
"""Update an application"""
|
||||
app = await app_service.get_application_by_id(app_id)
|
||||
if not app:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Application with ID {app_id} not found"
|
||||
)
|
||||
|
||||
# Check ownership (admins can update all)
|
||||
user = await user_service.get_user_by_username(current_user.username)
|
||||
if current_user.role != "admin" and app.owner_id != str(user.id):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Not authorized to update this application"
|
||||
)
|
||||
|
||||
updated_app = await app_service.update_application(app_id, app_data)
|
||||
if not updated_app:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Application with ID {app_id} not found"
|
||||
)
|
||||
|
||||
return ApplicationResponse(
|
||||
_id=str(updated_app.id),
|
||||
name=updated_app.name,
|
||||
client_id=updated_app.client_id,
|
||||
redirect_uris=updated_app.redirect_uris,
|
||||
grant_types=updated_app.grant_types,
|
||||
scopes=updated_app.scopes,
|
||||
owner_id=updated_app.owner_id,
|
||||
created_at=updated_app.created_at,
|
||||
updated_at=updated_app.updated_at
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/{app_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_application(
|
||||
app_id: str,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
app_service: ApplicationService = Depends(get_application_service),
|
||||
user_service: UserService = Depends(get_user_service)
|
||||
):
|
||||
"""Delete an application"""
|
||||
app = await app_service.get_application_by_id(app_id)
|
||||
if not app:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Application with ID {app_id} not found"
|
||||
)
|
||||
|
||||
# Check ownership (admins can delete all)
|
||||
user = await user_service.get_user_by_username(current_user.username)
|
||||
if current_user.role != "admin" and app.owner_id != str(user.id):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Not authorized to delete this application"
|
||||
)
|
||||
|
||||
success = await app_service.delete_application(app_id)
|
||||
if not success:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Application with ID {app_id} not found"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
@router.post("/{app_id}/regenerate-secret", response_model=ApplicationWithSecret)
|
||||
async def regenerate_client_secret(
|
||||
app_id: str,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
app_service: ApplicationService = Depends(get_application_service),
|
||||
user_service: UserService = Depends(get_user_service)
|
||||
):
|
||||
"""
|
||||
Regenerate client secret for an application
|
||||
|
||||
Returns the application with the new client_secret (only shown once!)
|
||||
"""
|
||||
app = await app_service.get_application_by_id(app_id)
|
||||
if not app:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Application with ID {app_id} not found"
|
||||
)
|
||||
|
||||
# Check ownership (admins can regenerate all)
|
||||
user = await user_service.get_user_by_username(current_user.username)
|
||||
if current_user.role != "admin" and app.owner_id != str(user.id):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Not authorized to regenerate secret for this application"
|
||||
)
|
||||
|
||||
result = await app_service.regenerate_client_secret(app_id)
|
||||
if not result:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Application with ID {app_id} not found"
|
||||
)
|
||||
|
||||
updated_app, new_secret = result
|
||||
|
||||
return ApplicationWithSecret(
|
||||
_id=str(updated_app.id),
|
||||
name=updated_app.name,
|
||||
client_id=updated_app.client_id,
|
||||
client_secret=new_secret, # New plain text secret (only shown once)
|
||||
redirect_uris=updated_app.redirect_uris,
|
||||
grant_types=updated_app.grant_types,
|
||||
scopes=updated_app.scopes,
|
||||
owner_id=updated_app.owner_id,
|
||||
created_at=updated_app.created_at,
|
||||
updated_at=updated_app.updated_at
|
||||
)
|
||||
|
||||
@ -1,14 +1,137 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
|
||||
from app.core.auth import get_current_active_user, User
|
||||
from app.core.database import get_database
|
||||
from app.services.monitoring_service import MonitoringService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/system")
|
||||
async def get_system_status(current_user: User = Depends(get_current_active_user)):
|
||||
"""Get system status"""
|
||||
return {"status": "healthy", "services": []}
|
||||
|
||||
def get_monitoring_service(db=Depends(get_database)) -> MonitoringService:
|
||||
"""Dependency to get monitoring service"""
|
||||
return MonitoringService(db)
|
||||
|
||||
|
||||
@router.get("/health")
|
||||
async def get_system_health(
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
monitoring_service: MonitoringService = Depends(get_monitoring_service)
|
||||
):
|
||||
"""
|
||||
Get overall system health status
|
||||
|
||||
Includes MongoDB, pipelines, and other component health checks
|
||||
"""
|
||||
health = await monitoring_service.get_system_health()
|
||||
return health
|
||||
|
||||
|
||||
@router.get("/metrics")
|
||||
async def get_system_metrics(
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
monitoring_service: MonitoringService = Depends(get_monitoring_service)
|
||||
):
|
||||
"""
|
||||
Get system-wide metrics
|
||||
|
||||
Includes counts and aggregations for keywords, pipelines, users, and applications
|
||||
"""
|
||||
metrics = await monitoring_service.get_system_metrics()
|
||||
return metrics
|
||||
|
||||
|
||||
@router.get("/logs")
|
||||
async def get_logs(current_user: User = Depends(get_current_active_user)):
|
||||
"""Get pipeline logs"""
|
||||
return {"logs": []}
|
||||
async def get_activity_logs(
|
||||
limit: int = Query(100, ge=1, le=1000, description="Maximum number of logs"),
|
||||
level: Optional[str] = Query(None, description="Filter by log level (INFO, WARNING, ERROR)"),
|
||||
start_date: Optional[datetime] = Query(None, description="Filter logs after this date"),
|
||||
end_date: Optional[datetime] = Query(None, description="Filter logs before this date"),
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
monitoring_service: MonitoringService = Depends(get_monitoring_service)
|
||||
):
|
||||
"""
|
||||
Get activity logs
|
||||
|
||||
Returns logs from all pipelines with optional filtering
|
||||
"""
|
||||
logs = await monitoring_service.get_activity_logs(
|
||||
limit=limit,
|
||||
level=level,
|
||||
start_date=start_date,
|
||||
end_date=end_date
|
||||
)
|
||||
return {"logs": logs, "total": len(logs)}
|
||||
|
||||
|
||||
@router.get("/database/stats")
|
||||
async def get_database_stats(
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
monitoring_service: MonitoringService = Depends(get_monitoring_service)
|
||||
):
|
||||
"""
|
||||
Get MongoDB database statistics (admin only)
|
||||
|
||||
Includes database size, collections, indexes, etc.
|
||||
"""
|
||||
if current_user.role != "admin":
|
||||
from fastapi import HTTPException, status
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Only admins can view database statistics"
|
||||
)
|
||||
|
||||
stats = await monitoring_service.get_database_stats()
|
||||
return stats
|
||||
|
||||
|
||||
@router.get("/database/collections")
|
||||
async def get_collection_stats(
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
monitoring_service: MonitoringService = Depends(get_monitoring_service)
|
||||
):
|
||||
"""
|
||||
Get statistics for all collections (admin only)
|
||||
|
||||
Includes document counts, sizes, and index information
|
||||
"""
|
||||
if current_user.role != "admin":
|
||||
from fastapi import HTTPException, status
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Only admins can view collection statistics"
|
||||
)
|
||||
|
||||
collections = await monitoring_service.get_collection_stats()
|
||||
return {"collections": collections, "total": len(collections)}
|
||||
|
||||
|
||||
@router.get("/pipelines/performance")
|
||||
async def get_pipeline_performance(
|
||||
hours: int = Query(24, ge=1, le=168, description="Number of hours to look back"),
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
monitoring_service: MonitoringService = Depends(get_monitoring_service)
|
||||
):
|
||||
"""
|
||||
Get pipeline performance metrics
|
||||
|
||||
Shows success rates, error counts, and activity for each pipeline
|
||||
"""
|
||||
performance = await monitoring_service.get_pipeline_performance(hours=hours)
|
||||
return performance
|
||||
|
||||
|
||||
@router.get("/errors/summary")
|
||||
async def get_error_summary(
|
||||
hours: int = Query(24, ge=1, le=168, description="Number of hours to look back"),
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
monitoring_service: MonitoringService = Depends(get_monitoring_service)
|
||||
):
|
||||
"""
|
||||
Get summary of recent errors
|
||||
|
||||
Shows error counts and recent error details
|
||||
"""
|
||||
summary = await monitoring_service.get_error_summary(hours=hours)
|
||||
return summary
|
||||
|
||||
@ -1,19 +1,343 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from typing import Optional, List
|
||||
|
||||
from app.core.auth import get_current_active_user, User
|
||||
from app.core.database import get_database
|
||||
from app.services.user_service import UserService
|
||||
from app.schemas.user import (
|
||||
UserCreate,
|
||||
UserUpdate,
|
||||
UserResponse,
|
||||
UserLogin,
|
||||
Token
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/")
|
||||
async def get_users(current_user: User = Depends(get_current_active_user)):
|
||||
"""Get all users"""
|
||||
return {"users": [], "total": 0}
|
||||
|
||||
@router.post("/")
|
||||
async def create_user(user_data: dict, current_user: User = Depends(get_current_active_user)):
|
||||
"""Create new user"""
|
||||
return {"message": "User created"}
|
||||
def get_user_service(db=Depends(get_database)) -> UserService:
|
||||
"""Dependency to get user service"""
|
||||
return UserService(db)
|
||||
|
||||
@router.get("/me")
|
||||
async def get_current_user_info(current_user: User = Depends(get_current_active_user)):
|
||||
"""Get current user info"""
|
||||
return current_user
|
||||
|
||||
@router.post("/login", response_model=Token)
|
||||
async def login(
|
||||
form_data: OAuth2PasswordRequestForm = Depends(),
|
||||
user_service: UserService = Depends(get_user_service)
|
||||
):
|
||||
"""
|
||||
Login endpoint for OAuth2 password flow
|
||||
|
||||
Returns JWT access token on successful authentication
|
||||
"""
|
||||
user = await user_service.authenticate_user(form_data.username, form_data.password)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect username or password",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
token = await user_service.create_access_token_for_user(user)
|
||||
return token
|
||||
|
||||
|
||||
@router.get("/me", response_model=UserResponse)
|
||||
async def get_current_user_info(
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
user_service: UserService = Depends(get_user_service)
|
||||
):
|
||||
"""Get current authenticated user info"""
|
||||
user = await user_service.get_user_by_username(current_user.username)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="User not found"
|
||||
)
|
||||
|
||||
return UserResponse(
|
||||
_id=str(user.id),
|
||||
username=user.username,
|
||||
email=user.email,
|
||||
full_name=user.full_name,
|
||||
role=user.role,
|
||||
disabled=user.disabled,
|
||||
created_at=user.created_at,
|
||||
last_login=user.last_login
|
||||
)
|
||||
|
||||
|
||||
@router.get("/", response_model=List[UserResponse])
|
||||
async def get_users(
|
||||
role: Optional[str] = Query(None, description="Filter by role (admin/editor/viewer)"),
|
||||
disabled: Optional[bool] = Query(None, description="Filter by disabled status"),
|
||||
search: Optional[str] = Query(None, description="Search in username, email, or full name"),
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
user_service: UserService = Depends(get_user_service)
|
||||
):
|
||||
"""Get all users (admin only)"""
|
||||
# Check if user is admin
|
||||
if current_user.role != "admin":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Only admins can list users"
|
||||
)
|
||||
|
||||
users = await user_service.get_users(role=role, disabled=disabled, search=search)
|
||||
|
||||
return [
|
||||
UserResponse(
|
||||
_id=str(u.id),
|
||||
username=u.username,
|
||||
email=u.email,
|
||||
full_name=u.full_name,
|
||||
role=u.role,
|
||||
disabled=u.disabled,
|
||||
created_at=u.created_at,
|
||||
last_login=u.last_login
|
||||
)
|
||||
for u in users
|
||||
]
|
||||
|
||||
|
||||
@router.get("/stats")
|
||||
async def get_user_stats(
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
user_service: UserService = Depends(get_user_service)
|
||||
):
|
||||
"""Get user statistics (admin only)"""
|
||||
if current_user.role != "admin":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Only admins can view user statistics"
|
||||
)
|
||||
|
||||
stats = await user_service.get_user_stats()
|
||||
return stats
|
||||
|
||||
|
||||
@router.get("/{user_id}", response_model=UserResponse)
|
||||
async def get_user(
|
||||
user_id: str,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
user_service: UserService = Depends(get_user_service)
|
||||
):
|
||||
"""Get a user by ID (admin only or own user)"""
|
||||
# Check if user is viewing their own profile
|
||||
user = await user_service.get_user_by_id(user_id)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"User with ID {user_id} not found"
|
||||
)
|
||||
|
||||
# Allow users to view their own profile, or admins to view any profile
|
||||
if user.username != current_user.username and current_user.role != "admin":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Not authorized to view this user"
|
||||
)
|
||||
|
||||
return UserResponse(
|
||||
_id=str(user.id),
|
||||
username=user.username,
|
||||
email=user.email,
|
||||
full_name=user.full_name,
|
||||
role=user.role,
|
||||
disabled=user.disabled,
|
||||
created_at=user.created_at,
|
||||
last_login=user.last_login
|
||||
)
|
||||
|
||||
|
||||
@router.post("/", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_user(
|
||||
user_data: UserCreate,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
user_service: UserService = Depends(get_user_service)
|
||||
):
|
||||
"""Create a new user (admin only)"""
|
||||
if current_user.role != "admin":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Only admins can create users"
|
||||
)
|
||||
|
||||
try:
|
||||
user = await user_service.create_user(user_data)
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(e)
|
||||
)
|
||||
|
||||
return UserResponse(
|
||||
_id=str(user.id),
|
||||
username=user.username,
|
||||
email=user.email,
|
||||
full_name=user.full_name,
|
||||
role=user.role,
|
||||
disabled=user.disabled,
|
||||
created_at=user.created_at,
|
||||
last_login=user.last_login
|
||||
)
|
||||
|
||||
|
||||
@router.put("/{user_id}", response_model=UserResponse)
|
||||
async def update_user(
|
||||
user_id: str,
|
||||
user_data: UserUpdate,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
user_service: UserService = Depends(get_user_service)
|
||||
):
|
||||
"""Update a user (admin only or own user with restrictions)"""
|
||||
user = await user_service.get_user_by_id(user_id)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"User with ID {user_id} not found"
|
||||
)
|
||||
|
||||
# Check permissions
|
||||
is_own_user = user.username == current_user.username
|
||||
is_admin = current_user.role == "admin"
|
||||
|
||||
if not is_own_user and not is_admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Not authorized to update this user"
|
||||
)
|
||||
|
||||
# Regular users can only update their own email and full_name
|
||||
if is_own_user and not is_admin:
|
||||
if user_data.role is not None or user_data.disabled is not None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Cannot change role or disabled status"
|
||||
)
|
||||
|
||||
try:
|
||||
updated_user = await user_service.update_user(user_id, user_data)
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(e)
|
||||
)
|
||||
|
||||
if not updated_user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"User with ID {user_id} not found"
|
||||
)
|
||||
|
||||
return UserResponse(
|
||||
_id=str(updated_user.id),
|
||||
username=updated_user.username,
|
||||
email=updated_user.email,
|
||||
full_name=updated_user.full_name,
|
||||
role=updated_user.role,
|
||||
disabled=updated_user.disabled,
|
||||
created_at=updated_user.created_at,
|
||||
last_login=updated_user.last_login
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_user(
|
||||
user_id: str,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
user_service: UserService = Depends(get_user_service)
|
||||
):
|
||||
"""Delete a user (admin only)"""
|
||||
if current_user.role != "admin":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Only admins can delete users"
|
||||
)
|
||||
|
||||
# Prevent self-deletion
|
||||
user = await user_service.get_user_by_id(user_id)
|
||||
if user and user.username == current_user.username:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Cannot delete your own user account"
|
||||
)
|
||||
|
||||
success = await user_service.delete_user(user_id)
|
||||
if not success:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"User with ID {user_id} not found"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
@router.post("/{user_id}/toggle", response_model=UserResponse)
|
||||
async def toggle_user_status(
|
||||
user_id: str,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
user_service: UserService = Depends(get_user_service)
|
||||
):
|
||||
"""Toggle user disabled status (admin only)"""
|
||||
if current_user.role != "admin":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Only admins can toggle user status"
|
||||
)
|
||||
|
||||
# Prevent self-toggle
|
||||
user = await user_service.get_user_by_id(user_id)
|
||||
if user and user.username == current_user.username:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Cannot toggle your own user status"
|
||||
)
|
||||
|
||||
updated_user = await user_service.toggle_user_status(user_id)
|
||||
if not updated_user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"User with ID {user_id} not found"
|
||||
)
|
||||
|
||||
return UserResponse(
|
||||
_id=str(updated_user.id),
|
||||
username=updated_user.username,
|
||||
email=updated_user.email,
|
||||
full_name=updated_user.full_name,
|
||||
role=updated_user.role,
|
||||
disabled=updated_user.disabled,
|
||||
created_at=updated_user.created_at,
|
||||
last_login=updated_user.last_login
|
||||
)
|
||||
|
||||
|
||||
@router.post("/change-password")
|
||||
async def change_password(
|
||||
old_password: str,
|
||||
new_password: str,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
user_service: UserService = Depends(get_user_service)
|
||||
):
|
||||
"""Change current user's password"""
|
||||
user = await user_service.get_user_by_username(current_user.username)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="User not found"
|
||||
)
|
||||
|
||||
success = await user_service.change_password(
|
||||
str(user.id),
|
||||
old_password,
|
||||
new_password
|
||||
)
|
||||
|
||||
if not success:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Incorrect old password"
|
||||
)
|
||||
|
||||
return {"message": "Password changed successfully"}
|
||||
|
||||
Reference in New Issue
Block a user