Files
denya-onecare/app/schemas/auth.py
T
root 3ae1062d65 P0 security batch: admin-only user mgmt, unified role model, login rate limiting, webhook secret, security headers, pagination caps
- Remove POST /api/auth/register (404); no sign-up UI; users are admin-managed
- Add admin-only POST/PATCH/DELETE /api/auth/users (forced canonical roles,
  self-lockout + reference guards)
- Unify role model in app/core/roles.py; reject unknown roles at creation and
  at login/JWT validation; startup normalizes unambiguous legacy aliases
- Login rate limiting ~5 fails/15 min per IP+email -> 429 (in-process, tunable)
- WhatsApp webhook requires X-Webhook-Secret; fail-closed when env unset;
  GET handshake uses constant-time verify token (403 on mismatch)
- GET /api/whatsapp/mock-log now requires auth
- Security headers middleware: X-Frame-Options DENY, nosniff, CSP on HTML,
  HSTS behind TLS
- Pagination: limit alias for page_size, hard cap enforced, both -> 422
2026-09-08 10:36:16 +00:00

99 lines
2.8 KiB
Python

"""Pydantic schemas for authentication endpoints."""
from __future__ import annotations
from pydantic import BaseModel, Field, field_validator, model_validator
from app.core.roles import CANONICAL_ROLES
def _validate_canonical_role(value: str | None) -> str | None:
"""Reject any role that is not part of the unified canonical taxonomy.
The role vocabulary is closed: admin user-management must only ever mint
canonical roles (see app/core/roles.py). Legacy/unknown strings
(``admin``, ``superadmin``, ``technician``, …) are rejected here so junk
roles can never be (re)created through the API.
"""
if value is None:
return None
role = value.strip()
if role not in CANONICAL_ROLES:
raise ValueError(
f"Unknown role '{value}'. Allowed roles: {', '.join(CANONICAL_ROLES)}"
)
return role
class LoginRequest(BaseModel):
email: str
password: str
class TokenResponse(BaseModel):
access_token: str
refresh_token: str
token_type: str = "bearer"
class RefreshRequest(BaseModel):
refresh_token: str
class UserOut(BaseModel):
id: int
email: str
full_name: str
phone: str | None
role: str
active: bool
model_config = {"from_attributes": True}
# ── Admin user management (self-registration is removed) ──────────────
class AdminCreateUserRequest(BaseModel):
"""Admin-created user. The role is mandatory and must be canonical.
``role`` is deliberately NOT optional and has no default — an admin must
state the intended role explicitly; the server never infers one.
"""
email: str = Field(min_length=1)
password: str = Field(min_length=8, description="Minimum 8 characters")
full_name: str = Field(min_length=1)
phone: str | None = None
role: str
@field_validator("role")
@classmethod
def _role_canonical(cls, value: str) -> str:
return _validate_canonical_role(value) # type: ignore[return-value]
@field_validator("email")
@classmethod
def _lower_email(cls, value: str) -> str:
return value.strip().lower()
class AdminUpdateUserRequest(BaseModel):
"""Admin edits to an existing user: role change and/or deactivation.
At least one field must be present. ``active=False`` deactivates the
account (login and token refresh then fail closed).
"""
role: str | None = None
active: bool | None = None
@field_validator("role")
@classmethod
def _role_canonical(cls, value: str | None) -> str | None:
return _validate_canonical_role(value)
@model_validator(mode="after")
def _at_least_one_field(self) -> "AdminUpdateUserRequest":
if self.role is None and self.active is None:
raise ValueError("Provide at least one of 'role' or 'active'")
return self