init: new structure + fix lint errors

This commit is contained in:
Daniil
2026-02-03 02:15:07 +03:00
commit 67e0f22b4f
89 changed files with 7654 additions and 0 deletions
+180
View File
@@ -0,0 +1,180 @@
from __future__ import annotations
import uuid
from datetime import timedelta
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.security import create_token, decode_token
from cpv3.infrastructure.settings import get_settings
from cpv3.db.session import get_db
from cpv3.modules.users.models import User
from cpv3.modules.users.schemas import (
TokenRefresh,
TokenRefreshResponse,
UserCreate,
UserLogin,
UserRead,
UserRegister,
UserRegisterResponse,
UserUpdate,
)
from cpv3.modules.users.service import UserService
users_router = APIRouter(prefix="/api/users", tags=["Users"])
auth_router = APIRouter(prefix="/auth", tags=["auth"])
def _issue_tokens(user: User) -> tuple[str, str]:
settings = get_settings()
access = create_token(
subject=str(user.id),
token_type="access",
expires_in=timedelta(minutes=settings.jwt_access_ttl_minutes),
extra={"is_staff": user.is_staff, "is_superuser": user.is_superuser},
)
refresh = create_token(
subject=str(user.id),
token_type="refresh",
expires_in=timedelta(days=settings.jwt_refresh_ttl_days),
)
return access, refresh
@users_router.get("/", response_model=list[UserRead])
async def list_all_users(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> list[UserRead]:
service = UserService(db)
users = await service.list_users(requester=current_user)
return [UserRead.model_validate(u) for u in users]
@users_router.post("/", response_model=UserRead, status_code=status.HTTP_201_CREATED)
async def create_user_endpoint(
body: UserCreate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> UserRead:
service = UserService(db)
try:
user = await service.create_user(body, requester=current_user)
except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) from e
return UserRead.model_validate(user)
@users_router.get("/me/", response_model=UserRead)
async def me(current_user: User = Depends(get_current_user)) -> UserRead:
return UserRead.model_validate(current_user)
@users_router.get("/{user_id}/", response_model=UserRead)
async def retrieve_user(
user_id: uuid.UUID,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> UserRead:
service = UserService(db)
user = await service.get_user_by_id(user_id)
if user is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Not found")
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)
@users_router.patch("/{user_id}/", response_model=UserRead)
async def patch_user(
user_id: uuid.UUID,
body: UserUpdate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> UserRead:
service = UserService(db)
user = await service.get_user_by_id(user_id)
if user is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Not found")
if not current_user.is_staff and user.id != current_user.id:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Forbidden")
try:
user = await service.update_user(user, body)
except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) from e
return UserRead.model_validate(user)
@users_router.delete("/{user_id}/", status_code=status.HTTP_204_NO_CONTENT)
async def delete_user(
user_id: uuid.UUID,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> Response:
service = UserService(db)
user = await service.get_user_by_id(user_id)
if user is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Not found")
if not current_user.is_staff and user.id != current_user.id:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Forbidden")
await service.deactivate_user(user)
return Response(status_code=status.HTTP_204_NO_CONTENT)
@auth_router.post(
"/register", response_model=UserRegisterResponse, status_code=status.HTTP_201_CREATED
)
async def register(body: UserRegister, db: AsyncSession = Depends(get_db)) -> UserRegisterResponse:
service = UserService(db)
try:
user = await service.register_user(body)
except ValueError as e:
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)
@auth_router.post("/login", response_model=UserRegisterResponse)
async def login(body: UserLogin, db: AsyncSession = Depends(get_db)) -> 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)
@auth_router.post("/refresh", response_model=TokenRefreshResponse)
async def refresh(body: TokenRefresh) -> TokenRefreshResponse:
try:
payload = decode_token(body.refresh)
if payload.get("type") != "refresh":
raise InvalidTokenError("wrong type")
user_id = uuid.UUID(str(payload.get("sub")))
settings = get_settings()
access = create_token(
subject=str(user_id),
token_type="access",
expires_in=timedelta(minutes=settings.jwt_access_ttl_minutes),
)
return TokenRefreshResponse(access=access, refresh=body.refresh)
except (ExpiredSignatureError, InvalidTokenError, ValueError):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid refresh token"
)