sync: migrate secure-online-shop to Gitea (2026-08-10)
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""Pydantic schemas package."""
|
||||
@@ -0,0 +1,85 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Annotated, Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, StrictStr, StringConstraints, field_validator
|
||||
|
||||
from app.models.user import RoleEnum
|
||||
|
||||
UsernameField = Annotated[
|
||||
StrictStr,
|
||||
StringConstraints(
|
||||
strip_whitespace=True,
|
||||
min_length=3,
|
||||
max_length=50,
|
||||
),
|
||||
]
|
||||
PasswordField = Annotated[
|
||||
StrictStr,
|
||||
StringConstraints(
|
||||
min_length=8,
|
||||
max_length=72,
|
||||
),
|
||||
]
|
||||
StrongPasswordField = Annotated[
|
||||
StrictStr,
|
||||
StringConstraints(
|
||||
min_length=12,
|
||||
max_length=72,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
class UserCreate(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
username: UsernameField
|
||||
password: StrongPasswordField
|
||||
|
||||
@field_validator("username")
|
||||
@classmethod
|
||||
def normalize_username(cls, value: str) -> str:
|
||||
return value.lower()
|
||||
|
||||
@field_validator("password")
|
||||
@classmethod
|
||||
def validate_password_strength(cls, value: str) -> str:
|
||||
has_upper = any(char.isupper() for char in value)
|
||||
has_lower = any(char.islower() for char in value)
|
||||
has_digit = any(char.isdigit() for char in value)
|
||||
has_special = any(not char.isalnum() for char in value)
|
||||
if not all((has_upper, has_lower, has_digit, has_special)):
|
||||
raise ValueError(
|
||||
"Password must include upper, lower, digit, and special characters"
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
class LoginRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
username: UsernameField
|
||||
password: PasswordField
|
||||
|
||||
@field_validator("username")
|
||||
@classmethod
|
||||
def normalize_username(cls, value: str) -> str:
|
||||
return value.lower()
|
||||
|
||||
|
||||
class UserResponse(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True, extra="forbid")
|
||||
|
||||
id: int
|
||||
username: str
|
||||
role: RoleEnum
|
||||
is_active: bool
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class TokenResponse(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
access_token: str
|
||||
token_type: Literal["bearer"] = "bearer"
|
||||
@@ -0,0 +1,51 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from typing import Annotated
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, StrictInt, model_validator
|
||||
|
||||
from app.models.order import OrderStatusEnum
|
||||
|
||||
|
||||
class OrderCreateItem(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
product_id: Annotated[StrictInt, Field(gt=0)]
|
||||
quantity: Annotated[StrictInt, Field(gt=0, le=1000)]
|
||||
|
||||
|
||||
class OrderCreate(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
items: Annotated[list[OrderCreateItem], Field(min_length=1, max_length=100)]
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_unique_products(self) -> "OrderCreate":
|
||||
product_ids = [item.product_id for item in self.items]
|
||||
if len(product_ids) != len(set(product_ids)):
|
||||
raise ValueError("Each product may only appear once per order")
|
||||
return self
|
||||
|
||||
|
||||
class OrderItemResponse(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True, extra="forbid")
|
||||
|
||||
id: int
|
||||
product_id: int
|
||||
product_name: str
|
||||
quantity: int
|
||||
unit_price: Decimal
|
||||
subtotal: Decimal
|
||||
|
||||
|
||||
class OrderResponse(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True, extra="forbid")
|
||||
|
||||
id: int
|
||||
status: OrderStatusEnum
|
||||
total_amount: Decimal
|
||||
created_at: datetime
|
||||
paid_at: datetime | None
|
||||
items: list[OrderItemResponse]
|
||||
@@ -0,0 +1,17 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
from app.models.order import OrderStatusEnum
|
||||
|
||||
|
||||
class PaymentResponse(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True, extra="forbid")
|
||||
|
||||
id: int
|
||||
status: OrderStatusEnum
|
||||
total_amount: Decimal
|
||||
paid_at: datetime | None
|
||||
@@ -0,0 +1,38 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from typing import Annotated
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictInt, StrictStr, StringConstraints
|
||||
|
||||
NameField = Annotated[
|
||||
StrictStr,
|
||||
StringConstraints(strip_whitespace=True, min_length=3, max_length=120),
|
||||
]
|
||||
DescriptionField = Annotated[
|
||||
StrictStr,
|
||||
StringConstraints(strip_whitespace=True, min_length=1, max_length=1000),
|
||||
]
|
||||
|
||||
|
||||
class ProductCreate(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
name: NameField
|
||||
description: DescriptionField | None = None
|
||||
price: Annotated[Decimal, Field(gt=0, max_digits=10, decimal_places=2)]
|
||||
stock: Annotated[StrictInt, Field(ge=0, le=1_000_000)]
|
||||
is_active: StrictBool = True
|
||||
|
||||
|
||||
class ProductResponse(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True, extra="forbid")
|
||||
|
||||
id: int
|
||||
name: str
|
||||
description: str | None
|
||||
price: Decimal
|
||||
stock: int
|
||||
is_active: bool
|
||||
created_at: datetime
|
||||
Reference in New Issue
Block a user