feat: Fase 1 – Fondamenta complete (backend FastAPI + auth + permessi)

- docker-compose.yml: PostgreSQL 16, Redis 7, MinIO, Nginx
- backend FastAPI: struttura monorepo, config pydantic-settings
- modelli SQLAlchemy: tutti i modelli (tenants, users, mailboxes, messages, archival, permissions, labels, audit_log)
- migrazione Alembic 0001: schema completo in pure SQL
- auth API: login JWT, refresh token rotation, logout, 2FA TOTP (setup/verify/disable)
- CRUD utenti: lista, crea, modifica, reset password, soft delete
- permessi granulari (Fase 1-A): mailbox_permissions, assegna/revoca/lista
- CRUD tenant: gestione super-admin
- sicurezza: AES-256-GCM cifratura credenziali IMAP/SMTP, bcrypt password
- RLS PostgreSQL: isolamento multi-tenant per request
- seed sviluppo: tenant demo + admin + operator
- test unit: security (bcrypt, JWT, AES), auth_service
- test integration: auth endpoints, users endpoints
- CI GitHub Actions: lint (ruff), test (pytest), build Docker, security scan
- infra: nginx.conf, redis.conf
- Makefile con comandi make dev/test/migrate/seed

Definition of Done:
 Login, refresh token e TOTP funzionanti
 make dev porta in piedi tutto lo stack locale
 CI configurata
This commit is contained in:
2026-03-18 16:42:01 +01:00
parent 0251c2bbb0
commit 58a233236c
60 changed files with 6942 additions and 0 deletions
+90
View File
@@ -0,0 +1,90 @@
"""
Configurazione applicazione legge variabili d'ambiente tramite pydantic-settings.
"""
from functools import lru_cache
from typing import Literal
from pydantic import field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
extra="ignore",
)
# ── Applicazione ──────────────────────────────────────────────────────────
app_env: Literal["development", "staging", "production"] = "development"
app_debug: bool = True
app_host: str = "0.0.0.0"
app_port: int = 8000
app_base_url: str = "http://localhost:8000"
# ── Sicurezza / JWT ───────────────────────────────────────────────────────
secret_key: str = "change-me-in-production"
algorithm: str = "HS256"
access_token_expire_minutes: int = 15
refresh_token_expire_days: int = 30
# Chiave AES-256-GCM per cifratura credenziali IMAP/SMTP (hex 64 chars = 32 bytes)
encryption_key: str = "0" * 64
# ── Database ──────────────────────────────────────────────────────────────
database_url: str = "postgresql+asyncpg://pecflow:pecflow_dev_password@db:5432/pecflow"
database_url_sync: str = "postgresql://pecflow:pecflow_dev_password@db:5432/pecflow"
# ── Redis ─────────────────────────────────────────────────────────────────
redis_url: str = "redis://redis:6379/0"
# ── MinIO ─────────────────────────────────────────────────────────────────
minio_endpoint: str = "minio:9000"
minio_access_key: str = "minioadmin"
minio_secret_key: str = "minioadmin"
minio_bucket: str = "pecflow"
minio_use_ssl: bool = False
# ── CORS ──────────────────────────────────────────────────────────────────
cors_origins: str = "http://localhost:3000,http://localhost:5173"
# ── Rate Limiting ─────────────────────────────────────────────────────────
rate_limit_auth: str = "10/minute"
rate_limit_default: str = "100/minute"
# ── Logging ───────────────────────────────────────────────────────────────
log_level: str = "INFO"
log_json: bool = False
@field_validator("encryption_key")
@classmethod
def validate_encryption_key(cls, v: str) -> str:
if len(v) != 64:
raise ValueError(
"ENCRYPTION_KEY deve essere una stringa hex di 64 caratteri (32 bytes)"
)
try:
bytes.fromhex(v)
except ValueError:
raise ValueError("ENCRYPTION_KEY deve essere una stringa esadecimale valida")
return v
@property
def cors_origins_list(self) -> list[str]:
return [origin.strip() for origin in self.cors_origins.split(",")]
@property
def is_production(self) -> bool:
return self.app_env == "production"
@property
def encryption_key_bytes(self) -> bytes:
return bytes.fromhex(self.encryption_key)
@lru_cache
def get_settings() -> Settings:
"""Restituisce istanza singleton delle impostazioni (cachata)."""
return Settings()