feat: add full project - backend, frontend, docker, specs and configs
This commit is contained in:
parent
b77c7d5a01
commit
e6cb06255b
24489 changed files with 61341 additions and 36 deletions
163
.specify/features/005-authentication/data-model.md
Normal file
163
.specify/features/005-authentication/data-model.md
Normal 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue