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
135
.specify/features/005-authentication/research.md
Normal file
135
.specify/features/005-authentication/research.md
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
# Research: 005-authentication
|
||||
|
||||
**Fase 0 — Resolução de Unknowns**
|
||||
**Data**: 2026-04-13
|
||||
|
||||
---
|
||||
|
||||
## Decision Log
|
||||
|
||||
### 1. Biblioteca de hashing de senha
|
||||
|
||||
**Decision**: `bcrypt>=4.2`
|
||||
**Rationale**: Bcrypt é o padrão da indústria para hashing de senhas — adaptativo, lento por design, resistente a ataques de força bruta com GPUs. Usar diretamente (sem passar por `werkzeug.security`) mantém a cadeia de dependências mínima (Princípio VI) e está alinhado com a instrução de design.
|
||||
**Alternatives considered**:
|
||||
- `werkzeug.security.generate_password_hash` — encapsula bcrypt mas adiciona dependência desnecessária
|
||||
- `argon2-cffi` — mais forte que bcrypt, porém não justificado para MVP com ~100 usuários
|
||||
- `passlib` — camada de abstração extra (YAGNI)
|
||||
|
||||
**Usage pattern**:
|
||||
```python
|
||||
import bcrypt
|
||||
|
||||
# Hash
|
||||
hashed = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt())
|
||||
password_hash = hashed.decode("utf-8") # armazenar como String(100)
|
||||
|
||||
# Verify
|
||||
bcrypt.checkpw(password.encode("utf-8"), stored_hash.encode("utf-8"))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Biblioteca JWT
|
||||
|
||||
**Decision**: `PyJWT>=2.9`
|
||||
**Rationale**: API limpa, amplamente adotada no ecossistema Flask, suporte nativo a expiração via claim `exp`. Leveza da dependência cumpre Princípio VI.
|
||||
**Alternatives considered**:
|
||||
- `python-jose` — orientado a JWE/JWKS, mais pesado (YAGNI)
|
||||
- `authlib` — voltado a OAuth2 / OpenID Connect (fora do escopo)
|
||||
- `flask-jwt-extended` — abstração extra útil para refresh tokens; sem refresh no escopo MVP, não se justifica
|
||||
|
||||
**Usage pattern**:
|
||||
```python
|
||||
import jwt
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# Encode
|
||||
payload = {"sub": str(user.id), "exp": datetime.utcnow() + timedelta(days=7)}
|
||||
token = jwt.encode(payload, JWT_SECRET_KEY, algorithm="HS256")
|
||||
|
||||
# Decode (lança jwt.ExpiredSignatureError ou jwt.InvalidTokenError em falha)
|
||||
data = jwt.decode(token, JWT_SECRET_KEY, algorithms=["HS256"])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. UUID como chave primária do ClientUser
|
||||
|
||||
**Decision**: `default=uuid.uuid4` (gerado no Python)
|
||||
**Rationale**: Consistência com o padrão já adotado no model `Property`. O ID gerado em Python fica disponível antes do flush, simplificando testes e a construção da resposta de registro.
|
||||
**Alternatives considered**:
|
||||
- `server_default=text("gen_random_uuid()")` — gerado no banco, menos consistente com padrão existente
|
||||
|
||||
---
|
||||
|
||||
### 4. Normalização de e-mail
|
||||
|
||||
**Decision**: `email.lower().strip()` como `@field_validator` nos schemas Pydantic (`RegisterIn` e `LoginIn`)
|
||||
**Rationale**: FR-005 exige normalização para minúsculas. Fazê-la na camada de schema garante que o banco nunca armazene variantes de case, e que a comparação no login seja sempre consistente — sem precisar de `LOWER()` em queries SQL.
|
||||
|
||||
---
|
||||
|
||||
### 5. Armazenamento do token no frontend
|
||||
|
||||
**Decision**: `localStorage` com chave `imob_token`
|
||||
**Rationale**: Escolha MVP pragmática. HttpOnly cookies seriam mais seguros contra XSS, mas exigiriam configuração adicional de CORS e CSRF. O projeto usa Axios com requisições XHR; consistência com o padrão SPA do projeto.
|
||||
**Alternatives considered**:
|
||||
- `sessionStorage` — perde estado ao fechar aba; viola SC-005
|
||||
- HttpOnly cookie — mais seguro para produção, mas exigiria mudança na política CORS/CSRF (fora do escopo MVP)
|
||||
|
||||
**Risk noted**: Token em `localStorage` é vulnerável a XSS. Mitigação via CSP headers fica na responsabilidade de infraestrutura (fora do escopo desta feature). Documentado como débito de segurança para versão futura.
|
||||
|
||||
---
|
||||
|
||||
### 6. Axios interceptor vs. wrapper manual
|
||||
|
||||
**Decision**: `axios.interceptors.request.use` na instância existente em `services/api.ts`
|
||||
**Rationale**: Centraliza a injeção do header `Authorization` em um único ponto. FR-015 é satisfeito sem nenhuma alteração nas chamadas existentes. Adicionar ao objeto Axios existente evita fragmentação de instâncias (Princípio VI).
|
||||
**Alternatives considered**:
|
||||
- Wrapper manual por serviço — duplicação de código
|
||||
- Novo arquivo `axiosInstance.ts` — fragmenta a instância sem ganho
|
||||
|
||||
---
|
||||
|
||||
### 7. Estrutura do `require_auth` decorator
|
||||
|
||||
**Decision**: Decorator simples em `backend/app/utils/auth.py` usando `flask.g`
|
||||
**Rationale**: Abordagem mínima e idiomática no Flask. `g` é o mecanismo nativo de context-local para guardar estado por requisição. Sem nova dependência.
|
||||
**Pattern**:
|
||||
```python
|
||||
from functools import wraps
|
||||
from flask import g, request, jsonify
|
||||
import jwt
|
||||
|
||||
def require_auth(f):
|
||||
@wraps(f)
|
||||
def decorated(*args, **kwargs):
|
||||
token = _extract_token(request)
|
||||
if not token:
|
||||
return jsonify({"error": "Token de acesso inválido ou expirado."}), 401
|
||||
try:
|
||||
payload = jwt.decode(token, JWT_SECRET_KEY, algorithms=["HS256"])
|
||||
g.current_user = ClientUser.query.get(payload["sub"])
|
||||
if g.current_user is None:
|
||||
return jsonify({"error": "Token de acesso inválido ou expirado."}), 401
|
||||
except jwt.PyJWTError:
|
||||
return jsonify({"error": "Token de acesso inválido ou expirado."}), 401
|
||||
return f(*args, **kwargs)
|
||||
return decorated
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 8. `UserOut` sem `password_hash`
|
||||
|
||||
**Decision**: `UserOut` exclui `password_hash`
|
||||
**Rationale**: Nunca expor hash de senha em resposta da API. Princípio de menor privilégio (Constituição V). `UserOut` = {id, name, email, role, created_at}.
|
||||
|
||||
---
|
||||
|
||||
### 9. `pydantic[email]` para `EmailStr`
|
||||
|
||||
**Decision**: Adicionar `pydantic[email]` nas dependências (ou verificar se `email-validator` já está presente)
|
||||
**Rationale**: `pydantic.EmailStr` requer o pacote `email-validator`. Verificar `pyproject.toml` antes da tarefa de implementação — se ausente, adicionar junto com PyJWT e bcrypt.
|
||||
**Action**: A tarefa de instalação de dependências deve incluir `"pydantic[email]"` ou checar se `email-validator>=2.0` já está listado.
|
||||
Loading…
Add table
Add a link
Reference in a new issue