feat(T01): auth + user management + RBAC + base frontend + i18n
- Backend: FastAPI, JWT auth (HS256), bcrypt, RBAC middleware - User CRUD (admin-only), soft-delete, pagination - Pydantic BaseSettings config, async SQLAlchemy 2.0 - 50 backend tests, 88% coverage - Frontend: Next.js 14 App Router, Tailwind, design tokens - Login page, auth context, API client with auto-refresh - i18n: next-intl, DE/EN (31 keys each) - 6 base UI components (Button, Input, Card, Table, Modal, Toast) - 12 frontend tests, npm build success
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
"""Pydantic schemas for user-related request and response bodies."""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Literal, Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, EmailStr, Field
|
||||
|
||||
|
||||
class LoginRequest(BaseModel):
|
||||
"""POST /api/v1/auth/login request body."""
|
||||
email: EmailStr
|
||||
password: str = Field(..., min_length=1)
|
||||
|
||||
|
||||
class TokenResponse(BaseModel):
|
||||
"""JWT token pair returned after login or refresh."""
|
||||
access_token: str
|
||||
refresh_token: str
|
||||
token_type: str = "bearer"
|
||||
expires_in: int = Field(..., description="Access token TTL in seconds")
|
||||
|
||||
|
||||
class RefreshRequest(BaseModel):
|
||||
"""POST /api/v1/auth/refresh request body."""
|
||||
refresh_token: str
|
||||
|
||||
|
||||
class UserBase(BaseModel):
|
||||
"""Base user fields shared across schemas."""
|
||||
email: EmailStr
|
||||
full_name: str = Field(..., min_length=1, max_length=200)
|
||||
role: Literal["admin", "verkaeufer", "buchhaltung"] = "verkaeufer"
|
||||
language: str = Field(default="de", max_length=5)
|
||||
|
||||
|
||||
class UserCreate(UserBase):
|
||||
"""POST /api/v1/users request body."""
|
||||
password: str = Field(..., min_length=8, max_length=128)
|
||||
|
||||
|
||||
class UserUpdate(BaseModel):
|
||||
"""PUT /api/v1/users/:id request body (all fields optional)."""
|
||||
email: Optional[EmailStr] = None
|
||||
full_name: Optional[str] = Field(None, min_length=1, max_length=200)
|
||||
role: Optional[Literal["admin", "verkaeufer", "buchhaltung"]] = None
|
||||
language: Optional[str] = Field(None, max_length=5)
|
||||
is_active: Optional[bool] = None
|
||||
|
||||
|
||||
class UserResponse(BaseModel):
|
||||
"""User response schema (never exposes password_hash)."""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: uuid.UUID
|
||||
email: str
|
||||
full_name: str
|
||||
role: str
|
||||
language: str
|
||||
is_active: bool
|
||||
created_at: Optional[datetime] = None
|
||||
updated_at: Optional[datetime] = None
|
||||
|
||||
|
||||
class UserListResponse(BaseModel):
|
||||
"""Paginated user list response."""
|
||||
items: list[UserResponse]
|
||||
total: int
|
||||
page: int
|
||||
page_size: int
|
||||
|
||||
|
||||
class ErrorResponse(BaseModel):
|
||||
"""Standard error response format."""
|
||||
error: dict
|
||||
|
||||
|
||||
class HealthResponse(BaseModel):
|
||||
"""Health check response."""
|
||||
status: str = "ok"
|
||||
Reference in New Issue
Block a user