new features
This commit is contained in:
@@ -2,17 +2,21 @@ from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from datetime import timedelta
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Response, status
|
||||
from jwt import ExpiredSignatureError, InvalidTokenError
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from cpv3.infrastructure.auth import get_current_user
|
||||
from cpv3.infrastructure.deps import get_storage
|
||||
from cpv3.infrastructure.security import create_token, decode_token
|
||||
from cpv3.infrastructure.settings import get_settings
|
||||
from cpv3.infrastructure.storage.base import StorageService
|
||||
from cpv3.db.session import get_db
|
||||
from cpv3.modules.users.models import User
|
||||
from cpv3.modules.users.schemas import (
|
||||
PasswordChange,
|
||||
TokenRefresh,
|
||||
TokenRefreshResponse,
|
||||
UserCreate,
|
||||
@@ -28,6 +32,21 @@ users_router = APIRouter(prefix="/api/users", tags=["Users"])
|
||||
auth_router = APIRouter(prefix="/auth", tags=["auth"])
|
||||
|
||||
|
||||
def _is_s3_key(value: str) -> bool:
|
||||
"""Return True if value looks like a bare S3 key, not a full URL."""
|
||||
parsed = urlparse(value)
|
||||
return not parsed.scheme and not parsed.netloc
|
||||
|
||||
|
||||
async def _resolve_avatar(user: User, storage: StorageService) -> UserRead:
|
||||
"""Build UserRead with a fresh presigned avatar URL."""
|
||||
data = UserRead.model_validate(user)
|
||||
if data.avatar:
|
||||
if _is_s3_key(data.avatar):
|
||||
data.avatar = await storage.url(data.avatar)
|
||||
return data
|
||||
|
||||
|
||||
def _issue_tokens(user: User) -> tuple[str, str]:
|
||||
settings = get_settings()
|
||||
|
||||
@@ -49,10 +68,11 @@ def _issue_tokens(user: User) -> tuple[str, str]:
|
||||
async def list_all_users(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
storage: StorageService = Depends(get_storage),
|
||||
) -> list[UserRead]:
|
||||
service = UserService(db)
|
||||
users = await service.list_users(requester=current_user)
|
||||
return [UserRead.model_validate(u) for u in users]
|
||||
return [await _resolve_avatar(u, storage) for u in users]
|
||||
|
||||
|
||||
@users_router.post("/", response_model=UserRead, status_code=status.HTTP_201_CREATED)
|
||||
@@ -60,6 +80,7 @@ async def create_user_endpoint(
|
||||
body: UserCreate,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
storage: StorageService = Depends(get_storage),
|
||||
) -> UserRead:
|
||||
service = UserService(db)
|
||||
try:
|
||||
@@ -67,12 +88,29 @@ async def create_user_endpoint(
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) from e
|
||||
|
||||
return UserRead.model_validate(user)
|
||||
return await _resolve_avatar(user, storage)
|
||||
|
||||
|
||||
@users_router.get("/me/", response_model=UserRead)
|
||||
async def me(current_user: User = Depends(get_current_user)) -> UserRead:
|
||||
return UserRead.model_validate(current_user)
|
||||
async def me(
|
||||
current_user: User = Depends(get_current_user),
|
||||
storage: StorageService = Depends(get_storage),
|
||||
) -> UserRead:
|
||||
return await _resolve_avatar(current_user, storage)
|
||||
|
||||
|
||||
@users_router.post("/me/change-password/", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def change_password(
|
||||
body: PasswordChange,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> Response:
|
||||
service = UserService(db)
|
||||
try:
|
||||
await service.change_password(current_user, body.current_password, body.new_password)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) from e
|
||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
@users_router.get("/{user_id}/", response_model=UserRead)
|
||||
@@ -80,6 +118,7 @@ async def retrieve_user(
|
||||
user_id: uuid.UUID,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
storage: StorageService = Depends(get_storage),
|
||||
) -> UserRead:
|
||||
service = UserService(db)
|
||||
user = await service.get_user_by_id(user_id)
|
||||
@@ -89,7 +128,7 @@ async def retrieve_user(
|
||||
if not current_user.is_staff and user.id != current_user.id:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Forbidden")
|
||||
|
||||
return UserRead.model_validate(user)
|
||||
return await _resolve_avatar(user, storage)
|
||||
|
||||
|
||||
@users_router.patch("/{user_id}/", response_model=UserRead)
|
||||
@@ -98,6 +137,7 @@ async def patch_user(
|
||||
body: UserUpdate,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
storage: StorageService = Depends(get_storage),
|
||||
) -> UserRead:
|
||||
service = UserService(db)
|
||||
user = await service.get_user_by_id(user_id)
|
||||
@@ -112,7 +152,7 @@ async def patch_user(
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) from e
|
||||
|
||||
return UserRead.model_validate(user)
|
||||
return await _resolve_avatar(user, storage)
|
||||
|
||||
|
||||
@users_router.delete("/{user_id}/", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@@ -136,7 +176,11 @@ async def delete_user(
|
||||
@auth_router.post(
|
||||
"/register", response_model=UserRegisterResponse, status_code=status.HTTP_201_CREATED
|
||||
)
|
||||
async def register(body: UserRegister, db: AsyncSession = Depends(get_db)) -> UserRegisterResponse:
|
||||
async def register(
|
||||
body: UserRegister,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
storage: StorageService = Depends(get_storage),
|
||||
) -> UserRegisterResponse:
|
||||
service = UserService(db)
|
||||
try:
|
||||
user = await service.register_user(body)
|
||||
@@ -144,18 +188,24 @@ async def register(body: UserRegister, db: AsyncSession = Depends(get_db)) -> Us
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) from e
|
||||
|
||||
access, refresh = _issue_tokens(user)
|
||||
return UserRegisterResponse(user=UserRead.model_validate(user), access=access, refresh=refresh)
|
||||
user_read = await _resolve_avatar(user, storage)
|
||||
return UserRegisterResponse(user=user_read, access=access, refresh=refresh)
|
||||
|
||||
|
||||
@auth_router.post("/login", response_model=UserRegisterResponse)
|
||||
async def login(body: UserLogin, db: AsyncSession = Depends(get_db)) -> UserRegisterResponse:
|
||||
async def login(
|
||||
body: UserLogin,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
storage: StorageService = Depends(get_storage),
|
||||
) -> UserRegisterResponse:
|
||||
service = UserService(db)
|
||||
user = await service.authenticate(body.username, body.password)
|
||||
if user is None:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")
|
||||
|
||||
access, refresh = _issue_tokens(user)
|
||||
return UserRegisterResponse(user=UserRead.model_validate(user), access=access, refresh=refresh)
|
||||
user_read = await _resolve_avatar(user, storage)
|
||||
return UserRegisterResponse(user=user_read, access=access, refresh=refresh)
|
||||
|
||||
|
||||
@auth_router.post("/refresh", response_model=TokenRefreshResponse)
|
||||
|
||||
Reference in New Issue
Block a user