feat: add full project - backend, frontend, docker, specs and configs

This commit is contained in:
MatheusAlves96 2026-04-20 23:59:45 -03:00
parent b77c7d5a01
commit e6cb06255b
24489 changed files with 61341 additions and 36 deletions

View file

@ -0,0 +1,163 @@
# 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.