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
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
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:
AuthService— Validação de tokens sociais, geração JWT, magic linksJwtStrategy— Passport strategy para Bearer tokensJwtAuthGuard— Guard reutilizável em endpoints protegidos- DTOs:
RegisterDto,LoginDto,SocialGoogleDto,SocialFacebookDto,SocialAppleDto,PasswordlessRequestDto,PasswordlessVerifyDto,RefreshDto
📻 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:
RadiosService— CRUD rádios, queries geo, filtrosRadioProgramsService— CRUD programas gravadosIcyCollectorService— Coletor de ICY metadata em tempo realIcyMetadataService— Parser de streams ICYMyTunerRadioService— Cliente da MyTuner API para EPG- Schemas:
Radio,RadioProgram,RadioSchedule,IcyHistory
🎙️ 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:
PodcastsService— CRUD podcastsEpisodesService— CRUD episódiosPodcastIngestService— Ingestão completa (RSS parse + upsert)PodcastIndexService— Cliente Podcast Index APIEpisodeSyncService— Sincronização de episódios via RSSRSSParserService— Parser de feeds RSS/Atom- Schemas:
Podcast,Episode
🔍 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
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
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
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:
main.tschamainitializeKeyVault()antes de criar o NestFactoryKeyVaultService.loadSecretsIntoEnv()mapeia cada secret →process.env- Secrets opcionais (social auth, Redis, ML) não falham startup se ausentes
ConfigModule.forRoot()capturaprocess.envcom secrets carregadosEnvironmentVariables(class-validator) valida as variáveis obrigatórias
Schemas MongoDB
Relações entre Collections
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 |