# Data Model: 005-authentication **Fase 1 — Modelo de Dados** **Data**: 2026-04-13 --- ## Entidades ### ClientUser **Tabela**: `client_users` **Módulo**: `backend/app/models/user.py` **Classe**: `ClientUser` | Campo | Tipo SQLAlchemy | Tipo Python | Nullable | Constraints | Notas | |-------|-----------------|-------------|----------|-------------|-------| | `id` | `UUID(as_uuid=True)` | `uuid.UUID` | NOT NULL | PK, `default=uuid.uuid4` | Gerado no Python antes do flush | | `name` | `String(150)` | `str` | NOT NULL | — | Nome completo | | `email` | `String(254)` | `str` | NOT NULL | UNIQUE, INDEX | Normalizado para lowercase via schema Pydantic | | `password_hash` | `String(100)` | `str` | NOT NULL | — | Hash bcrypt (60 chars); String(100) com margem de segurança | | `role` | `String(20)` | `str` | NOT NULL | `default='client'` | Único papel ativo nesta versão | | `created_at` | `DateTime` | `datetime` | NOT NULL | `server_default=func.now()` | Timezone-naive UTC | **Indexes**: `email` (único + índice explícito — frequente em queries de login) **Relacionamentos**: nenhum nesta versão **DDL esperado** (gerenciado via Alembic, não escrever manualmente): ```sql CREATE TABLE client_users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(150) NOT NULL, email VARCHAR(254) NOT NULL UNIQUE, password_hash VARCHAR(100) NOT NULL, role VARCHAR(20) NOT NULL DEFAULT 'client', created_at TIMESTAMP NOT NULL DEFAULT now() ); CREATE INDEX ix_client_users_email ON client_users (email); ``` --- ## Schemas Pydantic **Módulo**: `backend/app/schemas/auth.py` ### `RegisterIn` Valida o corpo da requisição `POST /api/v1/auth/register`. ```python class RegisterIn(BaseModel): name: str = Field(min_length=1, max_length=150) email: EmailStr password: str = Field(min_length=8) @field_validator("email") @classmethod def normalize_email(cls, v: str) -> str: return v.lower().strip() ``` ### `LoginIn` Valida o corpo da requisição `POST /api/v1/auth/login`. ```python class LoginIn(BaseModel): email: EmailStr password: str @field_validator("email") @classmethod def normalize_email(cls, v: str) -> str: return v.lower().strip() ``` ### `UserOut` Resposta segura com dados do usuário (sem `password_hash`). ```python class UserOut(BaseModel): model_config = ConfigDict(from_attributes=True) id: UUID name: str email: str role: str created_at: datetime ``` ### `AuthTokenOut` Resposta de register e login bem-sucedidos. ```python class AuthTokenOut(BaseModel): access_token: str user: UserOut ``` --- ## Regras de Validação | Regra | Campo | Resposta em falha | |-------|-------|------------------| | `min_length=8` | `password` em `RegisterIn` | HTTP 422 | | `min_length=1, max_length=150` | `name` em `RegisterIn` | HTTP 422 | | `EmailStr` | `email` em `RegisterIn` / `LoginIn` | HTTP 422 | | Email único | `email` em `ClientUser` | HTTP 409 (capturado na rota via `IntegrityError`) | | Normalize lowercase | `email` (ambos os schemas) | Aplicado silenciosamente via `field_validator` | --- ## Transições de Estado O `ClientUser` não possui máquina de estados nesta versão: - **Created**: via `POST /api/v1/auth/register` - **Read**: via `POST /api/v1/auth/login` + `GET /api/v1/auth/me` - **Update / Delete**: fora do escopo desta feature --- ## Tipos Frontend **Módulo**: `frontend/src/types/auth.ts` ```typescript export interface User { id: string; name: string; email: string; role: string; } export interface AuthState { user: User | null; token: string | null; isAuthenticated: boolean; } ``` --- ## Contexto de Autenticação Frontend **Módulo**: `frontend/src/contexts/AuthContext.tsx` ```typescript interface AuthContextType { user: User | null; token: string | null; isAuthenticated: boolean; login: (email: string, password: string) => Promise; logout: () => void; register: (name: string, email: string, password: string) => Promise; } ``` **Inicialização**: ao montar `AuthProvider`, carregar `imob_token` do `localStorage` e chamar `GET /api/v1/auth/me` para hidratar `user`. Se o token estiver expirado/inválido, limpar o `localStorage` e definir estado unauthenticated.