163 lines
4.3 KiB
Markdown
163 lines
4.3 KiB
Markdown
# 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<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.
|