sass-imobiliaria/.specify/features/005-authentication/data-model.md

4.3 KiB

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):

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.

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.

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).

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.

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

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

interface AuthContextType {
  user: User | null;
  token: string | null;
  isAuthenticated: boolean;
  login: (email: string, password: string) => Promise<void>;
  logout: () => void;
  register: (name: string, email: string, password: string) => Promise<void>;
}

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.