🖥️

Server Architecture

Audissey Server Architecture

Visão Geral

O backend da Audissey é uma plataforma NestJS monorepo (gerida por Nx) composta por duas aplicações Node.js e um serviço Python:

Aplicação Framework Porta Função
API NestJS + Express 3000 REST API principal, autenticação, CRUD, pesquisa, docs
Worker NestJS + BullMQ 3001 Ingestão automática, sync de conteúdo, jobs agendados
ML Service Python FastAPI 8000 Classificação de géneros, recomendações, embeddings

Diagrama de Arquitetura do Servidor

graph TB %% ═══════════════════════════════════════════ %% CLIENTS %% ═══════════════════════════════════════════ subgraph CLIENTS["📱 Clientes"] MOBILE["📱 Flutter App
Android / iOS"] SWAGGER["📖 Swagger UI
/api/api-docs"] DOCS_UI["📚 Docs Web
/api/docs"] end %% ═══════════════════════════════════════════ %% API APPLICATION %% ═══════════════════════════════════════════ subgraph API_APP["🌐 API Application (NestJS — :3000)"] direction TB subgraph MIDDLEWARE["⚙️ Middleware Pipeline"] direction LR CORS["CORS"] HELMET["Helmet"] CORR_ID["Correlation ID
(x-correlation-id)"] THROTTLE["ProxyAwareThrottlerGuard
100 req/min por cliente e rota"] VALIDATION["ValidationPipe
whitelist + transform"] LOG_INT["HttpLogging
Interceptor"] EX_FILTER["GlobalException
Filter"] end subgraph AUTH_MOD["🔐 Auth Module"] direction TB AUTH_CTRL["AuthController
/api/auth/*"] AUTH_SVC["AuthService"] JWT_STRAT["JwtStrategy
(Passport)"] JWT_GUARD["JwtAuthGuard"] AUTH_ENDPOINTS["Endpoints:
POST register
POST login
POST refresh
POST logout
GET email-exists
POST social/google
POST social/facebook
POST social/apple
POST passwordless/request
GET passwordless/open
POST passwordless/verify"] end subgraph RADIOS_MOD["📻 Radios Module"] direction TB RADIOS_CTRL["RadiosController
/api/radios/*"] PROGRAMS_CTRL["RadioProgramsController
/api/radio-programs/*"] SEED_CTRL_R["RadiosSeedController
/api/seed/radios"] RADIOS_SVC["RadiosService"] PROGRAMS_SVC["RadioProgramsService"] ICY_COLLECTOR["IcyCollectorService
Real-time metadata"] MYTUNER["MyTunerRadioService
EPG data"] RADIOS_ENDPOINTS["Endpoints:
CRUD radios
GET nearby / nearby-geo
GET :id/now-playing
GET :id/now-playing-program
GET :id/programs/today
POST/GET :id/schedule
POST :id/icy-epg/enable|disable
POST :id/icy-epg/collect-now
GET icy-epg/stats|enabled"] end subgraph PODCASTS_MOD["🎙️ Podcasts Module"] direction TB PODCASTS_CTRL["PodcastsController
/api/podcasts/*"] EPISODES_CTRL["EpisodesController
/api/episodes/*"] INGEST_CTRL["PodcastIngestController
/api/podcasts/ingest/*"] SYNC_CTRL["EpisodeSyncController
/api/episodes/sync/*"] SEED_CTRL_P["SeedController
/api/seed/podcasts"] PODCASTS_SVC["PodcastsService"] EPISODES_SVC["EpisodesService"] INGEST_SVC["PodcastIngestService"] PI_SVC["PodcastIndexService
Discovery API"] RSS_SVC["RSSParserService"] EPISODE_SYNC_SVC["EpisodeSyncService"] PODCASTS_ENDPOINTS["Endpoints:
CRUD podcasts
GET search
GET :id/episodes
CRUD episodes
POST ingest / ingest/bulk
POST episodes/sync / :podcastId
POST episodes/sync/global/recent"] end subgraph DISCOVERY_MOD["🔍 Discovery Modules"] direction TB HOME_CTRL["HomeController
/api/home/*"] HOME_SVC["HomeService"] SEARCH_CTRL["SearchController
/api/search/*"] SEARCH_SVC["SearchService"] DISCOVERY_ENDPOINTS["Endpoints:
GET home/highlights
GET search?q=...&type=..."] end subgraph USER_MOD["👤 Users Module"] direction TB USERS_CTRL["UsersController
/api/users/*"] USERS_SVC["UsersService"] USER_ENDPOINTS["Endpoints:
GET users/me
PATCH users/me"] end subgraph ADMIN_MOD["⚡ Admin Module"] direction TB ADMIN_CTRL["AdminController
/api/admin/*"] INGESTION_SVC["IngestionStatusService"] ADMIN_ENDPOINTS["Endpoints:
GET ingestion-status
GET ingestion-status/history
GET content-features/stats
GET content-features/:type/:id
GET content-features/search"] end subgraph SYSTEM_MOD["🏥 System Modules"] direction TB HEALTH_CTRL["HealthController
/api/health/*"] DOCS_CTRL["DocsController
/api/docs/*"] ALERTS_CTRL["AlertsController
/api/alerts/*"] ALERTS_SVC["AlertsService
Sender emails"] ALERTS_SCHED["AlertsScheduler
Cron checks"] SYSTEM_ENDPOINTS["Endpoints:
GET health / health/alive / health/info
GET docs / docs/view/:name / docs/raw/:name
GET alerts/status / alerts/check/:service"] end subgraph COMMON["🔧 Common (Shared)"] direction TB LOGGER["CustomLoggerService
Structured logging"] TELEMETRY["AppInsights
Telemetry client"] CF_SVC["ContentFeaturesService"] CN_SVC["ContentNormalizationService
38 géneros, 300+ variantes"] GENRES["GenreMappings
PT/EN/ES"] CF_SCHEMA["ContentFeatures Schema"] JOB_LOG_SCHEMA["JobExecutionLog Schema"] end end %% ═══════════════════════════════════════════ %% WORKER APPLICATION %% ═══════════════════════════════════════════ subgraph WORKER_APP["⚙️ Worker Application (NestJS — :3001)"] direction TB subgraph WORKER_JOBS["📋 Jobs Module"] direction TB JOBS_CTRL["JobsController
/api/jobs/*"] SYNC_RADIOS_PROC["SyncRadiosProcessor"] SYNC_PROGRAMS_PROC["SyncProgramsProcessor"] SYNC_PODCASTS_PROC["SyncPodcastsProcessor"] SYNC_EPISODES_PROC["SyncEpisodesProcessor"] JOBS_ENDPOINTS["Trigger Endpoints:
POST trigger/sync-radios
POST trigger/sync-programs
POST trigger/sync-podcasts
POST trigger/sync-podcasts-with-discovery
POST trigger/sync-episodes
GET stats"] end subgraph WORKER_RADIOS["📻 Radios Sync"] direction TB RB_SVC["RadioBrowserService
5000+ estações"] RS_SVC["RadiosSyncService
Upsert + normalize"] SV_SVC["StreamValidatorService
URL + bitrate check"] RS_SCHED["RadiosSyncScheduler
Cron: 03:00 daily"] end subgraph WORKER_PODCASTS["🎙️ Podcasts Sync"] direction TB PS_SVC["PodcastsSyncService
Podcast Index discovery"] ES_SVC["EpisodesSyncService
RSS feed parsing"] W_RSS_SVC["RSSParserService"] end end %% ═══════════════════════════════════════════ %% ML SERVICE %% ═══════════════════════════════════════════ subgraph ML_APP["🧠 ML Service (Python FastAPI — :8000)"] direction TB ML_CLASSIFY["GenreClassifier
BERT/RoBERTa"] ML_RECOMMEND["RecommendationModel
Collaborative filtering"] ML_EMBED["EmbeddingModel
MiniLM + wav2vec2"] end %% ═══════════════════════════════════════════ %% DATA LAYER %% ═══════════════════════════════════════════ subgraph DATA["💾 Data Layer"] direction TB subgraph MONGO["🍃 MongoDB / Cosmos DB"] direction TB COL_USERS["users"] COL_RADIOS["radios"] COL_PROGRAMS["radioprograms"] COL_SCHEDULES["radioschedules"] COL_ICY["icyhistories"] COL_PODCASTS["podcasts"] COL_EPISODES["episodes"] COL_CF["contentfeatures"] COL_JOBS["jobexecutionlogs"] end REDIS["🔴 Redis
BullMQ queues
Rate limiting cache"] end %% ═══════════════════════════════════════════ %% CONFIG & SECURITY %% ═══════════════════════════════════════════ subgraph CONFIG["🔐 Config & Security"] direction TB KV["Azure Key Vault
Secret loading on startup"] ENV_VAL["EnvironmentVariables
class-validator schema"] CONFIG_MOD["ConfigModule
isGlobal: true"] end %% ═══════════════════════════════════════════ %% EXTERNAL SERVICES %% ═══════════════════════════════════════════ subgraph EXTERNAL["🌍 External Services"] direction TB PODCAST_IDX["Podcast Index API"] RADIO_BROWSER["Radio Browser API"] MYTUNER_API["MyTuner Radio API"] GOOGLE_AUTH["Google OAuth"] FACEBOOK_AUTH["Facebook Login"] APPLE_AUTH["Apple Sign-In"] SENDER["Sender
Magic links + Alerts"] APP_INSIGHTS["Azure Application
Insights"] end %% ═══════════════════════════════════════════ %% CONNECTIONS %% ═══════════════════════════════════════════ %% Clients → API MOBILE -->|"HTTPS REST"| API_APP SWAGGER -->|"HTTP"| API_APP DOCS_UI -->|"HTTP"| API_APP %% API Middleware chain CORS --> HELMET --> CORR_ID --> THROTTLE --> VALIDATION --> LOG_INT --> EX_FILTER %% API → Data AUTH_MOD -->|"Mongoose"| MONGO RADIOS_MOD -->|"Mongoose"| MONGO PODCASTS_MOD -->|"Mongoose"| MONGO DISCOVERY_MOD -->|"Mongoose"| MONGO USER_MOD -->|"Mongoose"| MONGO ADMIN_MOD -->|"Mongoose"| MONGO %% Worker → Data WORKER_JOBS -->|"BullMQ"| REDIS WORKER_RADIOS -->|"Mongoose"| MONGO WORKER_PODCASTS -->|"Mongoose"| MONGO %% Config KV -->|"Startup load"| API_APP CONFIG_MOD -->|"process.env"| API_APP %% External integrations AUTH_SVC -->|"OAuth2"| GOOGLE_AUTH AUTH_SVC -->|"OIDC JWKS"| FACEBOOK_AUTH AUTH_SVC -->|"JWKS jose"| APPLE_AUTH AUTH_SVC -->|"Magic links"| SENDER ALERTS_SVC -->|"Email alerts"| SENDER TELEMETRY -->|"Telemetry"| APP_INSIGHTS RB_SVC -->|"HTTP"| RADIO_BROWSER MYTUNER -->|"HTTP"| MYTUNER_API PI_SVC -->|"HTTP"| PODCAST_IDX PS_SVC -->|"HTTP"| PODCAST_IDX %% API → ML API_APP -.->|"HTTP (futuro)"| ML_APP %% Styles classDef clientStyle fill:#e3f2fd,stroke:#1565c0,color:#0d47a1 classDef apiStyle fill:#e8f5e9,stroke:#2e7d32,color:#1b5e20 classDef workerStyle fill:#fff3e0,stroke:#ef6c00,color:#e65100 classDef mlStyle fill:#fce4ec,stroke:#c62828,color:#b71c1c classDef dataStyle fill:#f3e5f5,stroke:#7b1fa2,color:#4a148c classDef configStyle fill:#fffde7,stroke:#f9a825,color:#f57f17 classDef externalStyle fill:#e0f7fa,stroke:#00838f,color:#006064 class MOBILE,SWAGGER,DOCS_UI clientStyle class AUTH_CTRL,RADIOS_CTRL,PODCASTS_CTRL,HOME_CTRL,SEARCH_CTRL,USERS_CTRL,ADMIN_CTRL,HEALTH_CTRL,DOCS_CTRL,ALERTS_CTRL apiStyle class JOBS_CTRL,SYNC_RADIOS_PROC,SYNC_PROGRAMS_PROC,SYNC_PODCASTS_PROC,SYNC_EPISODES_PROC workerStyle class ML_CLASSIFY,ML_RECOMMEND,ML_EMBED mlStyle class COL_USERS,COL_RADIOS,COL_PROGRAMS,COL_SCHEDULES,COL_ICY,COL_PODCASTS,COL_EPISODES,COL_CF,COL_JOBS,REDIS dataStyle class KV,ENV_VAL,CONFIG_MOD configStyle class PODCAST_IDX,RADIO_BROWSER,MYTUNER_API,GOOGLE_AUTH,FACEBOOK_AUTH,APPLE_AUTH,SENDER,APP_INSIGHTS externalStyle

Módulos da API

Estrutura do Request Pipeline

graph LR REQ["📨 HTTP Request"] --> CORS["CORS
Origins permitidas"] CORS --> HELMET["Helmet
Security headers"] HELMET --> CORR["Correlation ID
x-correlation-id"] CORR --> THROTTLE["ProxyAwareThrottlerGuard
Rate limiting
100 req/60s por cliente e rota"] THROTTLE --> PIPE["ValidationPipe
whitelist: true
transform: true"] PIPE --> GUARD{"Auth Guard?"} GUARD -->|"Public"| CTRL["Controller"] GUARD -->|"Protected"| JWT["JwtAuthGuard
→ JwtStrategy
→ Bearer token"] JWT --> CTRL CTRL --> SVC["Service
Business Logic"] SVC --> DB["Mongoose
MongoDB"] DB --> RES["📤 Response"] CTRL --> LOG["HttpLogging
Interceptor"] CTRL -->|"Error"| FILTER["GlobalException
Filter"] classDef reqStyle fill:#e3f2fd,stroke:#1565c0,color:#0d47a1 classDef midStyle fill:#fff9c4,stroke:#f9a825,color:#f57f17 classDef authStyle fill:#ffccbc,stroke:#bf360c,color:#bf360c classDef bizStyle fill:#c8e6c9,stroke:#2e7d32,color:#1b5e20 classDef errStyle fill:#ffcdd2,stroke:#c62828,color:#b71c1c class REQ,RES reqStyle class CORS,HELMET,CORR,THROTTLE,PIPE midStyle class GUARD,JWT authStyle class CTRL,SVC,DB bizStyle class LOG,FILTER errStyle

Inventário Completo de Módulos

🔐 Auth Module (`/api/auth`)

Endpoint Método Auth Descrição
/auth/register POST 🔓 Registo com email/password
/auth/login POST 🔓 Login email/password → JWT
/auth/refresh POST 🔓 Refresh token → novo JWT
/auth/logout POST 🔒 Revoga tokens (tokenVersion++)
/auth/session GET 🔒 Valida sessão → { userId }
/auth/email-exists GET 🔓 Verifica se email existe (10/min/IP)
/auth/social/google POST 🔓 Login com Google OAuth
/auth/social/facebook POST 🔓 Login com Facebook OIDC
/auth/social/apple POST 🔓 Login com Apple Sign-In
/auth/passwordless/request POST 🔓 Pede magic link (Sender)
/auth/passwordless/open GET 🔓 Deep link → app com token
/auth/passwordless/verify POST 🔓 Verifica magic link token

Componentes:

📻 Radios Module (`/api/radios` + `/api/radio-programs`)

Endpoint Método Auth Descrição
/radios POST 🔒 Criar rádio
/radios GET 🔓 Listar rádios (filtros: genre, language, country, sort)
/radios/nearby GET 🔓 Rádios próximas — fallback sem coordenadas (country/city, ordenação por popularidade; idioma só reordena)
/radios/nearby-geo GET 🔓 Geo-query com raio adaptativo ($near + haversine fallback). Retorna distance_km. Raio expande automaticamente até 1000 km
/radios/:id GET 🔓 Detalhe de rádio
/radios/:id PUT 🔒 Atualizar rádio
/radios/:id DELETE 🔒 Remover rádio
/radios/:id/now-playing GET 🔓 ICY metadata atual
/radios/:id/now-playing-program GET 🔓 Programa atual via EPG
/radios/:id/programs/today GET 🔓 Grade do dia
/radios/:id/schedule POST 🔒 Submeter programação
/radios/:id/schedule GET 🔓 Ver programação (7 dias)
/radios/:id/icy-epg/enable POST 🔒 Ativar coleta ICY
/radios/:id/icy-epg/disable POST 🔒 Desativar coleta ICY
/radios/:id/icy-epg/collect-now POST 🔒 Coleta manual imediata
/radios/icy-epg/stats GET 🔓 Estatísticas ICY
/radios/icy-epg/enabled GET 🔓 Rádios com ICY ativo
/radio-programs CRUD Misto CRUD de programas gravados
/radio-programs/current GET 🔓 Programas ao vivo agora

As escritas do catálogo (POST/PUT/DELETE /radios e /radio-programs, POST /radios/:id/schedule, POST /radios/:id/icy-epg/*, POST /seed/radios) são só para admins com escrita (adminRole: admin): um utilizador ou um viewer recebe 403.

Componentes:

🎙️ Podcasts Module (`/api/podcasts` + `/api/episodes` + `/api/podcasts/ingest` + `/api/episodes/sync`)

Endpoint Método Auth Descrição
/podcasts POST 🔒 Criar podcast
/podcasts GET 🔓 Listar (language, category)
/podcasts/search GET 🔓 Pesquisa texto em podcasts
/podcasts/:id GET/PUT/DELETE Misto CRUD individual
/podcasts/:id/episodes GET 🔓 Listar episódios
/episodes CRUD Misto CRUD de episódios
/episodes/podcast/:podcastId GET 🔓 Episódios por podcast
/podcasts/ingest POST 🔒 Ingerir podcast (RSS → DB)
/podcasts/ingest/bulk POST 🔒 Ingestão em bulk
/episodes/sync POST 🔒 Sync episódios globalmente
/episodes/sync/:podcastId POST 🔒 Sync episódios de 1 podcast
/episodes/sync/global/recent POST 🔒 Sync episódios recentes
/seed/podcasts POST 🔒 Seed de dados de teste

As escritas de podcasts e episódios (POST/PUT/DELETE /podcasts e /episodes, POST /seed/podcasts), os syncs a pedido (POST /episodes/sync/*, que chamam a Podcast Index API) e a ingestão de feeds (POST /podcasts/ingest e /bulk, que vão buscar o URL dado, http(s) e no máximo 50 por pedido) são só para admins com escrita (adminRole: admin): um utilizador ou um viewer recebe 403.

Componentes:

🔍 Search Module (`/api/search`)

Endpoint Método Auth Descrição
/search GET 🔓 Pesquisa unificada (q, type, limit, skip)

Cross-collection: Pesquisa simultânea em Radio, Podcast, Episode com relevância.

🏠 Home Module (`/api/home`)

Endpoint Método Auth Descrição
/home/highlights GET 🔓 Feed personalizado (favoritos, local, trending, live, new)

Cross-collection: Agrega Radio, Podcast, RadioProgram — contextual por hora/localização.

👤 Users Module (`/api/users`)

Endpoint Método Auth Descrição
/users/me GET 🔒 Perfil do utilizador atual
/users/me PATCH 🔒 Atualizar perfil

⚡ Admin Module (`/api/admin`)

Endpoint Método Auth Descrição
/admin/ingestion-status GET 🔓 Status atual de ingestão
/admin/ingestion-status/history GET 🔓 Histórico de ingestões
/admin/content-features/stats GET 🔓 Estatísticas de content features
/admin/content-features/:type/:id GET 🔓 Feature de um conteúdo específico
/admin/content-features/search GET 🔓 Pesquisa em content features

🏥 Health Module (`/api/health`)

Endpoint Método Auth Descrição
/health GET 🔓 Health check completo (DB + deps)
/health/alive GET 🔓 Liveness probe
/health/info GET 🔓 Info da aplicação (versão, uptime)

🔔 Alerts (Common)

Endpoint Método Auth Descrição
/alerts/status GET 🔓 Estado dos serviços monitorizados
/alerts/check/:service GET 🔓 Verificar saúde de 1 serviço

📚 Docs Module (`/api/docs`)

Endpoint Método Auth Descrição
/docs GET 🔓 Índice de documentação (HTML)
/docs/view/:docName GET 🔓 Render Markdown → HTML com Mermaid
/docs/raw/:docName GET 🔓 Markdown raw

Worker Application

Diagrama de Processamento

graph TB subgraph TRIGGERS["⏰ Triggers"] CRON_R["Cron: 03:00
Sync Rádios"] CRON_P["Cron: 04:00
Sync Programação"] CRON_PC["Cron: 05:00
Sync Podcasts"] CRON_E["Cron: 06:00
Sync Episódios"] MANUAL["POST /api/jobs/trigger/*
Manual trigger"] end subgraph QUEUES["📋 BullMQ Queues (Redis)"] Q_R["sync-radios
concurrency: 1
rate: 1/min"] Q_P["sync-programs
concurrency: 1
rate: 1/min"] Q_PC["sync-podcasts
concurrency: 1
rate: 1/min"] Q_E["sync-episodes
concurrency: 2
rate: 5/min"] end subgraph PROCESSORS["⚙️ Processors"] direction TB subgraph RADIO_SYNC["📻 Radio Sync Pipeline"] RB_FETCH["RadioBrowserService
Fetch 5000+ estações"] RS_PROCESS["RadiosSyncService
Upsert + normalize"] SV_CHECK["StreamValidatorService
Test URLs + bitrate"] end subgraph PROGRAM_SYNC["📅 Program Sync Pipeline"] MT_FETCH["MyTuner API
Fetch EPG data"] EP_PROCESS["SyncProgramsProcessor
Upsert schedules"] end subgraph PODCAST_SYNC["🎙️ Podcast Sync Pipeline"] PI_DISCOVER["Podcast Index API
Discovery + trending"] PS_PROCESS["PodcastsSyncService
Discover + upsert"] end subgraph EPISODE_SYNC["📋 Episode Sync Pipeline"] RSS_PARSE["RSSParserService
Parse feeds"] ES_PROCESS["EpisodesSyncService
Upsert episodes"] end end subgraph OUTPUT["💾 Output"] DB["MongoDB / Cosmos DB"] LOGS["JobExecutionLog
Status + duração"] end CRON_R --> Q_R CRON_P --> Q_P CRON_PC --> Q_PC CRON_E --> Q_E MANUAL --> Q_R & Q_P & Q_PC & Q_E Q_R --> RB_FETCH --> RS_PROCESS --> SV_CHECK --> DB Q_P --> MT_FETCH --> EP_PROCESS --> DB Q_PC --> PI_DISCOVER --> PS_PROCESS --> DB Q_E --> RSS_PARSE --> ES_PROCESS --> DB RS_PROCESS --> LOGS EP_PROCESS --> LOGS PS_PROCESS --> LOGS ES_PROCESS --> LOGS classDef trigStyle fill:#e3f2fd,stroke:#1565c0,color:#0d47a1 classDef queueStyle fill:#fff3e0,stroke:#ef6c00,color:#e65100 classDef procStyle fill:#e8f5e9,stroke:#2e7d32,color:#1b5e20 classDef outStyle fill:#f3e5f5,stroke:#7b1fa2,color:#4a148c class CRON_R,CRON_P,CRON_PC,CRON_E,MANUAL trigStyle class Q_R,Q_P,Q_PC,Q_E queueStyle class RB_FETCH,RS_PROCESS,SV_CHECK,MT_FETCH,EP_PROCESS,PI_DISCOVER,PS_PROCESS,RSS_PARSE,ES_PROCESS procStyle class DB,LOGS outStyle

Módulos do Worker

Módulo Componentes Função
RadiosSyncModule RadioBrowserService, RadiosSyncService, StreamValidatorService, RadiosSyncScheduler Ingestão de rádios + validação de streams
PodcastsSyncModule PodcastsSyncService, EpisodesSyncService, RSSParserService Ingestão de podcasts + sync RSS
JobsModule JobsController, 4× Processors Orchestração BullMQ, triggers manuais, stats

Autenticação & Segurança

Fluxo de Autenticação

graph TB subgraph AUTH_FLOWS["🔐 Fluxos de Autenticação"] direction TB subgraph EMAIL_AUTH["📧 Email/Password"] REG["POST /auth/register
email + password + name"] LOGIN["POST /auth/login
email + password"] REG -->|"bcrypt hash"| DB_USER["User document
+ hashed password"] LOGIN -->|"bcrypt compare"| JWT_GEN["JWT Generation"] end subgraph SOCIAL_AUTH["🌐 Social Login"] GOOGLE["POST /auth/social/google
idToken"] FACEBOOK["POST /auth/social/facebook
token (Limited Login)"] APPLE["POST /auth/social/apple
identityToken + authCode"] GOOGLE -->|"google-auth-library
OAuth2Client.verifyIdToken"| VERIFY["Verify with provider"] FACEBOOK -->|"OIDC JWKS from
limited.facebook.com"| VERIFY APPLE -->|"jose JWKS from
appleid.apple.com"| VERIFY VERIFY -->|"findOrCreate user"| JWT_GEN end subgraph MAGIC_LINK["🔗 Passwordless"] PL_REQ["POST /auth/passwordless/request
email"] PL_OPEN["GET /auth/passwordless/open
token (deep link)"] PL_VERIFY["POST /auth/passwordless/verify
token"] PL_REQ -->|"Generate OTP
SHA-256 hash"| SENDER_ML["Sender
Magic link email"] SENDER_ML -->|"User clicks"| PL_OPEN PL_OPEN -->|"App deep link"| PL_VERIFY PL_VERIFY -->|"Verify hash + TTL"| JWT_GEN end end subgraph JWT_LAYER["🎟️ JWT Layer"] JWT_GEN -->|"Sign"| ACCESS["Access Token
24h expiry"] JWT_GEN -->|"Sign"| REFRESH["Refresh Token
30d expiry"] ACCESS -->|"Bearer header"| JWT_STRAT_D["JwtStrategy
(Passport)"] JWT_STRAT_D -->|"Validate"| JWT_GUARD_D["JwtAuthGuard"] REFRESH -->|"POST /auth/refresh"| JWT_GEN end subgraph TOKEN_MGMT["🔄 Token Management"] LOGOUT["POST /auth/logout"] -->|"tokenVersion++"| REVOKE["Todos os tokens
anteriores inválidos"] end classDef emailStyle fill:#e8f5e9,stroke:#2e7d32,color:#1b5e20 classDef socialStyle fill:#e3f2fd,stroke:#1565c0,color:#0d47a1 classDef magicStyle fill:#f3e5f5,stroke:#7b1fa2,color:#4a148c classDef jwtStyle fill:#fff9c4,stroke:#f9a825,color:#f57f17 classDef revokeStyle fill:#ffcdd2,stroke:#c62828,color:#b71c1c class REG,LOGIN,DB_USER emailStyle class GOOGLE,FACEBOOK,APPLE,VERIFY socialStyle class PL_REQ,PL_OPEN,PL_VERIFY,SENDER_ML magicStyle class JWT_GEN,ACCESS,REFRESH,JWT_STRAT_D,JWT_GUARD_D jwtStyle class LOGOUT,REVOKE revokeStyle

Camadas de Segurança

Camada Implementação Detalhe
CORS NestJS CORS Origins configuráveis
Headers Helmet Security headers automáticos
Rate Limiting ThrottlerModule 100 req/60s por cliente e rota; usa o IP validado mais à direita de X-Forwarded-For no Azure Container Apps
Validation ValidationPipe whitelist + forbidNonWhitelisted + transform
Authentication Passport JWT Bearer token em Authorization header
Password Hashing bcrypt Salt rounds configurável
Token Revocation tokenVersion Incremento invalida todos os tokens
Secrets Azure Key Vault Loaded on startup, 5min cache
Logging CustomLoggerService Structured JSON com correlationId
Exception Handling GlobalExceptionFilter Error format padronizado
Telemetry Application Insights Errors, requests, custom events

Pedidos HTTP de saída

Por omissão, os módulos da API e do worker usam publicHttpModuleOptions para
pedidos a URLs guardados ou de terceiros: agentes de endereço público validam o
DNS e os IPs literais ao estabelecer a ligação, verificam cada redirecionamento e
desativam proxies definidos no ambiente. Apenas InternalHttpService acede aos
nossos serviços (ML, API e Azure). Os testes http-module-guard.spec.ts da API e do
worker exigem estas configurações e limitam os pontos de injeção do cliente interno.
SSRF_ALLOW_PRIVATE_URLS=true destina-se exclusivamente ao desenvolvimento local.
Os clientes protegidos dos módulos seguem no máximo 5 redirecionamentos (antes,
alguns usavam o limite de 21 do axios) e mantêm as ligações abertas; o worker
valida stream_url antes de pedir reconhecimento ao ML, mas os pedidos feitos
pelo próprio serviço ML (reconhecimento e URLs de imagens em extract-schedule)
ainda não estão protegidos.

O Chromium headless resolve o DNS por si próprio: a proteção nesse percurso
valida apenas os URLs e não cobre DNS rebinding.

Gestão de Configuração

Key Vault → Environment Variables

graph LR subgraph KV["🔐 Azure Key Vault"] direction TB KV1["mongo-root-username"] KV2["mongo-root-password"] KV3["mongo-uri"] KV4["jwt-secret"] KV5["podcast-index-api-key"] KV6["podcast-index-api-secret"] KV7["google-oauth-client-ids"] KV8["google-server-client-id"] KV9["facebook-app-id"] KV10["facebook-app-secret"] KV11["redis-connection-string"] end subgraph STARTUP["⏯️ Startup (main.ts)"] INIT["initializeKeyVault()
Before NestFactory.create"] LOAD["loadSecretsIntoEnv()
For each secret mapping"] end subgraph ENV["💻 process.env"] E1["MONGO_ROOT_USERNAME"] E2["MONGO_ROOT_PASSWORD"] E3["MONGO_URI / MONGODB_URI"] E4["JWT_SECRET"] E5["PODCAST_INDEX_API_KEY"] E6["PODCAST_INDEX_API_SECRET"] E7["GOOGLE_OAUTH_CLIENT_IDS"] E8["GOOGLE_SERVER_CLIENT_ID"] E9["FACEBOOK_APP_ID"] E10["FACEBOOK_APP_SECRET"] E11["REDIS_CONNECTION_STRING"] end subgraph APP["📦 ConfigModule"] CM["ConfigService.get()"] end KV -->|"1. Startup"| STARTUP STARTUP -->|"2. Map secrets"| ENV ENV -->|"3. ConfigModule.forRoot()"| APP classDef kvStyle fill:#fff9c4,stroke:#f9a825,color:#f57f17 classDef startStyle fill:#e8f5e9,stroke:#2e7d32,color:#1b5e20 classDef envStyle fill:#e3f2fd,stroke:#1565c0,color:#0d47a1 classDef appStyle fill:#f3e5f5,stroke:#7b1fa2,color:#4a148c class KV1,KV2,KV3,KV4,KV5,KV6,KV7,KV8,KV9,KV10,KV11 kvStyle class INIT,LOAD startStyle class E1,E2,E3,E4,E5,E6,E7,E8,E9,E10,E11 envStyle class CM appStyle

Fluxo de startup:

  1. main.ts chama initializeKeyVault() antes de criar o NestFactory
  2. KeyVaultService.loadSecretsIntoEnv() mapeia cada secret → process.env
  3. Secrets opcionais (social auth, Redis, ML) não falham startup se ausentes
  4. ConfigModule.forRoot() captura process.env com secrets carregados
  5. EnvironmentVariables (class-validator) valida as variáveis obrigatórias

Schemas MongoDB

Relações entre Collections

erDiagram USER ||--o{ RADIO : "favorites (radioIds)" USER { string email PK string password string name int age string preferredLanguage string timezone array favoriteRadioIds string googleSub string facebookId string appleSub boolean isPremium date subscriptionExpiry int tokenVersion string passwordlessToken date passwordlessExpiry } RADIO ||--o{ RADIO_SCHEDULE : "programação" RADIO ||--o{ RADIO_PROGRAM : "programas gravados" RADIO ||--o{ ICY_HISTORY : "metadata history" RADIO { string radioId PK string name string url string urlResolved array alternativeUrls string country string countryCode string state array genres array tags string codec int bitrate float geoLat float geoLong string nowPlaying string currentProgram int clickCount int votes string favicon string homepage } RADIO_SCHEDULE { ObjectId radioId FK json programs string timezone enum source } RADIO_PROGRAM { ObjectId radioId FK string title string startTime string endTime array hosts string description string genre } ICY_HISTORY { ObjectId radioId FK string title date capturedAt json metadata } PODCAST ||--o{ EPISODE : "episódios" PODCAST { string podcastIndexId PK string title string author string feedUrl string artwork array categories string language int episodeCount float rating int reviewCount float trendingScore boolean featured int subscriptions } EPISODE { ObjectId podcastId FK string title int duration string enclosureUrl int episodeNumber int seasonNumber string description date datePublished int plays int likes int comments } CONTENT_FEATURES { enum contentType string contentId FK array normalizedGenres string isoLanguage string isoCountry array textEmbedding array audioEmbedding json popularityMetrics array keywords json temporalFeatures } JOB_EXECUTION_LOG { string jobType date startedAt date completedAt enum status int itemsProcessed int errors json metadata } RADIO ||--o{ CONTENT_FEATURES : "ML metadata" PODCAST ||--o{ CONTENT_FEATURES : "ML metadata" EPISODE ||--o{ CONTENT_FEATURES : "ML metadata"

Resumo de Endpoints (Total: ~70+)

Módulo Prefix Endpoints Descrição
Auth /auth 11 Registo, login, social, passwordless, refresh, logout
Radios /radios 17 CRUD, geo, now-playing, EPG, ICY, schedule
Radio Programs /radio-programs 7 CRUD programas gravados, current
Podcasts /podcasts 7 CRUD, search, episodes listing
Episodes /episodes 6 CRUD episódios
Podcast Ingest /podcasts/ingest 2 Ingestão single + bulk
Episode Sync /episodes/sync 3 Sync global, por podcast, recentes
Home /home 1 Highlights personalizados
Search /search 1 Pesquisa unificada
Users /users 2 Perfil GET/PATCH
Admin /admin 5 Ingestion status, content features
Health /health 3 Alive, health, info
Docs /docs 3 Index, view, raw
Alerts /alerts 2 Status, check service
Seed /seed 2 Radios + podcasts seed
Worker Jobs /jobs (Worker) 6 Triggers manuais + stats