From 9352cf4b5359f64790a6197177574b7e0d297063 Mon Sep 17 00:00:00 2001 From: aniebietafia Date: Thu, 14 May 2026 12:12:24 +0100 Subject: [PATCH 1/2] docs: fixing documentation and cleanup Signed-off-by: aniebietafia --- app/core/api-docs.md | 77 -- app/db/api-docs.md | 59 - app/external_services/cloudinary/api-docs.md | 116 -- app/external_services/deepgram/api-docs.md | 71 -- app/external_services/deepl/api-docs.md | 71 -- app/external_services/openai_tts/api-docs.md | 73 -- app/external_services/voiceai/api-docs.md | 74 -- app/kafka/api-docs.md | 122 -- app/models/api-docs.md | 84 -- app/modules/auth/api-docs.md | 1053 ------------------ app/modules/meeting/api-docs.md | 584 ---------- app/modules/meeting/ws_router.py | 7 +- app/modules/user/api-docs.md | 242 ---- app/routers/api-docs.md | 107 -- app/schemas/api-docs.md | 152 --- app/services/api-docs.md | 91 -- 16 files changed, 3 insertions(+), 2980 deletions(-) delete mode 100644 app/core/api-docs.md delete mode 100644 app/db/api-docs.md delete mode 100644 app/external_services/cloudinary/api-docs.md delete mode 100644 app/external_services/deepgram/api-docs.md delete mode 100644 app/external_services/deepl/api-docs.md delete mode 100644 app/external_services/openai_tts/api-docs.md delete mode 100644 app/external_services/voiceai/api-docs.md delete mode 100644 app/kafka/api-docs.md delete mode 100644 app/models/api-docs.md delete mode 100644 app/modules/auth/api-docs.md delete mode 100644 app/modules/meeting/api-docs.md delete mode 100644 app/modules/user/api-docs.md delete mode 100644 app/routers/api-docs.md delete mode 100644 app/schemas/api-docs.md delete mode 100644 app/services/api-docs.md diff --git a/app/core/api-docs.md b/app/core/api-docs.md deleted file mode 100644 index cfc4418..0000000 --- a/app/core/api-docs.md +++ /dev/null @@ -1,77 +0,0 @@ -# FluentMeet Core Application Documentation - -> **Package Location:** `/app/core` -> **Purpose:** Houses all fundamental components that are globally shared across the entire application ecosystem, strictly agnostic of specific application models. - ---- - -## Table of Contents - -- [Overview](#overview) -- [Configuration (`config.py`)](#configuration-configpy) -- [Security (`security.py`)](#security-securitypy) -- [Exception Handlers & Responses](#exception-handlers--responses) -- [System Dependencies (`dependencies.py`)](#system-dependencies-dependenciespy) -- [Sanitization (`sanitize.py`)](#sanitization-sanitizepy) - ---- - -## Overview - -The `app/core` package serves as the backbone of the application. It bootstraps application config configurations asynchronously, intercepts exceptions homogeneously, drives system security schemas securely, and houses FastApi `Depends()` routines globally to evade circular imports. - ---- - -## Configuration (`config.py`) - -Leverages `pydantic_settings`. - -### The `Settings` Object -* Extracts natively parameters stored inside `./.env` matching dynamically against types automatically parsing logic. -* Resolves variables for Database URls, JWT Secrets, Redis caches, Kafka bootstrap brokers explicitly, and cloud provider APIs like OpenAI / DL environments seamlessly. -* Forces dynamic fallback loading the PyProject version using `tomllib`. - ---- - -## Security (`security.py`) - -Handles cryptographic payload verification schemas explicitly without accessing Database constructs seamlessly. - -* **Bcrypt Password Context:** `hash_password()` and `verify_password()`. - * Implements a native exception wrapper patching standard deprecated `passlib` behaviors failing aggressively on unmanaged `bcrypt 4.0.0+` versions transparently overriding bounds dynamically. -* **JWT Creation (`encode`):** - * `create_access_token()`: Returns a short-lived token using explicit TTL mappings native to configuration structures (expiring natively in ~60mins). - * `create_refresh_token()`: Returns a long-lived tuple returning the securely allocated JTI identifier logic explicitly mappings directly against settings (e.g., 7 days). - ---- - -## Exception Handlers & Responses - -### Responses (`error_responses.py`) -Standardizes REST API outputs homogenously guaranteeing frontend UI frameworks never fail parsing generic trace responses gracefully. - -* `ErrorDetail`: Nested lists explicitly tracking localized parameter validation triggers dynamically. -* `ErrorResponse`: Unifies status, descriptor `code`, human-readable `message` securely. - -### Handlers (`exception_handlers.py`) -Registered on core startup logic intercepting framework exceptions dynamically. - -* Converts Starlette/FastAPI `RequestValidationError` cleanly into `400` validation constraints structures. -* Binds generic unhandled HTTP 500 stacks dynamically dumping details efficiently via `sanitize_for_log()`. - -### Custom Error Framework (`exceptions.py`) -Developers natively invoke `raise BadRequestException("Missing ID")` mapping gracefully dynamically down to HTTP structures utilizing the Handlers. Allows custom error codes defined seamlessly (e.g. `code="INVALID_OTP"` natively mapped). - ---- - -## System Dependencies (`dependencies.py`) - -Decouples authentication blocks natively allowing models mapping efficiently natively circumventing explicit Circular dependencies seamlessly. - -Provides FastApi injectable logic defining explicit Token/Bearer evaluations transparently parsing JWT variables gracefully extracting explicit target entities locally from the Database dynamically checking `is_active` flags before propagating securely to Endpoint Routers automatically. - ---- - -## Sanitization (`sanitize.py`) - -Intercepts log mechanisms aggressively globally preventing explicit log-spoofing injection vectors smoothly intercepting inputs wrapping string payloads automatically truncating heavy lengths tracking string components securely natively tracking unmanaged inputs across routes dynamically. diff --git a/app/db/api-docs.md b/app/db/api-docs.md deleted file mode 100644 index e1b2099..0000000 --- a/app/db/api-docs.md +++ /dev/null @@ -1,59 +0,0 @@ -# FluentMeet DB Core Documentation - -> **Package Location:** `/app/db` -> **Purpose:** Configures global synchronous SQLAlchemy engines and database dependency generators for FastAPI route bindings. - ---- - -## Table of Contents - -- [Overview](#overview) -- [Architecture](#architecture) -- [Public API (`session.py`)](#public-api-sessionpy) -- [Fallback Mechanisms](#fallback-mechanisms) - ---- - -## Overview - -The `app/db` package encapsulates all direct setup hooks binding ORM actions to the backing relational database (PostgreSQL predominantly). It securely controls Engine Lifecycles so connections are aggressively pooled rather than blindly spun up over HTTP requests. - ---- - -## Architecture - -This package adopts a synchronous SQLAlchemy standard methodology natively configuring `sqlalchemy.orm.Session`. Since heavy async code exists largely on the Kafka Real-time periphery (`app/services`) rather than standard user-CRUD actions, maintaining a stable sync DB API avoids extensive threading deadlocks. - -To maximize usability across developers who configure robust `asyncpg` bindings globally via `.env`, the module intentionally coerces engine driver definitions natively upon launch mapping `asyncpg` configurations forcibly into `psycopg` parameters transparently without failing. - ---- - -## Public API (`session.py`) - -### Connection Contexts - -#### `get_engine()` -Acts as a lazy-loaded Singleton accessor fetching the Global cache `_ENGINE_STATE`. -* **Behavior:** Checks the dict. If `None`, triggers `create_engine` appending `pool_pre_ping=True` and binds the global `SessionLocal` macro. -* **Returns:** Returns an actively configured `sqlalchemy.engine.Engine`. - -#### `get_db()` -A standard Python Generator used specifically as a wrapper `Depends(get_db)` inside FastAPI Routers logic. -* **Behavior:** Fetches a local thread-isolated `Session` bounding its lifecycle to a strict `try-finally` context forcing cleanup queries. -* **Yields:** Returning a strict `sqlalchemy.orm.Session` reference resolving automatically explicitly returning connections back to the `Engine` pool upon return. - -### Interceptors - -#### `_coerce_sync_url(url)` -An internal helper bridging application boundaries. -* **Behavior:** Actively intercepts the raw string extracted from `settings.DATABASE_URL`. If standard `+asyncpg` bindings are detected, it rewrites the string dynamically returning `postgresql+psycopg2://...`. -* **Args:** `url` *(str)* -* **Returns:** Mutated valid sync DB *(str)*. - ---- - -## Fallback Mechanisms - -To ensure Continuous Integration structures and simple developer test suites execute natively without manually spinning up Postgres docker containers universally, the DB context falls back elegantly natively inside `get_engine()`. - -If `psycopg` is missing upon a load initialization triggered by an API request (`ModuleNotFoundError` exception thrown during driver bindings), the system suppresses the error logic falling back internally provisioning an ephemeral SQLite database on local paths (`sqlite:///./fluentmeet.db`) allowing raw ORM testing natively bypassing strict configurations. diff --git a/app/external_services/cloudinary/api-docs.md b/app/external_services/cloudinary/api-docs.md deleted file mode 100644 index a1960df..0000000 --- a/app/external_services/cloudinary/api-docs.md +++ /dev/null @@ -1,116 +0,0 @@ -# FluentMeet Cloudinary SDK Documentation - -> **Package Location:** `/app/external_services/cloudinary` -> **Purpose:** Abstracted Cloud Storage service wrapping the official Cloudinary Python SDK. - ---- - -## Table of Contents - -- [Overview](#overview) -- [Architecture & Setup](#architecture--setup) -- [Public Providers API (`service.py`)](#public-providers-api-servicepy) -- [Validation & Constants](#validation--constants) -- [Data Schemas (`schemas.py`)](#data-schemas-schemaspy) -- [Error Handling (`exceptions.py`)](#error-handling-exceptionspy) - ---- - -## Overview - -The `app/external_services/cloudinary` package provides a fully decoupled, strongly-typed layer over the `cloudinary` SDK. It exposes the `StorageService` to handle asynchronous file uploads limit-checks, MIME validations, and secure resource deletions without leaking Cloudinary's specific configuration logic into the rest of the application (e.g., User or Meeting routers). - ---- - -## Architecture & Setup - -### Initialization (`config.py`) - -Configuration runs lazily utilizing an `ensure_configured()` interceptor. -When the `StorageService` is instantiated for the first time by the `get_storage_service()` FastAPI dependency, it reaches out to `config.py` which pulls: - -- `CLOUDINARY_CLOUD_NAME` -- `CLOUDINARY_API_KEY` -- `CLOUDINARY_API_SECRET` - -from the application `settings` and statically configures the `cloudinary.config(secure=True)` global. Tracking the bool state `_configured` prevents redundant config payload calls. - ---- - -## Public Providers API (`service.py`) - -The `StorageService` contains distinct semantic methods. Under the hood, these methods delegate to a private `_upload()` coroutine after rigorously enforcing validations. - -### Upload Methods - -All upload methods require a `FastAPI.UploadFile` and a target `folder`. They natively support providing an optional `public_id` to enforce naming conventions (like using a User UUID for their avatar so it automatically overwrites). - -* `upload_image(...)` - * **Enforces:** Image Mimetypes & Image Size Limit. - * **Features:** Supports passing a dictionary chunk `transformation` (e.g., bounding box cropping, face targeting) to natively crop representations on the CDN before resting. - -* `upload_video(...)` - * **Enforces:** Video Mimetypes & Video Size Limit. - -* `upload_raw(...)` - * **Enforces:** Static Mimetypes (PDFs, ZIPs, txt) and uses Image Size limit. - -### Delete Method - -* `delete_asset(public_id: str, resource_type: str)` - * Provides targeted resource teardown ensuring GDPR erasure compliance on User and Asset destruction. - ---- - -## Validation & Constants - -The package proactively protects the API from malformed or malicious file dumps. -Defined in `constants.py`: - -**MIME Types Allowed:** -* **Images:** `image/jpeg`, `image/png`, `image/webp`, `image/gif`, `image/svg+xml` -* **Videos:** `video/mp4`, `video/webm`, `video/quicktime`, `video/x-msvideo` -* **Static:** `application/pdf`, `application/zip`, `text/plain`, `text/csv` - -**Folder Namespacing:** -Allows environment safety. `FOLDER_AVATARS`, `FOLDER_RECORDINGS`, `FOLDER_UPLOADS`. - -**Size Validations:** -The internal `_validate_file` scans both the incoming HTTP `content_type` header string and calculates the `file.size` threshold dynamically against the respective `.env` limit mappings. - ---- - -## Data Schemas (`schemas.py`) - -To decouple the application router returns from Cloudinary's raw dynamic `dict` responses, outcomes are strictly marshaled via Pydantic: - -### `UploadResult` -```python -{ - "public_id": "fluentmeet/avatars/abx123", - "secure_url": "https://res.cloudinary.com/...", - "resource_type": "image", - "format": "webp", - "bytes": 481023, - "width": 400, - "height": 400 -} -``` - -### `DeleteResult` -```python -{ - "public_id": "fluentmeet/avatars/abx123", - "result": "ok" # or "not found" -} -``` - ---- - -## Error Handling (`exceptions.py`) - -The service raises context-specific HTTP exceptions inheriting appropriately from Base classes so FastAPI naturally constructs HTTP 400s or 500s: - -* **`FileValidationError`** (400 Bad Request): Thrown synchronously when MIME or Megabyte limits are exceeded before a network call is made. -* **`StorageUploadError`** (500 Internal Error): Thrown if the Cloudinary API rejects the data packet. -* **`StorageDeleteError`** (500 Internal Error): Thrown if an explicit API delete fails fatally. diff --git a/app/external_services/deepgram/api-docs.md b/app/external_services/deepgram/api-docs.md deleted file mode 100644 index 6b64244..0000000 --- a/app/external_services/deepgram/api-docs.md +++ /dev/null @@ -1,71 +0,0 @@ -# FluentMeet Deepgram Integration Documentation - -> **Package Location:** `/app/external_services/deepgram` -> **Purpose:** Handles external asynchronous integrations with the Deepgram Speech-to-Text API. - ---- - -## Table of Contents - -- [Overview](#overview) -- [Architecture](#architecture) -- [Public API](#public-api) -- [Configuration](#configuration) - ---- - -## Overview - -The `app/external_services/deepgram` package wraps the Deepgram REST `/v1/listen` endpoint natively enabling extremely fast conversion of `bytes` objects into text Strings. - -It is designed to be fully stateless and heavily depends on FastAPI standard dependencies & `httpx.AsyncClient` objects rather than installing Deepgram's heavy Python SDK, preserving application footprint and avoiding dependency bloat. - ---- - -## Architecture - -This package exposes a single class `DeepgramSTTService` bound as a Singleton. -It is actively injected and utilized globally by the `STTWorker` consumer daemon listening to Kafka `audio.raw`. - -### Execution Flow -1. Receives base64-decoded PCM strings. -2. Injects required API metadata mapping to settings boundaries. -3. Fires the `POST` request out asynchronously to the web REST Endpoint returning results. - ---- - -## Public API - -### `DeepgramSTTService` (`service.py`) - -A fully typed async service wrapping the REST endpoint. - -#### `transcribe(audio_bytes, language, sample_rate, encoding)` -Sends a block of data to Deepgram to fetch an interpretation. -* **Args:** - * `audio_bytes` *(bytes)*: Standard PCM binary string or OPUS stream bytes. - * `language` *(str)*: A localized ISO 639-1 code hint (e.g., `"en"`). - * `sample_rate` *(int)*: Standard `16000` (Hz). - * `encoding` *(str)*: Tells Deepgram the format (`"linear16"` or `"opus"`). -* **Returns:** - Returns a unified `dict` payload structure standard against multiple engines: - ```python - { - "text": "Hello world", - "confidence": 0.99, - "detected_language": "en", - "latency_ms": 32.5 - } - ``` -* **Exception Behavior:** Raises `httpx.HTTPStatusError` aggressively when anything other than an HTTP 2xx code is returned to enforce fallback failure and Dead-Letter-Queue routing in the caller blocks. - ---- - -## Configuration - -### `get_deepgram_headers()` (`config.py`) - -Ensures the authentication mechanisms are mapped securely from environment definitions. - -* Builds the dict mapping `Authorization: Token ` -* Fails fast natively issuing `RuntimeError` on startup if `DEEPGRAM_API_KEY` is completely missing from `.env` or Server Environment. diff --git a/app/external_services/deepl/api-docs.md b/app/external_services/deepl/api-docs.md deleted file mode 100644 index 813c63a..0000000 --- a/app/external_services/deepl/api-docs.md +++ /dev/null @@ -1,71 +0,0 @@ -# FluentMeet DeepL & LLM Translation Documentation - -> **Package Location:** `/app/external_services/deepl` -> **Purpose:** Handles external asynchronous integrations with the DeepL `/v2/translate` API and provides OpenAI LLM algorithmic fallbacks. - ---- - -## Table of Contents - -- [Overview](#overview) -- [Architecture](#architecture) -- [Public API (`service.py`)](#public-api-servicepy) -- [Fallback Mechanisms](#fallback-mechanisms) -- [Language Code Mapping](#language-code-mapping) - ---- - -## Overview - -The `app/external_services/deepl` package acts as the active backend for stage 3 of the real-time audio pipeline. It intercepts STT transcriptions and converts them dynamically into alternate target languages required by individual users in the meeting lobby. - ---- - -## Architecture - -To remain fully stateless and incredibly lightweight without depending on strict external third-party SDKs, the translation engines fire purely via `httpx.AsyncClient` objects wrapping the provider APIs. - -### Services Exposed -1. **`DeepLTranslationService`**: The primary translation engine pointing at `api-free.deepl.com`. -2. **`OpenAITranslationFallback`**: A secondary translation engine pivoting to `gpt-4o-mini` capable of interpreting unsupported dialects or surviving a DeepL service outage. - -These are injected globally by the `TranslationWorker` daemon in the `app/services` directory. - ---- - -## Public API (`service.py`) - -Both Translation services export an identical `translate()` asynchronous signature allowing polymorphing swapping on error conditions. - -#### `translate(text, source_language, target_language)` -* **Args:** - * `text` *(str)*: The text buffer requiring translation. - * `source_language` *(str)*: ISO 639-1 Hint language tag (e.g., `"fr"`). - * `target_language` *(str)*: Target localized ISO 639-1 tag constraint. -* **Returns:** - Returns a unified `dict` payload structure standard against multiple engines: - ```python - { - "translated_text": "Bonjour le monde", - "latency_ms": 115.5 - } - ``` -* **Exception Behavior:** Both primary engines explicitly raise `httpx.HTTPStatusError` aggressively so the `TranslationWorker` pipeline code can manage failures explicitly or execute immediate fallbacks. - ---- - -## Fallback Mechanisms - -DeepL is phenomenally fast, but supports a relatively narrow funnel of active language mappings. - -Inside the logic, before spinning up an HTTP context, the DeepL mapping is checked via `supports_language()`. If this yields `False`, or if a 500 API exception cascades back from DeepL, the system instantly catches the logic and bounces the payload securely to `OpenAITranslationFallback`. - -The fallback prompts OpenAI using a zero-shot strictly confined chat string: `"You are a professional translator. Translate the following text from {source} to {target}. Return ONLY the translated text, nothing else."` - ---- - -## Language Code Mapping - -DeepL requires esoteric capitalization modifications (e.g. `EN-US` instead of `en`, `PT-BR` instead of `pt`) which breaks pipeline standards. - -The service defines a private internal mapping table `_DEEPL_LANG_MAP` that captures the front-end user `en`, `de`, `fr` lowercase configurations and dynamically adapts them to DeepL formatting on ingress, reverting answers gracefully back natively before the function returns them up into Kafka topics. diff --git a/app/external_services/openai_tts/api-docs.md b/app/external_services/openai_tts/api-docs.md deleted file mode 100644 index a976f6c..0000000 --- a/app/external_services/openai_tts/api-docs.md +++ /dev/null @@ -1,73 +0,0 @@ -# FluentMeet OpenAI TTS Documentation - -> **Package Location:** `/app/external_services/openai_tts` -> **Purpose:** Handles external asynchronous integrations with the OpenAI Text-to-Speech API. - ---- - -## Table of Contents - -- [Overview](#overview) -- [Architecture](#architecture) -- [Public API (`service.py`)](#public-api-servicepy) -- [Audio Formats](#audio-formats) -- [Configuration](#configuration) - ---- - -## Overview - -The `app/external_services/openai_tts` package acts as the active backend for stage 4 of the real-time audio pipeline. It intercepts translated text streams and synthesizes them into dynamic real-time human voices using OpenAI's `tts-1` model via the `/v1/audio/speech` endpoints. - ---- - -## Architecture - -To minimize dependencies and footprint, avoiding heavy pip installments, the `OpenAITTSService` entirely abstracts OpenAI SDK endpoints via raw `httpx.AsyncClient` objects natively. - -It is designed as a pure stateless singleton and gets injected dynamically into the `TTSWorker` Daemon inside `app/services` based on the `.env` file configuration setting dictating whether Voice.ai or OpenAI drives speech synthesis. - ---- - -## Public API (`service.py`) - -### `OpenAITTSService` - -The fully asynchronous service layer encapsulating synthesis logic. - -#### `synthesize(text, voice, encoding)` -Executes the API request to retrieve the generated Audio chunk. -* **Args:** - * `text` *(str)*: Target string text block to convert to voice. - * `voice` *(str, optional)*: OpenAI voice profile (`alloy`, `echo`, `fable`, `onyx`, `nova`, `shimmer`). Overrides environmental mapping defaults. - * `encoding` *(str)*: Required chunk encoding mapping (`"linear16"` or `"opus"`). -* **Returns:** - Returns a strict `dict` containing the binary footprint needed to transmit over Kafka natively. - ```python - { - "audio_bytes": b"\\x01\\x00\\xFF...", - "sample_rate": 24000, - "latency_ms": 284.1 - } - ``` -* **Exception Behavior:** Immediately raises `httpx.HTTPStatusError` on non-200 responses to enforce the system-wide Dead Letter Queue routing schema via the `Exceptions` trapping mechanism. - ---- - -## Audio Formats - -Native AI API endpoints refer to raw data by highly specific designations natively (e.g. `pcm` instead of `linear16`). The underlying module natively provides the dictionary `_FORMAT_MAP` routing internal definitions like `"linear16"` directly to `"pcm"` in the OpenAPI REST schemas. - -*Note: OpenAI inherently resolves standard `pcm` packets natively to a 24kHZ mono output footprint, distinct from STT endpoints expecting 16kHz standard configurations.* - ---- - -## Configuration - -### `get_openai_tts_headers()` (`config.py`) - -Generates strict formatting API headers. - -* Builds the JSON dict mapping: `Authorization: Bearer ` natively. -* Enforces `Content-Type: application/json`. -* Acts as an architecture boundary: automatically throws a `RuntimeError` failure natively on instantiation if the application failed to boot with `OPENAI_API_KEY` defined. diff --git a/app/external_services/voiceai/api-docs.md b/app/external_services/voiceai/api-docs.md deleted file mode 100644 index d23a9b9..0000000 --- a/app/external_services/voiceai/api-docs.md +++ /dev/null @@ -1,74 +0,0 @@ -# FluentMeet Voice.ai Integration Documentation - -> **Package Location:** `/app/external_services/voiceai` -> **Purpose:** Handles external asynchronous integrations with the Voice.ai Text-to-Speech Generation API. - ---- - -## Table of Contents - -- [Overview](#overview) -- [Architecture](#architecture) -- [Public API (`service.py`)](#public-api-servicepy) -- [Format & Model Targeting](#format--model-targeting) -- [Configuration](#configuration) - ---- - -## Overview - -The `app/external_services/voiceai` package acts as the active backend for stage 4 of the real-time audio pipeline. It intercepts translated text streams and synthesizes them into dynamic real-time human voices using the Voice.ai `/api/v1/tts/speech` endpoints. Note that this package runs dynamically as an alternative to OpenAI depending on standard environment configurations (`ACTIVE_TTS_PROVIDER="voiceai"`). - ---- - -## Architecture - -This service acts identically to the OpenAI SDK. To maintain tight coupling with core architectures, ignoring bulk Python packages, it resolves all remote calls using `httpx.AsyncClient` blocks statelessly. - -The configuration relies on environment variables, pulling `VOICEAI_TTS_MODEL` and configuring payload definitions instantly per-request. - ---- - -## Public API (`service.py`) - -### `VoiceAITTSService` - -The fully asynchronous service layer encapsulated via Singleton pattern mapping logic to `/tts/speech`. - -#### `synthesize(text, language, voice_id, encoding)` -Initiates asynchronous remote calls to stream speech endpoints. -* **Args:** - * `text` *(str)*: Target string text block mapped to conversion. - * `language` *(str)*: Native mapping used specifically by Voice.ai context engines (e.g., swapping to multilingual vs english default models automatically). - * `voice_id` *(str, optional)*: An explicit ID tag generated via Voice.ai console for custom cloned models. Defaults to default models if None. - * `encoding` *(str)*: Encoding request (`"linear16"` or `"opus"`). -* **Returns:** - Returns a unified `dict` format identical to OpenAI payload structures, guaranteeing seamless swapping inside caller DAEMONS without syntax rewrites. - ```python - { - "audio_bytes": b"\\x01\\x00\\xFF...", - "sample_rate": 16000, - "latency_ms": 352.1 - } - ``` -* **Exception Behavior:** Immediately traps non-200 configurations routing `httpx.HTTPStatusError` directly to Kafka Retry protocols. - ---- - -## Format & Model Targeting - -Voice.ai resolves API properties inherently different from standard TTS parameters: - -* **Format Resolutions (`_FORMAT_MAP`):** Internal definitions `"linear16"` correctly route towards `"pcm_16000"` parameter arrays. Internal definitions `"opus"` target `"opus_48000_64"`. This directly influences returned `sample_rate` logic dynamically (switching from 16kHz to 48kHz automatically). -* **Model Adjustments:** Voice.ai tracks multiple models explicitly. If `VOICEAI_TTS_MODEL` is set to `"multilingual-something"`, but the detected/passed `language` is purely `"en"`, the `_synthesize` module inherently edits the parameter dictionary replacing `.replace("multilingual-", "")` resolving natively to a faster specialized english model automatically. - ---- - -## Configuration - -### `get_voiceai_headers()` (`config.py`) - -Generates strict formatting API headers. - -* Builds the JSON dict mapping: `Authorization: Bearer ` natively. -* Acts as an architecture boundary triggering explicit `RuntimeError` failure on initialization if `VOICE_AI_API_KEY` isn't accessible in server scope. diff --git a/app/kafka/api-docs.md b/app/kafka/api-docs.md deleted file mode 100644 index 6d5919f..0000000 --- a/app/kafka/api-docs.md +++ /dev/null @@ -1,122 +0,0 @@ -# FluentMeet Kafka Architecture Documentation - -> **Package Location:** `/app/kafka` -> **Purpose:** Event-driven architecture infrastructure, abstracting AIOKafka underlying intricacies. - ---- - -## Table of Contents - -- [Overview](#overview) -- [Architecture & Lifecycle](#architecture--lifecycle) -- [Topic Registry](#topic-registry) -- [Producers & Consumers](#producers--consumers) - - [KafkaProducer (`producer.py`)](#kafkaproducer-producerpy) - - [BaseConsumer (`consumer.py`)](#baseconsumer-consumerpy) -- [Dead Letter Queues (DLQ) & Retries](#dead-letter-queues-dlq--retries) -- [Event Schemas (`schemas.py`)](#event-schemas-schemaspy) -- [Error Handling (`exceptions.py`)](#error-handling-exceptionspy) - ---- - -## Overview - -The `app/kafka` package provides a high-level, strongly-typed asynchronous wrapper over `aiokafka`. -It entirely hides the serialization mechanisms from feature-level developers and implements hardened stability patterns out-of-the-box including Singleton Lifecycle Management, Automatic Topic Provisioning, Manual Offset Commits, Linear Retry Backoffs, and automatic Dead Letter Queue (DLQ) routing. - ---- - -## Architecture & Lifecycle - -The package revolves around the `KafkaManager` (`manager.py`). -This is a strictly controlled Singleton bound to the FastAPI application lifespan (typically started inside `app/main.py @asynccontextmanager`). - -**Lifecycle sequence:** -1. **Instantiate:** `get_kafka_manager()` creates the `KafkaProducer` and registers instances of `BaseConsumer` (e.g., `EmailConsumerWorker`, `STTWorker`). -2. **Start (`manager.start()`):** - - Automatically provisions missing Kafka topics defined in `topics.py` using `AIOKafkaAdminClient`. - - Starts the global `KafkaProducer`. - - Starts background `asyncio.Task` loops for each registered `BaseConsumer`. -3. **Run:** The application accepts requests, firing items into the Producer, and Consumers eagerly rip items from the broker. -4. **Shutdown (`manager.stop()`):** - - Gently cancels and awaits all consumer `asyncio.Task` loops. - - Cleans up and stops the producer. - ---- - -## Topic Registry - -Defined in `topics.py`. All standard strings are prefixed or namespaced by domain. The manager auto-creates these alongside their mirror `dlq.` prefixes. - -| Topic Constant | String | Purpose | -|---|---|---| -| `NOTIFICATIONS_EMAIL` | `notifications.email` | Dispatch queue for Jinja2 rendered SMTP emails via Mailgun. | -| `AUDIO_RAW` | `audio.raw` | Stage 1 WebSocket base64 binary PCM streams. | -| `TEXT_ORIGINAL` | `text.original` | Stage 2 original STT transcription strings. | -| `TEXT_TRANSLATED` | `text.translated` | Stage 3 Multi-casted translation blocks. | -| `AUDIO_SYNTHESIZED` | `audio.synthesized` | Stage 4 TTS returning audio binary blocks for egress. | - -*(Media upload topics like `media.upload` are registered but currently inactive awaiting feature expansions).* - ---- - -## Producers & Consumers - -### KafkaProducer (`producer.py`) - -A clean abstraction over `AIOKafkaProducer`. -- **Serialization:** Forces all payloads through `json.dumps` natively. Requires developers to pass Pydantic `BaseEvent` models. -- **Methods:** `send(topic, event, key)` and a `.ping()` health-check tool to verify broker connectivity via forcing metadata refreshes. - -### BaseConsumer (`consumer.py`) - -An `abc.ABC` parent class that all worker daemons (like `STTWorker`) must inherit from. -Subclasses implement a single asynchronous method: `async def handle(self, event: BaseEvent) -> None`. - -**Built-In Resiliency Features:** -1. **Manual Commits:** By default, disables `auto_commit`. An offset block is *only* marked as processed on the broker if the `.handle()` function exits flawlessly. A pod crash mid-process guarantees message re-delivery. -2. **Typed Context:** Automatically intercepts incoming `bytes`, unpacks the JSON, and leverages the subclass's declared `event_schema` to build and validate a Pydantic object before passing it inside `.handle()`. - ---- - -## Dead Letter Queues (DLQ) & Retries - -If `.handle()` throws an Exception, the BaseConsumer automatically traps it and triggers the **Retry Matrix**. - -1. **Linear Backoff:** Uses `settings.KAFKA_MAX_RETRIES` (default 3) and `settings.KAFKA_RETRY_BACKOFF_MS`. A failed event sleeps its asynchronous task scaling linearly (e.g., attempt 1 sleeps 1s, attempt 2 sleeps 2s). -2. **DLQ Routing:** If the max retries are exhausted, the event is permanently considered unrecoverable (poison pill). -3. Instead of stalling the Kafka partition, the Consumer packages the original failed payload + integer retry counters + text exception names into a rigid **`DLQEvent`** schema. -4. It commands the *Producer* to fling this DLQ object into `dlq.{original_topic}` (e.g., `dlq.notifications.email`). -5. The offset is *then* committed, allowing the partition to move forward. - ---- - -## Event Schemas (`schemas.py`) - -All objects traversing the Kafka broker must inherit from `BaseEvent[T]`. - -**`BaseEvent` Wrapper:** -Every payload gets an automatic unique UUID `event_id` and an ISO UTC `timestamp`. This is crucial for tracking events across distributed tracing platforms. - -**`DLQEvent`:** -```json -{ - "original_event_id": "uuid", - "original_topic": "notifications.email", - "original_event": { ... payload blob ... }, - "error_message": "TransientEmailDeliveryError: Mailgun 500", - "failed_at": "datetime", - "retry_count": 3 -} -``` - -*(Note: The high-speed pipeline payloads are located centrally in `/app/schemas/pipeline.py` rather than here, separating abstract infrastructure schemas from heavy feature logic).* - ---- - -## Error Handling (`exceptions.py`) - -Extends the core `FluentMeetException` allowing HTTP frameworks or health checks to parse standard Error Codes. -- `KafkaConnectionError` -- `KafkaPublishError` -- `KafkaConsumeError` diff --git a/app/models/api-docs.md b/app/models/api-docs.md deleted file mode 100644 index a340ab7..0000000 --- a/app/models/api-docs.md +++ /dev/null @@ -1,84 +0,0 @@ -# FluentMeet Models Documentation - -> **Package Location:** `/app/models` -> **Purpose:** Centralized SQLAlchemy Declarative Base and Alembic Schema Aggregation. - ---- - -## Table of Contents - -- [Overview](#overview) -- [Architecture](#architecture) -- [Components](#components) - - [The Declarative Base (`base.py`)](#the-declarative-base-basepy) - - [Model Aggregation (`__init__.py`)](#model-aggregation-__init__py) - ---- - -## Overview - -The `app/models` package serves as the foundational data layer configuration for the FluentMeet application. Unlike older monolithic architectures that place all SQLAlchemy models in a single `models.py` file, FluentMeet uses a domain-driven structure where models live inside their respective feature modules (e.g., `app/modules/meeting/models.py`). - -To satisfy SQLAlchemy and Alembic strict requirements for database schema generation, this package acts as the central initialization and aggregation point for the ORM. - ---- - -## Architecture - -This package solves the classic ORM bootstrap problem by centralizing the `Base` class, which every module imports, and then importing those completed models back into an `__init__.py` so Alembic's `env.py` has a single, complete metadata registry object to inspect during migrations. - -``` -┌─────────────────────────┐ ┌─────────────────────────┐ -│ app/modules/auth/ │ │ app/modules/meeting/ │ -│ models.py │ │ models.py │ -│ (User, Tokens, etc.) │ │ (Room, Participant...) │ -└────────────┬────────────┘ └────────────┬────────────┘ - │ │ - ▼ ▼ -┌─────────────────────────────────────────────────────────────┐ -│ app/models/__init__.py │ -│ │ -│ from app.models.base import Base │ -│ from app.modules.meeting.models import Room, ... │ -│ │ -│ __all__ = ["Base", "Room", ...] │ -└────────────────────────────┬────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ alembic/env.py │ -│ │ -│ from app.models import Base │ -│ target_metadata = Base.metadata │ -└─────────────────────────────────────────────────────────────┘ -``` - ---- - -## Components - -### The Declarative Base (`base.py`) - -This file contains the absolute minimum required to establish the SQLAlchemy 2.0 ORM base using `DeclarativeBase`. - -```python -from sqlalchemy.orm import DeclarativeBase - -class Base(DeclarativeBase): - pass -``` - -**Why it's isolated:** -Decoupling the `Base` metadata from the actual Model files prevents Circular Import errors between the central registry and the feature modules that need to inherit from it. - -### Model Aggregation (`__init__.py`) - -This file aggregates the distributed ORM models, making them easily accessible. - -It currently exports: -- `Base` (The core declarative metadata class) -- `Room` (from `app.modules.meeting.models`) -- `Participant` (from `app.modules.meeting.models`) -- `MeetingInvitation` (from `app.modules.meeting.models`) - -*(Note: It is recommended that as new modules are created—such as the `auth` module—their respective ORM entities e.g., `User`, are also imported into this initialization file to ensure complete coverage by the Alembic auto-migration tool.)* diff --git a/app/modules/auth/api-docs.md b/app/modules/auth/api-docs.md deleted file mode 100644 index eec5a29..0000000 --- a/app/modules/auth/api-docs.md +++ /dev/null @@ -1,1053 +0,0 @@ -# FluentMeet Authentication API Documentation - -> **Base URL:** `/api/v1/auth` -> **Version:** 1.0 · **Protocol:** REST over HTTPS · **Content-Type:** `application/json` - ---- - -## Table of Contents - -- [Overview](#overview) -- [Architecture](#architecture) -- [Authentication Flow](#authentication-flow) -- [Security Mechanisms](#security-mechanisms) -- [Endpoints](#endpoints) - - [POST /signup](#post-signup) - - [POST /login](#post-login) - - [GET /verify-email](#get-verify-email) - - [POST /resend-verification](#post-resend-verification) - - [POST /forgot-password](#post-forgot-password) - - [POST /reset-password](#post-reset-password) - - [POST /change-password](#post-change-password) - - [POST /logout](#post-logout) - - [POST /refresh-token](#post-refresh-token) - - [GET /google/login](#get-googlelogin) - - [GET /google/callback](#get-googlecallback) -- [Data Models](#data-models) -- [Request / Response Schemas](#request--response-schemas) -- [Error Codes Reference](#error-codes-reference) -- [Configuration Reference](#configuration-reference) -- [Internal Services](#internal-services) - ---- - -## Overview - -The FluentMeet authentication module provides a complete identity and access management system built on **FastAPI**. It supports: - -- **Email/password registration** with mandatory email verification. -- **Google OAuth 2.0** social login with automatic account linking. -- **JWT-based session management** using short-lived access tokens and long-lived refresh tokens. -- **Refresh Token Rotation** with automatic reuse detection and full session invalidation. -- **Account lockout** after repeated failed login attempts. -- **Password recovery** via secure one-time email tokens. -- **Rate limiting** on all sensitive endpoints via SlowAPI. - -All tokens are signed with **HS256** and a server-side secret key. Refresh tokens are delivered exclusively via **HttpOnly, Secure, SameSite=Strict** cookies and are never exposed in response bodies. - ---- - -## Architecture - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ FastAPI Router │ -│ (app/modules/auth/router.py) │ -├──────────┬──────────┬──────────────┬───────────┬────────────────┤ -│ │ │ │ │ │ -│ AuthService AuthVerification GoogleOAuth AccountLockout │ -│ (service.py) Service Service Service │ -│ │ (verification.py) (oauth_google.py) (account_ │ -│ │ │ │ │ lockout.py) │ -│ ▼ ▼ │ ▼ │ -│ ┌──────────┐ ┌──────────┐ │ ┌────────────┐ │ -│ │ Security │ │ Email │ │ │ Redis │ │ -│ │ Service │ │ Producer │ │ │ (lockout + │ │ -│ │(core/ │ │ Service │ │ │ tokens) │ │ -│ │security) │ │ │ │ └────────────┘ │ -│ └──────────┘ └──────────┘ │ │ -│ │ │ │ -│ ▼ │ │ -│ ┌──────────┐ ┌──────────┐ ┌────────────┐ │ -│ │PostgreSQL│ │ Google │ │ TokenStore │ │ -│ │ (Users, │ │ OAuth2 │ │ Service │ │ -│ │ Tokens) │ │ Provider │ │(token_ │ │ -│ └──────────┘ └──────────┘ │ store.py) │ │ -│ └────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### Module Files - -| File | Purpose | -|----------------------|-----------------------------------------------------------------------------------| -| `router.py` | FastAPI route definitions and HTTP-layer logic | -| `service.py` | Core business logic — signup, login, password reset/change, OAuth user resolution | -| `schemas.py` | Pydantic request/response models and validators | -| `models.py` | SQLAlchemy ORM models (`User`, `VerificationToken`, `PasswordResetToken`) | -| `dependencies.py` | FastAPI dependency injection factories | -| `verification.py` | Email verification token lifecycle | -| `token_store.py` | Redis-backed refresh token storage and access token blacklisting | -| `account_lockout.py` | Redis-backed brute-force protection | -| `oauth_google.py` | Google OAuth 2.0 authorization code flow | -| `constants.py` | Enums — `UserRole`, `SupportedLanguage` | - ---- - -## Authentication Flow - -### Email/Password Registration Flow - -``` -Client Server Email Service - │ │ │ - │ POST /signup │ │ - │ {email, password, ...} ──► │ │ - │ │── Create user (unverified) │ - │ │── Generate verification token │ - │ │── Enqueue verification email ──► - │ ◄── 201 {user_id, ...} │ │ - │ │ │ - │ User clicks email link │ │ - │ GET /verify-email?token=...──►│ │ - │ │── Validate token │ - │ │── Set is_verified = True │ - │ ◄── 200 {message} │ │ - │ │ │ - │ POST /login │ │ - │ {email, password} ──► │ │ - │ │── Verify credentials │ - │ │── Check lockout / verified │ - │ │── Issue AT + RT │ - │ ◄── 200 {access_token} │ │ - │ ◄── Set-Cookie: refresh_token│ │ -``` - -### Token Refresh Flow (Rotation) - -``` -Client Server Redis - │ │ │ - │ POST /refresh-token │ │ - │ Cookie: refresh_token=...──► │ │ - │ │── Decode RT JWT │ - │ │── Check JTI valid? ─────► │ - │ │ ◄── Yes │ - │ │── Revoke old JTI ───────► │ - │ │── Save new JTI ─────────► │ - │ ◄── 200 {new_access_token} │ │ - │ ◄── Set-Cookie: new_rt │ │ - │ │ │ - │ ⚠️ Reuse of OLD RT │ │ - │ POST /refresh-token │ │ - │ Cookie: old_rt ──► │ │ - │ │── Check JTI valid? ─────► │ - │ │ ◄── No (revoked!) │ - │ │── REVOKE ALL sessions ──► │ - │ ◄── 401 REFRESH_TOKEN_REUSE │ │ -``` - -### Google OAuth 2.0 Flow - -``` -Client Server Google - │ │ │ - │ GET /google/login ──► │ │ - │ │── Generate state token │ - │ │── Store in Redis (10min) │ - │ ◄── 302 → Google consent │ │ - │ │ │ - │ (user authenticates with Google) │ - │ │ │ - │ GET /google/callback │ │ - │ ?code=...&state=... ──► │ │ - │ │── Verify state from Redis│ - │ │── Exchange code ─────────►│ - │ │ ◄── access_token │ - │ │── Get user info ─────────►│ - │ │ ◄── {email, name, ...} │ - │ │── Find or create user │ - │ │── Issue AT + RT │ - │ ◄── 302 → frontend#access_token=... │ - │ ◄── Set-Cookie: refresh_token│ │ -``` - ---- - -## Security Mechanisms - -### JWT Token Strategy - -| Token | Delivery | Lifetime | Claims | Storage | -|-------------------|-----------------|-----------------------|------------------------------------------------|------------------------------------| -| **Access Token** | Response body | 60 min (configurable) | `sub` (email), `jti`, `exp`, `type: "access"` | Client-side (memory/localStorage) | -| **Refresh Token** | HttpOnly cookie | 7 days (configurable) | `sub` (email), `jti`, `exp`, `type: "refresh"` | Redis (server-side JTI validation) | - -- **Algorithm:** HS256 -- **Library:** python-jose -- **Signing Key:** `SECRET_KEY` from environment - -### Refresh Token Rotation - -Every token refresh issues a **new** refresh token and revokes the old one. If a revoked token is reused (indicating possible theft), **all sessions for that user are immediately invalidated**. - -### Access Token Blacklisting - -On logout, the access token's JTI is added to a Redis blacklist with a TTL matching the token's remaining lifetime. The `get_current_user` dependency checks this blacklist on every authenticated request. - -### Account Lockout Policy - -| Parameter | Default | Description | -|-----------------------------|---------|-------------------------------------| -| `MAX_FAILED_LOGIN_ATTEMPTS` | 5 | Consecutive failures before lockout | -| `ACCOUNT_LOCKOUT_DAYS` | 5 | Duration of the lockout period | - -**Redis Keys:** -- `login_attempts:{email}` — integer counter, no TTL (cleared on success) -- `account_locked:{email}` — flag (`"1"`), TTL = lockout period - -A successful login resets the failure counter. - -### Password Hashing - -- **Primary:** passlib with bcrypt scheme -- **Fallback:** raw bcrypt (for compatibility with newer bcrypt builds) -- Auto-deprecated hash schemes are upgraded on verification. - -### Rate Limiting - -All sensitive endpoints are rate-limited using **SlowAPI** (based on client IP): - -| Endpoint | Limit | -|-----------------------------|-----------| -| `POST /login` | 10/minute | -| `POST /resend-verification` | 3/minute | -| `POST /forgot-password` | 5/minute | -| `POST /reset-password` | 5/minute | -| `POST /change-password` | 10/minute | -| `POST /logout` | 20/minute | -| `POST /refresh-token` | 30/minute | - -### Cookie Security - -All refresh token cookies are set with: - -``` -HttpOnly: true (no JavaScript access) -Secure: true (HTTPS only) -SameSite: strict (no cross-site requests) -Path: /api/v1/auth -Max-Age: -``` - ---- - -## Endpoints - ---- - -### POST /signup - -Register a new user account. A verification email is sent asynchronously. - -**Request Body:** - -```json -{ - "email": "user@example.com", - "password": "securePass123", - "confirm_password": "securePass123", - "full_name": "Jane Doe", - "speaking_language": "en", - "listening_language": "fr", - "accepted_terms": true -} -``` - -| Field | Type | Required | Constraints | -|----------------------|------------------|----------|-----------------------------------------------------------------------| -| `email` | `string (email)` | ✅ | Valid email, auto-lowercased | -| `password` | `string` | ✅ | Minimum 8 characters | -| `confirm_password` | `string` | ✅ | Must match password exactly | -| `accepted_terms` | `boolean` | ✅ | Must be `true` — user must accept Terms of Service and Privacy Policy | -| `full_name` | `string \| null` | ❌ | Max 255 chars, auto-trimmed | -| `speaking_language` | `enum` | ❌ | Default: `"en"`. Values: `en`, `fr`, `de`, `es`, `it`, `pt` | -| `listening_language` | `enum` | ❌ | Default: `"en"`. Values: `en`, `fr`, `de`, `es`, `it`, `pt` | - -**Response: `201 Created`** - -```json -{ - "id": "550e8400-e29b-41d4-a716-446655440000", - "email": "user@example.com", - "full_name": "Jane Doe", - "speaking_language": "en", - "listening_language": "fr", - "user_role": "user", - "is_active": true, - "is_verified": false, - "created_at": "2026-04-10T12:00:00Z" -} -``` - -**Error Responses:** - -| Status | Code | Condition | -|--------|----------------------------|-------------------------------------------------------------| -| `409` | `EMAIL_ALREADY_REGISTERED` | An account with this email already exists | -| `422` | — | Validation error (missing fields, passwords mismatch, etc.) | - -**Side Effects:** -- Creates an unverified `User` record in PostgreSQL. -- Generates a `VerificationToken` (UUID, 24h expiry). -- Enqueues a verification email via Kafka (non-blocking; signup succeeds even if email dispatch fails). - ---- - -### POST /login - -Authenticate a registered user with email and password. - -**Request Body:** - -```json -{ - "email": "user@example.com", - "password": "securePass123" -} -``` - -| Field | Type | Required | -|------------|------------------|----------| -| `email` | `string (email)` | ✅ | -| `password` | `string` | ✅ | - -**Response: `200 OK`** - -```json -{ - "access_token": "eyJhbGciOiJIUzI1NiIs...", - "user_id": "550e8400-e29b-41d4-a716-446655440000", - "token_type": "bearer", - "expires_in": 3600 -} -``` - -**Response Headers:** - -``` -Set-Cookie: refresh_token=eyJ...; HttpOnly; Secure; SameSite=Strict; Path=/api/v1/auth; Max-Age=604800 -``` - -**Error Responses:** - -| Status | Code | Condition | -|--------|-----------------------|----------------------------------------------------------------------------------------| -| `400` | `MISSING_CREDENTIALS` | Empty request body | -| `401` | `INVALID_CREDENTIALS` | Wrong email or password *(Returns `details: [{"attempts_remaining": N}]`)* | -| `403` | `EMAIL_NOT_VERIFIED` | Account exists but email is not verified | -| `403` | `ACCOUNT_DELETED` | Account has been soft-deleted | -| `403` | `ACCOUNT_LOCKED` | Too many failed login attempts *(Returns `details: [{"lock_time_left": "duration"}]`)* | - -**Example Response (Invalid Credentials):** - -```json -{ - "status": "error", - "code": "INVALID_CREDENTIALS", - "message": "Invalid email or password.", - "details": [ - { - "attempts_remaining": 4 - } - ] -} -``` - -**Example Response (Account Locked):** - -```json -{ - "status": "error", - "code": "ACCOUNT_LOCKED", - "message": "Account is temporarily locked due to too many failed login attempts.", - "details": [ - { - "lock_time_left": "4 days, 23 hours and 29 minutes" - } - ] -} -``` - -**Rate Limit:** 10 requests/minute per IP. - -**Security Behavior:** -- Failed attempts increment the lockout counter (even for non-existent emails, to prevent timing attacks). -- After 5 consecutive failures → account locked for 5 days. -- Successful login resets the failure counter. - ---- - -### GET /verify-email - -Verify a user's email address using a token from the verification email. - -**Query Parameters:** - -| Parameter | Type | Required | -|-----------|-----------------|----------| -| `token` | `string (UUID)` | ✅ | - -**Example:** `GET /api/v1/auth/verify-email?token=550e8400-e29b-41d4-a716-446655440000` - -**Response: `200 OK`** - -```json -{ - "status": "ok", - "message": "Email successfully verified. You can now log in." -} -``` - -**Error Responses:** - -| Status | Code | Condition | -|--------|-----------------|---------------------------------------------| -| `400` | `MISSING_TOKEN` | No `token` query parameter provided | -| `400` | `INVALID_TOKEN` | Token is not a valid UUID or does not exist | -| `400` | `TOKEN_EXPIRED` | Token has expired (default: 24 hours) | - -**Side Effects:** -- Sets `user.is_verified = True`. -- Deletes the consumed `VerificationToken`. - ---- - -### POST /resend-verification - -Request a new verification email. Always returns a generic success message to prevent **user enumeration**. - -**Request Body:** - -```json -{ - "email": "user@example.com" -} -``` - -**Response: `200 OK`** - -```json -{ - "status": "ok", - "message": "If an account with that email exists, we have sent a verification email." -} -``` - -**Rate Limit:** 3 requests/minute per IP. - -**Behavior:** -- If the user does not exist or is already verified, the endpoint silently returns success. -- Existing unexpired verification tokens for the user are deleted before issuing a new one. - ---- - -### POST /forgot-password - -Request a password reset email. Always returns a generic success message to prevent **user enumeration**. - -**Request Body:** - -```json -{ - "email": "user@example.com" -} -``` - -**Response: `200 OK`** - -```json -{ - "status": "ok", - "message": "If an account with this email exists, a password reset link has been sent." -} -``` - -**Rate Limit:** 5 requests/minute per IP. - -**Behavior:** -- Silently returns success if user does not exist, is inactive, deleted, or unverified. -- Deletes any existing `PasswordResetToken` records for the user before creating a new one. -- Token expiry: configurable via `PASSWORD_RESET_TOKEN_EXPIRE_MINUTES` (default: 60 min). -- Sends a `password_reset` email template via Kafka (non-blocking). - ---- - -### POST /reset-password - -Reset the user's password using a one-time token received via email. - -**Request Body:** - -```json -{ - "token": "550e8400-e29b-41d4-a716-446655440000", - "new_password": "newSecurePass456" -} -``` - -| Field | Type | Required | Constraints | -|----------------|----------|----------|----------------------| -| `token` | `string` | ✅ | Non-empty | -| `new_password` | `string` | ✅ | Minimum 8 characters | - -**Response: `200 OK`** - -```json -{ - "status": "ok", - "message": "Password has been reset successfully. Please log in with your new password." -} -``` - -**Error Responses:** - -| Status | Code | Condition | -|--------|-----------------------|---------------------------------------------| -| `400` | `INVALID_RESET_TOKEN` | Token does not exist or user not found | -| `400` | `RESET_TOKEN_EXPIRED` | Token has expired | -| `400` | `SAME_PASSWORD` | New password is the same as the current one | - -**Rate Limit:** 5 requests/minute per IP. - -**Side Effects:** -- Updates `user.hashed_password` and `user.updated_at`. -- Deletes the consumed `PasswordResetToken`. -- Revokes **all** active refresh tokens for the user (forces re-login on all devices). -- Sends a `password_changed` security notification email. - ---- - -### POST /change-password - -Change the password for the currently authenticated user. - -**🔒 Requires Authentication:** `Authorization: Bearer ` - -**Request Body:** - -```json -{ - "current_password": "oldPass123", - "new_password": "newSecurePass456" -} -``` - -| Field | Type | Required | Constraints | -|--------------------|----------|----------|----------------------| -| `current_password` | `string` | ✅ | — | -| `new_password` | `string` | ✅ | Minimum 8 characters | - -**Response: `200 OK`** - -```json -{ - "status": "ok", - "message": "Password updated successfully." -} -``` - -**Error Responses:** - -| Status | Code | Condition | -|--------|-----------------------------------------|---------------------------------| -| `400` | `INCORRECT_PASSWORD` | Current password does not match | -| `400` | `SAME_PASSWORD` | New password is same as current | -| `401` | `MISSING_TOKEN` / `INVALID_CREDENTIALS` | Not authenticated | - -**Rate Limit:** 10 requests/minute per IP. - -**Side Effects:** -- Updates `user.hashed_password` and `user.updated_at`. -- Revokes **all** active refresh tokens (forces re-login on all devices). -- Sends a `password_changed` security notification email. - ---- - -### POST /logout - -Log out the current session by invalidating both the access and refresh tokens. - -**🔒 Requires Authentication:** `Authorization: Bearer ` - -**Request:** No body required. Refresh token is read from the `refresh_token` cookie. - -**Response: `200 OK`** - -```json -{ - "status": "ok", - "message": "Successfully logged out." -} -``` - -**Response Headers:** - -``` -Set-Cookie: refresh_token=; Path=/api/v1/auth; Max-Age=0 (cookie cleared) -``` - -**Error Responses:** - -| Status | Code | Condition | -|--------|-----------------------------------------|-------------------| -| `401` | `MISSING_TOKEN` / `INVALID_CREDENTIALS` | Not authenticated | - -**Rate Limit:** 20 requests/minute per IP. - -**Behavior:** -- Blacklists the access token JTI in Redis for its remaining TTL. -- Revokes the refresh token JTI (if the cookie is present). -- Clears the `refresh_token` HttpOnly cookie. - ---- - -### POST /refresh-token - -Rotate the refresh token and issue a new access token. Implements the **Refresh Token Rotation** pattern. - -**Request:** No body required. The refresh token is read from the `refresh_token` HttpOnly cookie. - -**Response: `200 OK`** - -```json -{ - "access_token": "eyJhbGciOiJIUzI1NiIs...", - "token_type": "bearer", - "expires_in": 3600 -} -``` - -**Response Headers:** - -``` -Set-Cookie: refresh_token=; HttpOnly; Secure; SameSite=Strict; Path=/api/v1/auth; Max-Age=604800 -``` - -**Error Responses:** - -| Status | Code | Condition | -|--------|-------------------------|---------------------------------------------------------| -| `401` | `MISSING_REFRESH_TOKEN` | No `refresh_token` cookie present | -| `401` | `INVALID_REFRESH_TOKEN` | Token is expired, malformed, or not a refresh token | -| `401` | `REFRESH_TOKEN_REUSE` | Revoked token was reused — **all** sessions invalidated | -| `403` | `ACCOUNT_DEACTIVATED` | User account has been deactivated or deleted | - -**Rate Limit:** 30 requests/minute per IP. - -**Security Behavior:** -- The old refresh token JTI is revoked before the new one is saved. -- If a previously revoked JTI is used again (reuse attack), **all** refresh tokens for the user are purged from Redis and a warning is logged. - ---- - -### GET /google/login - -Initiate the Google OAuth 2.0 authorization flow by redirecting the user to Google's consent screen. - -**Response: `302 Found`** - -Redirects to Google's OAuth consent URL with: -- `client_id`, `redirect_uri`, `scope: "openid email profile"` -- A cryptographically random `state` parameter stored in Redis for 10 minutes. - ---- - -### GET /google/callback - -Handle the callback from Google after user authentication. This endpoint is called by Google, not by the client directly. - -**Query Parameters:** - -| Parameter | Type | Required | -|-----------|----------|----------| -| `code` | `string` | ✅ | -| `state` | `string` | ✅ | - -**Response: `302 Found`** - -Redirects to: `{FRONTEND_BASE_URL}#access_token=` - -**Response Headers:** - -``` -Set-Cookie: refresh_token=; HttpOnly; Secure; SameSite=Strict; Path=/api/v1/auth; Max-Age=604800 -``` - -**Error Responses:** - -| Status | Code | Condition | -|--------|-------------------------|------------------------------------------| -| `400` | `INVALID_OAUTH_STATE` | State token is invalid or expired | -| `400` | `INVALID_OAUTH_PROFILE` | Google account does not provide an email | -| `403` | `ACCOUNT_LOCKED` | Account is locked due to failed attempts | -| `403` | `ACCOUNT_DEACTIVATED` | Account is deactivated or deleted | -| `502` | `OAUTH_PROVIDER_ERROR` | Failed to communicate with Google | - -**User Resolution Logic:** -1. If a user with the email exists: - - Links the Google ID if not already linked. - - Sets avatar URL if missing. - - Auto-verifies the email if not already verified. -2. If no user exists: - - Creates a new verified user with a random hashed password. - - Sets `google_id`, `full_name`, and `avatar_url` from the Google profile. - ---- - -## Data Models - -### User - -| Column | Type | Constraints | Description | -|----------------------|----------------|------------------------------|------------------------------| -| `id` | `UUID` | PK, indexed | Unique user identifier | -| `email` | `String(255)` | Unique, indexed, not null | Normalized to lowercase | -| `hashed_password` | `String(255)` | Not null | bcrypt hash | -| `full_name` | `String(255)` | Nullable | Display name | -| `is_active` | `Boolean` | Default: `True` | Account active flag | -| `is_verified` | `Boolean` | Default: `False` | Email verified flag | -| `created_at` | `DateTime(tz)` | Default: `utc_now` | Account creation timestamp | -| `updated_at` | `DateTime(tz)` | Default: `utc_now`, onupdate | Last modification timestamp | -| `deleted_at` | `DateTime(tz)` | Nullable | Soft-delete timestamp | -| `avatar_url` | `String(512)` | Nullable | Profile picture URL | -| `google_id` | `String(255)` | Unique, indexed, nullable | Google OAuth subject ID | -| `speaking_language` | `String(10)` | Default: `"en"` | Preferred speaking language | -| `listening_language` | `String(10)` | Default: `"en"` | Preferred listening language | -| `user_role` | `String(50)` | Default: `"user"`, indexed | Role: `"user"` or `"admin"` | - -### VerificationToken - -| Column | Type | Constraints | Description | -|--------------|----------------|---------------------------|---------------------------------| -| `id` | `Integer` | PK, indexed | Auto-increment identifier | -| `user_id` | `UUID` | FK → `users.id`, not null | Owning user | -| `token` | `String(36)` | Unique, indexed, not null | UUID v4 string | -| `expires_at` | `DateTime(tz)` | Not null | Default: 24 hours from creation | -| `created_at` | `DateTime(tz)` | Not null | Token creation timestamp | - -### PasswordResetToken - -| Column | Type | Constraints | Description | -|--------------|----------------|---------------------------|------------------------------------------------| -| `id` | `Integer` | PK, indexed | Auto-increment identifier | -| `user_id` | `UUID` | FK → `users.id`, not null | Owning user | -| `token` | `String(36)` | Unique, indexed, not null | UUID v4 string | -| `expires_at` | `DateTime(tz)` | Not null | Set from `PASSWORD_RESET_TOKEN_EXPIRE_MINUTES` | -| `created_at` | `DateTime(tz)` | Not null | Token creation timestamp | - ---- - -## Request / Response Schemas - -### Enums - -#### `SupportedLanguage` - -| Value | Label | -|-------|------------| -| `en` | English | -| `fr` | French | -| `de` | German | -| `es` | Spanish | -| `it` | Italian | -| `pt` | Portuguese | - -#### `UserRole` - -| Value | Description | -|---------|-------------------------| -| `user` | Standard user (default) | -| `admin` | Administrator | - -### Request Schemas - -| Schema | Used By | Fields | -|-----------------------------|-----------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------| -| `SignupRequest` | `POST /signup` | `email`, `password` (min 8), `confirm_password`, `accepted_terms` (must be `true`), `full_name?`, `speaking_language?`, `listening_language?` | -| `LoginRequest` | `POST /login` | `email`, `password` | -| `ResendVerificationRequest` | `POST /resend-verification` | `email` | -| `ForgotPasswordRequest` | `POST /forgot-password` | `email` | -| `ResetPasswordRequest` | `POST /reset-password` | `token` (min 1), `new_password` (min 8) | -| `ChangePasswordRequest` | `POST /change-password` | `current_password`, `new_password` (min 8) | - -### Response Schemas - -| Schema | Used By | Fields | -|-------------------------|-----------------------|------------------------------------------------------------------------------------------------------------------------------| -| `SignupResponse` | `POST /signup` | `id`, `email`, `full_name`, `speaking_language`, `listening_language`, `user_role`, `is_active`, `is_verified`, `created_at` | -| `LoginResponse` | `POST /login` | `access_token`, `user_id`, `token_type`, `expires_in` | -| `VerifyEmailResponse` | `GET /verify-email` | `status` (= `"ok"`), `message` | -| `ActionAcknowledgement` | Multiple endpoints | `status` (= `"ok"`), `message` | -| `RefreshTokenResponse` | `POST /refresh-token` | `access_token`, `token_type`, `expires_in` | - ---- - -## Error Codes Reference - -All errors follow a consistent JSON structure: - -```json -{ - "code": "ERROR_CODE", - "message": "Human-readable error description." -} -``` - -### Complete Error Code Table - -| Code | HTTP Status | Endpoint(s) | Description | -|----------------------------|-------------|--------------------------------------------------|--------------------------------------------------| -| `EMAIL_ALREADY_REGISTERED` | 409 | `/signup` | Duplicate email at registration | -| `MISSING_CREDENTIALS` | 400 | `/login` | Empty request body on login | -| `INVALID_CREDENTIALS` | 401 | `/login`, auth guard | Wrong email/password or invalid JWT | -| `EMAIL_NOT_VERIFIED` | 403 | `/login` | Attempting login before email verification | -| `ACCOUNT_DELETED` | 403 | `/login`, auth guard | Account has been soft-deleted | -| `ACCOUNT_LOCKED` | 403 | `/login`, `/google/callback` | Locked after too many failed attempts | -| `ACCOUNT_DEACTIVATED` | 403 | `/refresh-token`, `/google/callback`, auth guard | Account deactivated or deleted | -| `MISSING_TOKEN` | 400/401 | `/verify-email`, auth guard | Token not provided | -| `INVALID_TOKEN` | 400 | `/verify-email` | Token is malformed or not found | -| `TOKEN_EXPIRED` | 400 | `/verify-email` | Verification token has expired | -| `TOKEN_REVOKED` | 401 | Auth guard | Access token has been blacklisted | -| `INVALID_RESET_TOKEN` | 400 | `/reset-password` | Reset token not found or user missing | -| `RESET_TOKEN_EXPIRED` | 400 | `/reset-password` | Password reset token has expired | -| `SAME_PASSWORD` | 400 | `/reset-password`, `/change-password` | New password matches the current one | -| `INCORRECT_PASSWORD` | 400 | `/change-password` | Current password verification failed | -| `MISSING_REFRESH_TOKEN` | 401 | `/refresh-token` | No refresh token cookie present | -| `INVALID_REFRESH_TOKEN` | 401 | `/refresh-token` | Refresh token JWT is invalid or expired | -| `REFRESH_TOKEN_REUSE` | 401 | `/refresh-token` | Revoked token was replayed — all sessions killed | -| `INVALID_OAUTH_STATE` | 400 | `/google/callback` | CSRF state token invalid or expired | -| `INVALID_OAUTH_PROFILE` | 400 | `/google/callback` | Google profile missing email address | -| `OAUTH_PROVIDER_ERROR` | 502 | `/google/callback` | Failed to communicate with Google APIs | - ---- - -## Configuration Reference - -All values are configurable via environment variables or `.env` file. - -### Security & Tokens - -| Setting | Default | Description | -|---------------------------------------|----------------------------|-----------------------------------------------------| -| `SECRET_KEY` | `"placeholder_secret_key"` | JWT signing key. **Must be changed in production.** | -| `ALGORITHM` | `"HS256"` | JWT signing algorithm | -| `ACCESS_TOKEN_EXPIRE_MINUTES` | `60` | Access token lifetime in minutes | -| `REFRESH_TOKEN_EXPIRE_DAYS` | `7` | Refresh token lifetime in days | -| `VERIFICATION_TOKEN_EXPIRE_HOURS` | `24` | Email verification token lifetime in hours | -| `PASSWORD_RESET_TOKEN_EXPIRE_MINUTES` | `60` | Password reset token lifetime in minutes | - -### Account Lockout - -| Setting | Default | Description | -|-----------------------------|---------|-------------------------------------| -| `MAX_FAILED_LOGIN_ATTEMPTS` | `5` | Consecutive failures before lockout | -| `ACCOUNT_LOCKOUT_DAYS` | `5` | Duration of lockout in days | - -### Infrastructure - -| Setting | Default | Description | -|---------------------|---------------------------|------------------------------------------| -| `REDIS_HOST` | `"localhost"` | Redis server hostname | -| `REDIS_PORT` | `6379` | Redis server port | -| `FRONTEND_BASE_URL` | `"http://localhost:3000"` | Base URL for email links (verify, reset) | -| `API_V1_STR` | `"/api/v1"` | API version prefix | - -### Google OAuth - -| Setting | Default | Description | -|------------------------|---------|----------------------------------------------------------| -| `GOOGLE_CLIENT_ID` | `None` | OAuth client ID (required for OAuth) | -| `GOOGLE_CLIENT_SECRET` | `None` | OAuth client secret (required for OAuth) | -| `GOOGLE_REDIRECT_URI` | `None` | Callback URL registered with Google (required for OAuth) | - ---- - -## Internal Services - -### AuthService - -The core business logic coordinator. Injected with all subsystem dependencies via FastAPI's DI. - -**Constructor Dependencies:** -- `db: Session` — SQLAlchemy database session -- `security_service: SecurityService` — Password hashing and JWT operations -- `email_producer: EmailProducerService` — Async email dispatch via Kafka -- `auth_verification_service: AuthVerificationService` — Verification token CRUD -- `lockout_svc: AccountLockoutService` — Brute-force protection -- `token_store: TokenStoreService` — Redis refresh token and blacklist management - -**Public Methods:** - -| Method | Description | -|----------------------------------------------------------------|------------------------------------------------------| -| `signup(user_in, frontend_base_url)` | Create user, generate verification token, send email | -| `login(payload)` | Validate credentials, check guards, issue tokens | -| `forgot_password(email, frontend_base_url)` | Generate reset token, send email | -| `reset_password(token, new_password)` | Validate token, update password, revoke sessions | -| `change_password(user, current_password, new_password)` | Verify current, update hash, revoke sessions | -| `logout(email, access_jti, access_ttl_remaining, refresh_jti)` | Blacklist AT, revoke RT | -| `refresh_token(raw_token)` | Rotate refresh token with reuse detection | -| `resolve_oauth_user(email, google_id, name, avatar_url)` | Find/create/link OAuth user, issue tokens | - -### TokenStoreService - -Redis-backed service managing refresh token JTIs and access token blacklisting. - -**Redis Key Schemas:** - -| Key Pattern | TTL | Purpose | -|----------------------------------|-----------------------|-------------------------------| -| `refresh_token:{email}:{jti}` | Matches token expiry | Valid refresh token indicator | -| `blacklisted_access_token:{jti}` | Remaining AT lifetime | Blacklisted access token | - -**Public Methods:** - -| Method | Description | -|-----------------------------------------------|--------------------------------------------| -| `save_refresh_token(email, jti, ttl_seconds)` | Persist a new refresh token JTI | -| `revoke_refresh_token(email, jti)` | Delete a specific JTI | -| `is_refresh_token_valid(email, jti)` | Check if JTI exists (not revoked/expired) | -| `revoke_all_user_tokens(email)` | SCAN + pipeline delete all JTIs for a user | -| `blacklist_access_token(jti, ttl_seconds)` | Add AT JTI to blacklist | -| `is_access_token_blacklisted(jti)` | Check if AT is blacklisted | - -### AccountLockoutService - -Redis-backed brute-force protection tracking failed login attempts. - -**Public Methods:** - -| Method | Description | -|--------------------------------|----------------------------------------------| -| `is_locked(email)` | Check if account is currently locked | -| `record_failed_attempt(email)` | Increment counter; lock if threshold reached | -| `reset_attempts(email)` | Clear failure counter (on successful login) | - -### AuthVerificationService - -Manages verification token lifecycle for email verification. - -**Public Methods:** - -| Method | Description | -|--------------------------------------|------------------------------------------------| -| `create_verification_token(user_id)` | Generate and persist a new `VerificationToken` | -| `verify_email(token)` | Validate token, activate user, delete token | -| `resend_verification_email(email)` | Delete old tokens, create new one, send email | - -### GoogleOAuthService - -Handles the Google OAuth 2.0 authorization code flow. - -**Public Methods:** - -| Method | Description | -|-------------------------------|-----------------------------------------------------| -| `build_auth_url(state)` | Construct the Google consent URL with CSRF state | -| `exchange_code(code)` | Exchange authorization code for Google access token | -| `get_user_info(access_token)` | Fetch user profile from Google's userinfo endpoint | - ---- - -## Usage Examples - -### cURL: Register a New User - -```bash -curl -X POST http://localhost:8000/api/v1/auth/signup \ - -H "Content-Type: application/json" \ - -d '{ - "email": "jane@example.com", - "password": "mySecureP4ss!", - "full_name": "Jane Doe", - "speaking_language": "en", - "listening_language": "fr" - }' -``` - -### cURL: Login and Capture Refresh Cookie - -```bash -curl -X POST http://localhost:8000/api/v1/auth/login \ - -H "Content-Type: application/json" \ - -c cookies.txt \ - -d '{ - "email": "jane@example.com", - "password": "mySecureP4ss!" - }' -``` - -### cURL: Refresh Token - -```bash -curl -X POST http://localhost:8000/api/v1/auth/refresh-token \ - -b cookies.txt \ - -c cookies.txt -``` - -### cURL: Authenticated Request (Change Password) - -```bash -curl -X POST http://localhost:8000/api/v1/auth/change-password \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer " \ - -d '{ - "current_password": "mySecureP4ss!", - "new_password": "evenMoreSecure!" - }' -``` - -### cURL: Logout - -```bash -curl -X POST http://localhost:8000/api/v1/auth/logout \ - -H "Authorization: Bearer " \ - -b cookies.txt -``` - -### JavaScript: Full Login Flow - -```javascript -// 1. Login -const loginRes = await fetch('/api/v1/auth/login', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - credentials: 'include', // important: sends/receives cookies - body: JSON.stringify({ - email: 'jane@example.com', - password: 'mySecureP4ss!', - }), -}); -const { access_token, expires_in } = await loginRes.json(); - -// 2. Make authenticated requests -const res = await fetch('/api/v1/some-protected-endpoint', { - headers: { Authorization: `Bearer ${access_token}` }, - credentials: 'include', -}); - -// 3. Refresh token before expiry -const refreshRes = await fetch('/api/v1/auth/refresh-token', { - method: 'POST', - credentials: 'include', // sends the refresh_token cookie -}); -const { access_token: newToken } = await refreshRes.json(); - -// 4. Logout -await fetch('/api/v1/auth/logout', { - method: 'POST', - headers: { Authorization: `Bearer ${newToken}` }, - credentials: 'include', -}); -``` diff --git a/app/modules/meeting/api-docs.md b/app/modules/meeting/api-docs.md deleted file mode 100644 index b72ecff..0000000 --- a/app/modules/meeting/api-docs.md +++ /dev/null @@ -1,584 +0,0 @@ -# FluentMeet Meeting API Documentation - -> **Base URL:** `/api/v1/meetings` (Assuming router prefix, though undefined in `router.py`, wait let me check `main.py` or just document the endpoints as defined, usually it's `/api/v1/meetings`). -> **Version:** 1.0 · **Protocol:** REST over HTTPS & WebSockets · **Content-Type:** `application/json` - ---- - -## Table of Contents - -- [Overview](#overview) -- [Architecture](#architecture) -- [Meeting Lifecycle](#meeting-lifecycle) -- [Real-time State (Redis)](#real-time-state-redis) -- [REST Endpoints](#rest-endpoints) - - [POST /](#post-) - - [GET /history](#get-history) - - [GET /{room_code}](#get-room_code) - - [GET /{room_code}/participants](#get-room_codeparticipants) - - [POST /{room_code}/join](#post-room_codejoin) - - [POST /{room_code}/leave](#post-room_codeleave) - - [POST /{room_code}/admit/{user_id}](#post-room_codeadmituser_id) - - [POST /{room_code}/end](#post-room_codeend) - - [PATCH /{room_code}/config](#patch-room_codeconfig) - - [POST /{room_code}/invite](#post-room_codeinvite) -- [WebSocket Endpoints](#websocket-endpoints) - - [WS /signaling/{room_code}](#ws-signalingroom_code) - - [WS /audio/{room_code}](#ws-audioroom_code) - - [WS /captions/{room_code}](#ws-captionsroom_code) -- [Data Models](#data-models) -- [Request / Response Schemas](#request--response-schemas) -- [Internal Services](#internal-services) - ---- - -## Overview - -The FluentMeet meeting module provides comprehensive meeting management, supporting: - -- **Room Management:** Creation, scheduling, retrieval, updates, and forced ending. -- **Participant Tracking:** Identifying registered users and dynamic token-based guests. -- **Real-time State:** Lobby (waiting room) management and active connections tracked via Redis. -- **Invitations:** Email invitations utilizing Kafka email producers. -- **Live Streams (WebSockets):** WebRTC signaling, AI pipeline audio streaming (STT + TTS integration), and live translation captions. - ---- - -## Architecture - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ FastAPI Routers (REST & WebSockets) │ -│ (router.py, ws_router.py) │ -├─────────────────┬──────────────────────┬────────────────────────┤ -│ │ │ │ -│ MeetingService │ MeetingStateService │ MeetingRepository │ -│ (service.py) │ (state.py) │ (repository.py) │ -│ │ │ │ -│ │ │ │ │ │ │ -│ │ ▼ ▼ │ ▼ │ -│ │ ┌────────────┐ │ ┌────────────┐ │ -│ │ │ Redis │ │ │PostgreSQL │ │ -│ │ │(Live State)│ │ │(Rooms, Pts)│ │ -│ │ └────────────┘ │ └────────────┘ │ -│ ▼ ▼ │ -│ ┌────────────┐ ┌────────────────┐ │ -│ │ Email │ │ Kafka Pipeline │ │ -│ │ Producer │ │ Audio & Text │ │ -│ └────────────┘ └────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### Module Files - -| File | Purpose | -|----------------------|----------------------------------------------------------------------------------------| -| `router.py` | FastAPI REST route definitions for room CRUD and participant logic. | -| `ws_router.py` | WebSocket endpoints for WebRTC signaling, audio stream ingestion/egress, and captions. | -| `service.py` | Core business logic — joining, leaving, lobby logic, room updates. | -| `state.py` | Redis-backed ephemeral state tracking (lobby, live participants, active speaker). | -| `repository.py` | SQLAlchemy database wrapper for rooms and participants. | -| `schemas.py` | Pydantic request/response models and validators. | -| `models.py` | SQLAlchemy ORM models (`Room`, `Participant`, `MeetingInvitation`). | -| `dependencies.py` | FastAPI dependency injection factories. | -| `ws_dependencies.py` | WebSocket-specific JWT authentication (`authenticate_ws`). | -| `constants.py` | Definitions of message strings, defaults, and enums (`ParticipantRole`, `RoomStatus`). | - ---- - -## Meeting Lifecycle - -1. **Creation:** A Host creates a room (instant or scheduled). The room gets a `PENDING` status. -2. **Joining / Lobby:** - - Authenticated Users and Guests send `POST /{room_code}/join`. - - If the room is not active yet (for non-hosts) or the room requires host admission (lobby locked), the participant is waitlisted. - - Host joining auto-activates `PENDING` rooms. -3. **Live:** Live state (participants, active speaker) is pushed to Redis. WebSockets can now be securely accessed. -4. **Conclusion:** Host explicitly ends meeting (`POST /{room_code}/end`). This wipes Redis state and updates the DB to `ENDED`. - ---- - -## Real-time State (Redis) - -Live meeting state is ephemeral and purely managed inside Redis for high-performance retrieval and updates. - -**Redis Keys:** - -| Key Pattern | Data Structure | Purpose | -|-----------------------------------|----------------|---------------------------------------------------------------------------------------| -| `room:{room_code}:participants` | Hash | Stores connected user IDs and their JSON state (language, hardware_ready, status). | -| `room:{room_code}:lobby` | Hash | Stores waitlisted guest/user IDs, their display names, and target listening language. | -| `room:{room_code}:active_speaker` | String | Volatile key with a low TTL (e.g. 5s). Identifies current dominant speaker. | - ---- - -## REST Endpoints - -*(Endpoints assume prefix `/api/v1/meetings`, but refer to your main `FastAPI.include_router` setup for exact path.)* - ---- - -### POST / - -Create a new meeting room. - -**🔒 Requires Authentication:** `Authorization: Bearer ` - -**Request Body:** - -```json -{ - "name": "Project Alpha Sync", - "scheduled_at": "2026-04-10T15:00:00Z", - "settings": { - "lock_room": false, - "enable_transcription": true, - "max_participants": 2 - } -} -``` - -| Field | Type | Required | Notes | -|----------------|------------|----------|------------------------------------------------------------------| -| `name` | `string` | ✅ | Max 255 chars | -| `settings` | `object` | ❌ | Contains `lock_room`, `enable_transcription`, `max_participants` | -| `scheduled_at` | `datetime` | ❌ | Defaults to `null` (creates ad-hoc instant meeting) | - -**Response: `201 Created`** Returns a `RoomApiResponse` enveloping the created `RoomResponse`. - -```json -{ - "status_code": 201, - "status": "success", - "message": "Room created successfully.", - "data": { - "room_code": "TszC8Gahl_W-", - "name": "Project Alpha Sync", - "host_id": "d69d5ffe-1693-4f58-a56b-32dbe920f940", - "status": "pending", - "settings": { - "lock_room": true, - "enable_transcription": true, - "max_participants": 2 - }, - "scheduled_at": "2026-04-17T20:34:22.004223+01:00", - "created_at": "2026-04-17T20:34:22.043308+01:00", - "ended_at": null, - "join_url": "http://localhost:3000/meet/TszC8Gahl_W-", - "participant_count": null, - "total_participants": null, - "duration": null - } -} -``` - ---- - -### GET /history - -Retrieve a paginated list of meetings the user has hosted or participated in. - -**🔒 Requires Authentication:** `Authorization: Bearer ` - -**Query Parameters:** -- `role`: string `host`, `guest`, or `all` (default). -- `page`: int >= 1 -- `page_size`: int between 1-100 - -**Response: `200 OK`** Returns a paginated list of `MeetingHistoryItem` objects (fields: `room_code`, `name`, `duration_minutes`, `participant_count`, etc.) - -```json -{ - "status_code": 200, - "status": "success", - "message": "Meeting history retrieved successfully.", - "data": { - "total": 1, - "page": 1, - "page_size": 20, - "items": [ - { - "room_code": "TszC8Gahl_W-", - "name": "test-room", - "created_at": "2026-04-17T20:34:22.043308+01:00", - "ended_at": "2026-04-17T21:41:45.362279+01:00", - "duration_minutes": 67, - "participant_count": 2, - "role": "host" - } - ] - } -} -``` - ---- - -### GET /{room_code} - -Retrieve the current room's details including a live-calculated participant count. - -**Response: `200 OK`** -Returns standard `RoomResponse` inside an envelope. The `participant_count` will merge DB counts or Active Redis counts depending on the room's current state (`PENDING`/`ENDED` vs `ACTIVE`). - -```json -{ - "status_code": 200, - "status": "success", - "message": "Room details retrieved successfully.", - "data": { - "room_code": "TszC8Gahl_W-", - "name": "Project Alpha Sync", - "host_id": "d69d5ffe-1693-4f58-a56b-32dbe920f940", - "status": "active", - "settings": { - "lock_room": true, - "enable_transcription": true, - "max_participants": 2 - }, - "scheduled_at": "2026-04-17T20:34:22.004223+01:00", - "created_at": "2026-04-17T20:34:22.043308+01:00", - "ended_at": null, - "join_url": "http://localhost:3000/meet/TszC8Gahl_W-", - "participant_count": 1, - "total_participants": 5, - "duration_minutes": 15 - } -} -``` - ---- - -### GET /{room_code}/participants - -Get the live state of the active participants and the waiting list (lobby). - -**🔒 Requires Authentication:** `Authorization: Bearer ` (Host only) - -**Response: `200 OK`** Returns a payload containing lists of `active` connections and users in the `lobby`. - -```json -{ - "status": "success", - "message": "Live room state retrieved", - "data": { - "active": { - "d69d5ffe-1693-4f58-a56b-32dbe920f940": { - "status": "connected", - "language": "en", - "hardware_ready": true - }, - "03dde231-3472-456b-b9d8-a4fbcab1e60c": { - "status": "connected", - "language": "en", - "hardware_ready": true - } - }, - "lobby": {} - } -} -``` - -```json -{ - "status": "success", - "message": "Live room state retrieved", - "data": { - "active": { - "d69d5ffe-1693-4f58-a56b-32dbe920f940": { - "status": "connected", - "language": "de", - "hardware_ready": true - } - }, - "lobby": { - "03dde231-3472-456b-b9d8-a4fbcab1e60c": { - "display_name": "aniebiet afia", - "language": "es" - } - } - } -} -``` - ---- - -### POST /{room_code}/join - -Join a room or enter the lobby. Handles authentication automatically. Unauthenticated users must supply a display name. - -**Query / Header:** Handled automatically (Bearer Token makes you an authenticated user). - -**Request Body:** - -```json -{ - "listening_language": "en", - "speaking_language": "en", - "display_name": "Aniebiet Afia" -} -``` - -**Response: `200 OK`** -```json -{ - "status": "success", - "message": "Joined room successfully.", - "data": { - "status": "joined" - } -} -``` - -```json -{ - "status": "success", - "message": "Joined room successfully.", - "data": { - "status": "waiting" - } -} -``` - ---- - -### POST /{room_code}/leave - -Leave an active room. Drops the user out of the Redis tracking structures (participants hash or lobby hash) and sets `left_at` in the DB. - -**Authentication:** Optional. (If logged in, uses user ID; otherwise looks for `guest_session_id` out of a JWT). - -**Response** -```json -{ - "status": "success", - "message": "Left room successfully." -} -``` - ---- - -### POST /{room_code}/admit/{user_id} - -Admit a waitlisted participant out of the lobby and into the live room. - -**🔒 Requires Authentication:** Host only. - -**Response: `200 OK`** Returns the updated live participant list and lobby list in the same format as `GET /{room_code}/participants` to sync the host's view immediately. - -```json -{ - "status": "success", - "message": "User admitted to room." -} -``` - ---- - -### POST /{room_code}/end - -Forcibly end the meeting. Immediately updates the DB state to `ENDED`, tallies up the `duration_minutes`, and wipes all real-time structures in Redis. - -**🔒 Requires Authentication:** Host only. - -**Response: `200 OK`** -```json -{ - "status_code": 200, - "status": "success", - "message": "Meeting ended successfully.", - "data": { - "room_code": "TszC8Gahl_W-", - "name": "test-room", - "host_id": "d69d5ffe-1693-4f58-a56b-32dbe920f940", - "status": "ended", - "settings": { - "lock_room": false, - "enable_transcription": false, - "max_participants": 2 - }, - "scheduled_at": "2026-04-17T20:34:22.004223+01:00", - "created_at": "2026-04-17T20:34:22.043308+01:00", - "ended_at": "2026-04-17T21:41:45.362279+01:00", - "join_url": null, - "participant_count": null, - "total_participants": 2, - "duration": "1 hours, 7 minutes" - } -} -``` - ---- - -### PATCH /{room_code}/config - -Update a live room's settings natively. - -**🔒 Requires Authentication:** Host only. - -**Behavior:** -Modifies the room DB, then automatically invokes `ConnectionManager.broadcast_to_room` over WebSockets to sync settings with all connected peers immediately. - -**Request Body:** -```json -{ - "lock_room": true, - "enable_transcription": false, - "max_participants": 5 -} -``` - -**Response: `200 OK`** Returns the updated `RoomResponse` with the new settings. - -```json -{ - "status": "success", - "message": "Room configuration updated.", - "data": { - "settings": { - "lock_room": false, - "enable_transcription": false, - "max_participants": 2 - } - } -} -``` - ---- - -### POST /{room_code}/invite - -Dispatch email invitations utilizing the async Kafka email producer. - -**🔒 Requires Authentication:** Host only. - -**Request Body:** -```json -{ - "emails": ["user1@example.com", "user2@example.com"] -} -``` - -**Response: `200 OK`** Indicates how many emails successfully enqueued vs failed. - -```json -{ - "status_code": 200, - "status": "success", - "message": "Meeting invitations sent.", - "data": { - "sent": 2, - "failed": [] - } -} -``` - ---- - -## WebSocket Endpoints - -Clients connect using a `?token=` query parameter for authentication instead of HTTP headers. The JWT can be a standard Access Token or a Guest Token returned from `POST /{room_code}/join`. - -### WS /signaling/{room_code} - -- **Purpose:** Relay mechanism for WebRTC handshakes (Offer/Answer/ICE candidates). -- **Behavior:** Accepts payloads pointing to a `target_user_id` (unicast direct to them) or broadcast mode if empty. - -### WS /audio/{room_code} - -- **Purpose:** Fast bidirectional streaming to the AI Pipeline. -- **Ingestion:** Reads raw binary chunks from the client, sends as `audio.raw` chunks into Kafka. -- **Egress:** Listens for `audio.synthesized` chunks from Kafka. Filters frames checking if the client's `listening_language` explicitly matches the frame target. If it does, pushes binary bytes down the WebSocket to the client. - -### WS /captions/{room_code} - -- **Purpose:** Real-time text captions. -- **Behavior:** Connects to standard outputs (`text.original` and `text.translated`) in Kafka, formats into normalized `{event: "caption", speaker_id: ..., text: ...}` blobs, and pushes down the WebSocket. - ---- - -## Data Models - -### Room - -| Column | Type | Constraints | Description | -|----------------|--------------|---------------------------|---------------------------------------------| -| `id` | `UUID` | PK, indexed | Unique room identifier | -| `room_code` | `String(12)` | Unique, indexed, not null | URL-safe slug for the room | -| `host_id` | `UUID` | indexed, not null | Foreign Key reference to the user. | -| `status` | `String(10)` | Default `'pending'` | Room status (`pending`, `active`, `ended`) | -| `scheduled_at` | `DateTime` | Nullable | Optional future date | -| `settings` | `JSON` | Dict | Keys: `lock_room`, `max_participants`, etc. | - -### Participant - -| Column | Type | Constraints | Description | -|--------------------|---------------|-------------------|---------------------------------------------| -| `id` | `UUID` | PK, indexed | Unique participant identifier | -| `room_id` | `UUID` | indexed, not null | ForeignKey | -| `user_id` | `UUID` | Nullable | ForeignKey (Null if Guest) | -| `guest_session_id` | `UUID` | Nullable | Session tracking ID for anonymous guests | -| `display_name` | `String(255)` | Not Null | User's profile name OR guest-submitted name | -| `role` | `String(10)` | Default `'guest'` | Role: `host`, `participant`, `guest` | - -### MeetingInvitation - -| Column | Type | Constraints | Description | -|--------------|---------------|------------------|-------------------------------------------| -| `token` | `String(64)` | Unique, not null | Cryptographic token embedded in the email | -| `email` | `String(255)` | Not null | Targeted invited email | -| `expires_at` | `DateTime` | Not null | Automatically set +48 hours from dispatch | - ---- - -## Request / Response Schemas - -### Request Schemas - -| Schema | Used By | Fields | -|--------------------|-----------------|------------------------------------------------------------| -| `RoomCreate` | `POST /` | `name`, `settings`, `scheduled_at` | -| `JoinRoomRequest` | `POST /join` | `display_name (optional)`, `listening_language (optional)` | -| `RoomConfigUpdate` | `PATCH /config` | Matches settings fields | -| `InviteRequest` | `POST /invite` | `emails (list[str])` | - -### Enums - -#### `ParticipantRole` - -| Value | Description | -|---------------|----------------------------------------| -| `host` | The room creator. | -| `guest` | Unauthenticated / generic participant. | -| `participant` | A standard authenticated user. | - -#### `RoomStatus` - -| Value | Description | -|-----------|-------------------------------------------------------| -| `pending` | Created, but host hasn't explicitly entered the room. | -| `active` | The host has officially walked through the door. | -| `ended` | Meeting explicitly shut down by the host. | - ---- - -## Internal Services - -### MeetingService - -The core routing logic engine for the module. - -| Method | Purpose | -|-------------------|------------------------------------------------------------------------------------------------------------------------------------------------------| -| `create_room()` | Enforces unique slug handling and builds database references. | -| `join_room()` | Reconciles User identity vs Guest Token vs Returning PT states. Resolves if a user bypasses straight into the `ACTIVE` room or halts inside `Lobby`. | -| `update_config()` | Handles patching `room.settings` gracefully and prepares the payload. | - -### MeetingStateService - -Encapsulates all interaction with Redis for high-throughput ephemeral states like Live Participants or Lobbies. Uses native Redis paradigms like Pipelines and Hashes for quick mutations. - -| Method | Purpose | -|----------------------------------------------|--------------------------------------------------------| -| `add_participant()` / `remove_participant()` | Manages live room occupancy hash map. | -| `add_to_lobby()` / `admit_from_lobby()` | Waitlisting pipeline actions ensuring atomicity. | -| `cleanup_room()` | Destroys all traces of a room in Redis during `end()`. | diff --git a/app/modules/meeting/ws_router.py b/app/modules/meeting/ws_router.py index 3944deb..949ceef 100644 --- a/app/modules/meeting/ws_router.py +++ b/app/modules/meeting/ws_router.py @@ -95,8 +95,7 @@ async def signaling_websocket( target_user_id = payload.get("target_user_id") # Always inject the sender's identity so the recipient knows - # who sent the offer/answer/ice_candidate. Without this, - # from_user_id is undefined on the frontend. + # who sent the offer/answer/ice_candidate. payload["from_user_id"] = user_id # If target specified, unicast. Otherwise, broadcast. @@ -164,8 +163,8 @@ async def ingest_task() -> None: if message.get("text"): try: data = base64.b64decode(message["text"]) - except Exception: - logger.warning("Failed to decode base64 audio text frame.") + except Exception as exc: + logger.warning(f"Failed to decode base64 audio text frame. Skipping frame. Error: {exc}") continue elif "bytes" in message and message["bytes"] is not None: data = message["bytes"] diff --git a/app/modules/user/api-docs.md b/app/modules/user/api-docs.md deleted file mode 100644 index af8f61c..0000000 --- a/app/modules/user/api-docs.md +++ /dev/null @@ -1,242 +0,0 @@ -# FluentMeet User API Documentation - -> **Base URL:** `/api/v1/users` -> **Version:** 1.0 · **Protocol:** REST over HTTPS · **Content-Type:** `application/json` (except for avatar upload) - ---- - -## Table of Contents - -- [Overview](#overview) -- [Architecture](#architecture) -- [Endpoints](#endpoints) - - [GET /me](#get-me) - - [PATCH /me](#patch-me) - - [POST /me/avatar](#post-meavatar) - - [DELETE /me](#delete-me) -- [Request / Response Schemas](#request--response-schemas) -- [Internal Services](#internal-services) - ---- - -## Overview - -The FluentMeet user module manages the authenticated user's profile and account settings. It provides endpoints for: - -- **Profile Retrieval:** Fetching the current user's profile details safely (excluding sensitive data like hashed passwords). -- **Profile Updates:** Modifying display name and language preferences. -- **Avatar Management:** Uploading and securely replacing profile pictures via Cloudinary. -- **Account Deletion:** GDPR-compliant account deletion (soft and hard deletes) complete with immediate session invalidation and cloud asset cleanup. - ---- - -## Architecture - -The user module leans on the central `auth.models.User` ORM model but encapsulates all business logic related to profile management in its own `UserService`. - -``` -┌────────────────────────────────────────────────────────┐ -│ FastAPI Router │ -│ (app/modules/user/router.py) │ -├──────────────────────────┬─────────────────────────────┤ -│ │ │ -│ UserService │ StorageService │ -│ (service.py) │ (external_services/.../) │ -│ │ │ -│ │ │ │ │ -│ ▼ │ ▼ │ -│ ┌────────────┐ │ ┌──────────────┐ │ -│ │PostgreSQL │ │ │ Cloudinary │ │ -│ │ (Users) │ │ │ (Avatars) │ │ -│ └────────────┘ │ └──────────────┘ │ -│ ▼ │ -│ ┌────────────┐ │ -│ │ Redis │ │ -│ │ (Sessions) │ │ -│ └────────────┘ │ -└────────────────────────────────────────────────────────┘ -``` - -### Module Files - -| File | Purpose | -|---|---| -| `router.py` | FastAPI route definitions (`/me` endpoints). Handles session revocation for deletes and proxying to external services. | -| `service.py` | DB-level CRUD operations (`UserService`), handling safe partial updates, and soft/hard deletes. | -| `schemas.py` | Pydantic request/response models tailored for public profile consumption. | -| `dependencies.py` | FastAPI dependency injection factory (`get_user_service`). | -| `constants.py` | Standardized response messages and Cloudinary folder definitions (`AVATAR_FOLDER`). | -| `models.py` / `helpers.py` | Kept for module structural consistency (currently empty, relies on `app.modules.auth.models.User`). | - ---- - -## Endpoints - -*(All endpoints in this module implicitly require the user to be authenticated.)* - ---- - -### GET /me - -Retrieve the current authenticated user's profile. - -**🔒 Requires Authentication:** `Authorization: Bearer ` - -**Response: `200 OK`** - -```json -{ - "status_code": 200, - "status": "success", - "message": "User profile retrieved successfully.", - "data": { - "id": "550e8400-e29b-41d4-a716-446655440000", - "email": "user@example.com", - "full_name": "Jane Doe", - "avatar_url": "https://res.cloudinary.com/.../fluentmeet/avatars/abc.jpg", - "speaking_language": "en", - "listening_language": "fr", - "is_active": true, - "is_verified": true, - "user_role": "user", - "created_at": "2026-04-10T12:00:00Z" - } -} -``` - ---- - -### PATCH /me - -Update the current user's profile properties. The update payload is partial; only supplied fields are modified. - -**🔒 Requires Authentication:** `Authorization: Bearer ` - -**Request Body:** - -```json -{ - "full_name": "Jane H. Doe", - "listening_language": "es" -} -``` - -| Field | Type | Required | Notes | -|---|---|---|---| -| `full_name` | `string \| null` | ❌ | Max 255 chars | -| `speaking_language`| `string (enum)` | ❌ | Values: `en`, `fr`, `de`, `es`, `it`, `pt` | -| `listening_language`|`string (enum)` | ❌ | Values: `en`, `fr`, `de`, `es`, `it`, `pt` | - -**Response: `200 OK`** -Returns a `ProfileApiResponse` enclosing the updated `UserProfileResponse`. - ---- - -### POST /me/avatar - -Upload or replace the user's profile avatar. Files are stored and transformed heavily via Cloudinary (cropped to face). - -**🔒 Requires Authentication:** `Authorization: Bearer ` - -**Content-Type:** `multipart/form-data` - -**Request Body:** - -| Form Field | Type | Required | Notes | -|---|---|---|---| -| `avatar` | `File` | ✅ | Valid formats: JPEG, PNG, WebP. Max upload size: 5 MB. | - -**Behavior:** -1. If the user already has an avatar URL matching the host `AVATAR_FOLDER`, the server calculates the old `public_id` and explicitly hard-deletes the old asset from Cloudinary via `StorageService`. -2. The server issues a secure upload request parsing the uploaded file buffer to Cloudinary, forcing a synchronous face-cropping logic transform (`width=400, height=400, crop=fill, gravity=face`). -3. Overwrites the User `avatar_url` database string with the fresh `secure_url`. - -**Response: `200 OK`** -Returns an `AvatarUploadResponse` containing the full updated public user data. - ---- - -### DELETE /me - -Delete the authenticated user's account and instantly invalidate all sessions. - -**🔒 Requires Authentication:** `Authorization: Bearer ` - -**Query Parameters:** - -| Parameter | Type | Required | Description | -|---|---|---|---| -| `hard` | `boolean` | ❌ | Default: `false`. Standard request triggers a soft delete. Passing `?hard=true` triggers a permanent hard wipe. | - -**Behavior (Soft Delete - Default):** -- Modifies DB setting `deleted_at = NOW()` and `is_active = False`. The database row and connected relations are retained for recovery or auditing. - -**Behavior (Hard Delete - `?hard=true`):** -- Triggers GDPR-compliant total erasure. -- Parses the active Cloudinary `avatar_url` (if any), identifies the `public_id` and permanently deletes the remote image. -- Permanently deletes all `VerificationToken` rows bound to the user. -- Permanently hard-deletes the `User` database row itself. - -**Post-Delete Session Teardown (Triggered in both modes):** -1. **Redis Blacklist:** Evaluates the `jti` of the actively submitted `Bearer` token and blacklists the identifier natively inside the Token Store limiting its remaining lifetime to zero. -2. **Redis Revocation:** Scans for and wipes **all** currently valid Refresh Tokens tied to the user email. -3. **Cookie Ejection:** Attaches `Set-Cookie` directives setting the HTTP-only `refresh_token` value to nothing, essentially wiping it from the connected client browser. - -**Response: `200 OK`** -```json -{ - "status": "ok", - "message": "Account has been deactivated and scheduled for deletion." // (Or "Account has been successfully deleted." for hard delete) -} -``` - ---- - -## Request / Response Schemas - -### UserProfileResponse - -The primary sanitized entity containing the public footprint of an authenticated user. It strictly omits relational or highly sensitive fields (`hashed_password`, `deleted_at`, `updated_at`). - -| Field | Type | Description | -|---|---|---| -| `id` | `UUID` | Unique account string. | -| `email` | `string` | Normalized e-mail. | -| `full_name` | `string \| null` | | -| `avatar_url` | `string \| null` | FQDN Cloudinary link. | -| `speaking_language`| `string (enum)`| | -| `listening_language`| `string (enum)`| | -| `is_active` | `bool` | Default `true`. | -| `is_verified` | `bool` | True if user resolved their email verify prompt. | -| `user_role` | `string`| Default `user`. | -| `created_at`| `datetime`| | - -### Envelopes - -Endpoints consistently envelope success data inside the following wrappers: -- `ProfileApiResponse` -> `{ status_code, status, message, data: UserProfileResponse }` -- `AvatarUploadResponse` -> `{ status_code, status, message, data: UserProfileResponse }` -- `DeleteResponse` -> `{ status, message }` - ---- - -## Internal Services - -### UserService (`service.py`) - -A decoupled database manipulation layer interacting safely with the central `User` ORM Model. - -| Method | Purpose | -|---|---| -| `get_user_by_id(user_id)` | Single record entity load. | -| `update_user(user, update_data)` | Safely runs simple setter injections checking against null validations. | -| `update_avatar_url(user, url)` | Targeted atomical avatar set. | -| `soft_delete_user(user)` | Performs column mutations locking `deleted_at` & `is_active`. | -| `hard_delete_user(user)` | Runs heavy cascading hard deletion sequences targeting `VerificationToken`s before destroying the base SQL entity row. | - -### Helper Methods (`router.py`) - -| Method | Purpose | -|---|---| -| `_extract_public_id(secure_url)`| String modification utility used exclusively to derive the inner specific `public_id` string from an outbound Cloudinary URL required to command external delete actions over its API. | -| `_extract_bearer_token(request)`| Bypasses standard FastAPI dependency logic to manually intercept the raw Bearer JWT text off the live request, necessary for JTI calculation & token string Blacklisting during account deletion steps. | diff --git a/app/routers/api-docs.md b/app/routers/api-docs.md deleted file mode 100644 index 749496b..0000000 --- a/app/routers/api-docs.md +++ /dev/null @@ -1,107 +0,0 @@ -# FluentMeet Routers Documentation - -> **Package Location:** `/app/routers` -> **Purpose:** Centralized API Route Aggregation - ---- - -## Table of Contents - -- [Overview](#overview) -- [Architecture](#architecture) -- [Router Configuration](#router-configuration) - - [Authentication Router](#authentication-router) - - [User Router](#user-router) - - [Meeting Router](#meeting-router) - - [WebSocket Router](#websocket-router) -- [Integration](#integration) - ---- - -## Overview - -The `routers` package in FluentMeet is a lightweight, centralized aggregation layer. It uses FastAPI's `APIRouter.include_router()` method to collect the distinct, feature-based routers from various modules (authentication, user profile, meetings, websockets) and bundle them into a single, cohesive API router (`api_router`). - -This single `api_router` is then mounted by the main FastAPI application instance (typically in `app/main.py`), keeping the core application entry point clean and adhering to a modular, decoupled architecture. - ---- - -## Architecture - -The architecture relies on the feature packages defining their own localized routing and prefixes, which are then combined here. - -``` -┌────────────────────────────────────────────────────────┐ -│ app/main.py │ -│ app.include_router(api_router, prefix="/api/v1") │ -└──────────────────────────┬─────────────────────────────┘ - │ - ▼ -┌────────────────────────────────────────────────────────┐ -│ app/routers/api.py │ -│ (api_router) │ -├──────────────┬───────────────┬─────────────────────────┤ -│ │ │ │ -▼ ▼ ▼ ▼ -auth_router users_router meeting_router ws_router -(no prefix) (no prefix) (prefix="/meetings") (prefix="/ws") -│ │ │ │ -│ │ │ │ -app/modules/ app/modules/ app/modules/ app/modules/ -auth/ user/ meeting/ meeting/ -router.py router.py router.py ws_router.py -``` - -*Note: Feature modules like `auth` and `user` define their own sub-prefixes internally (e.g., `prefix="/auth"` and `prefix="/users"` respectively inside their own router definitions), whereas prefixes like `/meetings` and `/ws` are explicitly assigned during inclusion in `api.py`.* - ---- - -## Router Configuration - -The `api_router` integrates the following module routers: - -### Authentication Router -- **Imported from:** `app.modules.auth.router.router` -- **Prefix:** None assigned in `api.py` (Inherits `/auth` from the module itself). -- **Tags:** `auth` -- **Purpose:** Handles signup, login, password recovery, token rotation, and Google OAuth 2.0 flows. - -### User Router -- **Imported from:** `app.modules.user.router.router` -- **Prefix:** None assigned in `api.py` (Inherits `/users` from the module itself). -- **Tags:** `users` -- **Purpose:** Handles authenticated user profile fetching, updating, avatar uploading, and Soft/Hard GDPR-compliant account deletion. - -### Meeting Router -- **Imported from:** `app.modules.meeting.router.router` -- **Prefix:** `/meetings` (Explicitly assigned in `api.py`). -- **Tags:** `meetings` -- **Purpose:** Handles meeting room CRUD operations, configurations, waitlist lobby admission logic, and email invitations. - -### WebSocket Router -- **Imported from:** `app.modules.meeting.ws_router.router` -- **Prefix:** `/ws` (Explicitly assigned in `api.py`). -- **Tags:** `websockets` -- **Purpose:** Handles persistent connections for real-time WebRTC signaling (`/signaling`), Kafka-bridged audio stream ingestion/egress (`/audio`), and translated transcription payloads (`/captions`). - ---- - -## Integration - -To integrate this bundle into the main FastAPI application, `api_router` is imported and mounted inside the app initialization logic. - -**Example (`app/main.py`):** - -```python -from fastapi import FastAPI -from app.routers.api import api_router -from app.core.config import settings - -app = FastAPI( - title=settings.PROJECT_NAME, - version=settings.VERSION, -) - -# Mounts all collected routes under the global API prefix (e.g., /api/v1) -app.include_router(api_router, prefix=settings.API_V1_STR) -``` diff --git a/app/schemas/api-docs.md b/app/schemas/api-docs.md deleted file mode 100644 index 901afe3..0000000 --- a/app/schemas/api-docs.md +++ /dev/null @@ -1,152 +0,0 @@ -# FluentMeet Schemas Documentation - -> **Package Location:** `/app/schemas` -> **Purpose:** Global Pydantic definitions and Kafka Real-time Pipeline Schemas. - ---- - -## Table of Contents - -- [Overview](#overview) -- [Pipeline Architecture](#pipeline-architecture) -- [Pipeline Stages & Schemas](#pipeline-stages--schemas) - - [Stage 1: Raw Audio Ingest](#stage-1-raw-audio-ingest) - - [Stage 2: Transcribed Text](#stage-2-transcribed-text) - - [Stage 3: Translated Text](#stage-3-translated-text) - - [Stage 4: Synthesized Audio](#stage-4-synthesized-audio) -- [Data structures](#data-structures) -- [Enums](#enums) - ---- - -## Overview - -Unlike module-specific schemas (e.g., `app/modules/auth/schemas.py`), the `/app/schemas` package contains global and cross-boundary DTOs (Data Transfer Objects). Primarily, it defines the rigid contract used by the **Real-Time Audio Processing Pipeline** flowing through Kafka. - -These schemas ensure that the FastAPI web consumers, STT workers, Translation workers, and TTS workers all serialize and deserialize their payloads using identical schemas and base-64 encodings format. - -All pipeline events inherit from `BaseEvent[T]` (from `app.kafka.schemas`) allowing metadata headers to envelop the core payloads documented below. - ---- - -## Pipeline Architecture - -The schemas correspond directly to the 4 stages of the real-time processing loop orchestrated over Apache Kafka: - -``` -[ WebSocket Client (Binary) ] - │ - ▼ -[ STAGE 1: audio.raw ] ───▶ AudioChunkEvent - │ - ▼ -[ STAGE 2: text.original ] ───▶ TranscriptionEvent - │ - ▼ -[ STAGE 3: text.translated ] ───▶ TranslationEvent - │ - ▼ -[ STAGE 4: audio.synthesized ] ───▶ SynthesizedAudioEvent - │ - ▼ -[ WebSocket Egress (Binary) ] -``` - ---- - -## Pipeline Stages & Schemas - -### Stage 1: Raw Audio Ingest - -**Kafka Topic:** `audio.raw` -**Event wrapper:** `AudioChunkEvent` -> `{ event_type: "audio.chunk", payload: AudioChunkPayload }` - -**`AudioChunkPayload`** -Represents a chunk of binary audio intercepted from an active WebSocket stream. - -| Field | Type | Description | -|---|---|---| -| `room_id` | `string` | The active room code. | -| `user_id` | `string` | UUID or guest-tracking UUID of the active speaker. | -| `sequence_number`| `int` | Monotonically increasing chunk index ensuring ordering per-speaker. | -| `audio_data` | `string` | Base64-encoded raw application binary bytes. | -| `sample_rate` | `int` | Default: `16000` (Hz). | -| `encoding` | `AudioEncoding`| Default: `linear16` (PCM 16-bit). | -| `source_language`| `string` | Language code (ISO 639-1) the user is speaking (e.g. `"en"`). | - ---- - -### Stage 2: Transcribed Text - -**Kafka Topic:** `text.original` -**Event wrapper:** `TranscriptionEvent` -> `{ event_type: "text.transcription", payload: TranscriptionPayload }` - -**`TranscriptionPayload`** -Produced by the Speech-to-Text Worker (Deepgram) converting the raw audio chunk into its native text. - -| Field | Type | Description | -|---|---|---| -| `room_id` | `string` | | -| `user_id` | `string` | | -| `sequence_number`| `int` | Maintained from Stage 1. | -| `text` | `string` | The resulting recognized text. | -| `source_language`| `string` | Captured or auto-detected source ISO code. | -| `is_final` | `bool` | Default: `True`. Marks interim vs finalized chunks in continuous mode. | -| `confidence` | `float` | `0.0` - `1.0`. Accuracy confidence from the STT provider. | - ---- - -### Stage 3: Translated Text - -**Kafka Topic:** `text.translated` -**Event wrapper:** `TranslationEvent` -> `{ event_type: "text.translation", payload: TranslationPayload }` - -**`TranslationPayload`** -Produced by the Translation Worker (DeepL) when original text diverges from the room/listener requirements. - -| Field | Type | Description | -|---|---|---| -| `room_id` | `string` | | -| `user_id` | `string` | | -| `sequence_number`| `int` | Maintained from Stage 2. | -| `original_text` | `string` | Sent over from Stage 2. | -| `translated_text`| `string` | The targeted translated output. | -| `source_language`| `string` | ISO Code (e.g., `"en"`). | -| `target_language`| `string` | Target ISO Code (e.g., `"fr"`). | - ---- - -### Stage 4: Synthesized Audio - -**Kafka Topic:** `audio.synthesized` -**Event wrapper:** `SynthesizedAudioEvent` -> `{ event_type: "audio.synthesized", payload: SynthesizedAudioPayload }` - -**`SynthesizedAudioPayload`** -Produced by the Text-to-Speech Worker (OpenAI/Voice.ai) completing the loop. The WebSocket Egress consumer looks out for this and pipes the bytes back to the target clients. - -| Field | Type | Description | -|---|---|---| -| `room_id` | `string` | | -| `user_id` | `string` | | -| `sequence_number`| `int` | Maintained for client-side assembly ordering. | -| `audio_data` | `string` | Base64-encoded newly synthesized AI voice binary bytes. | -| `target_language`| `string` | Matching the TTS synthesis configuration. | -| `sample_rate` | `int` | Default: `16000` (Hz). | -| `encoding` | `AudioEncoding`| Default: `linear16`. | - ---- - -## Data structures -All audio data inside the `AudioChunkPayload` and `SynthesizedAudioPayload` are strictly shipped as stringified **Base64** text. -This bypasses binary limitation errors inside typical JSON-Kafka serializers keeping the system extremely fault resilient across serialization borders. Handlers are manually responsible for base64 decoding the block returning to byte arrays before delivery to the websocket streams or external TTS Providers APIs. - ---- - -## Enums - -### `AudioEncoding` - -| Value | Description | -|---|---| -| `linear16` | Standard PCM 16-bit signed, little-endian format. Required for maximal compatibility over native Browser WebSockets. | -| `opus` | Compressed format used primarily by higher-bandwidth connections if toggled active. | diff --git a/app/services/api-docs.md b/app/services/api-docs.md deleted file mode 100644 index c377ee2..0000000 --- a/app/services/api-docs.md +++ /dev/null @@ -1,91 +0,0 @@ -# FluentMeet Core Services Documentation - -> **Package Location:** `/app/services` -> **Purpose:** Core Business Logic, Kafka Workers, WebSockets, and Communications. - ---- - -## Table of Contents - -- [Overview](#overview) -- [Real-time Audio Pipeline Workers](#real-time-audio-pipeline-workers) - - [1. AudioIngestService (`audio_bridge.py`)](#1-audioingestservice-audio_bridgepy) - - [2. STTWorker (`stt_worker.py`)](#2-sttworker-stt_workerpy) - - [3. TranslationWorker (`translation_worker.py`)](#3-translationworker-translation_workerpy) - - [4. TTSWorker (`tts_worker.py`)](#4-ttsworker-tts_workerpy) -- [WebSocket Connection Management](#websocket-connection-management) - - [ConnectionManager (`connection_manager.py`)](#connectionmanager-connection_managerpy) -- [Email & Notification Services](#email--notification-services) - - [EmailProducerService (`email_producer.py`)](#emailproducerservice-email_producerpy) - - [EmailConsumerWorker (`email_consumer.py`)](#emailconsumerworker-email_consumerpy) - ---- - -## Overview - -The `app/services` package houses the heavy-lifting logic that connects FastAPI routers to external infrastructure (Kafka, Redis, Mailgun, AI Providers). - -Unlike module-specific services (e.g., `UserService` or `AuthService` which are mostly DB wrappers), the components in this package are highly asynchronous, globally utilized, and predominantly event-driven. - ---- - -## Real-time Audio Pipeline Workers - -The real-time AI audio pipeline is driven by a series of autonomous Kafka consumers (Workers) living in this package. - -### 1. AudioIngestService (`audio_bridge.py`) -- **Role:** Web-to-Kafka Bridge (Producer). -- **Behavior:** Called directly by the FastAPI WebSocket routers when binary audio frames arrive from a browser. It maintains an internal monotonic `sequence_number` per user, base64 encodes the binary PCM blob, and pushes an `AudioChunkEvent` to the **`audio.raw`** Kafka topic. - -### 2. STTWorker (`stt_worker.py`) -- **Role:** Speech-to-Text transcriber. -- **Topic Subscription:** **`audio.raw`** -- **Topic Publication:** **`text.original`** -- **Behavior:** Iterates through arriving raw audio events. Calls out to the active AI Service (`Deepgram` by default) to decode the speech. Emits a `TranscriptionEvent`. Also includes logic to mock the STT layer locally if no `DEEPGRAM_API_KEY` is present. - -### 3. TranslationWorker (`translation_worker.py`) -- **Role:** Target language resolution and translation. -- **Topic Subscription:** **`text.original`** -- **Topic Publication:** **`text.translated`** -- **Behavior:** - 1. Intercepts `final` transcriptions. - 2. Reaches into Redis via `MeetingStateService` to fetch the live roster for the `room_id`. - 3. Cultivates a unique `Set` of target listener languages present in the room. - 4. Calls `DeepL` (with an automatic `OpenAI` fallback if DeepL fails or a language is unsupported). - 5. Multi-casts loop: Publishes exactly one `TranslationEvent` per target language needed. - -### 4. TTSWorker (`tts_worker.py`) -- **Role:** Text-to-Speech synthesis. -- **Topic Subscription:** **`text.translated`** -- **Topic Publication:** **`audio.synthesized`** -- **Behavior:** Takes translated text snippets and calls an asynchronous synthesis provider (governed by the `ACTIVE_TTS_PROVIDER` setting, allowing toggling between `OpenAI` and `Voice.ai`). Emits base64 application audio frames ready for clients to ingest back over their open WebSockets. - ---- - -## WebSocket Connection Management - -### ConnectionManager (`connection_manager.py`) -- **Role:** Multi-pod scaling for WebSocket connections. -- **Behavior:** Standard FastAPI Websocket lists fail the moment you scale to 2+ pods or workers, because users in the same room might be connected to different pods. -- **Architecture (Redis Pub/Sub):** - - Maintains a local memory `dict` of active websocket clients. - - Automatically spins up an `asyncio.Task` to subscribe to a Redis channel named `ws:room:{room_code}` when the first user joins a room. - - Exposes `broadcast_to_room()` and `send_to_user()`. When called, these serialize the message and publish it to the Redis channel securely multi-casting across all active backend pods instantly. - - The internal subscriber `_listen_to_redis()` task pulls payloads off the Redis backplane and commands the local `WebSocket` items to transmit JSON back to clients. - ---- - -## Email & Notification Services - -### EmailProducerService (`email_producer.py`) -- **Role:** Non-blocking async queue offloader. -- **Topic:** **`notifications.email`** -- **Behavior:** Injected into HTTP endpoints (e.g., `POST /auth/forgot-password`). It prevents endpoints from hanging on HTTP mailer calls. It accepts subject blocks, a template name, and its dictionary context payload, emitting it into Kafka. - -### EmailConsumerWorker (`email_consumer.py`) -- **Role:** Dedicated template rendering and mailer HTTP agent. -- **Topic Subscription:** **`notifications.email`** -- **Behavior:** - 1. Pulls email requests out of the Kafka broker. - 2. Utilizes **Jinja2** to compile the injected context variables against atomic HTML files stored in the `app/templates/email/` directory. - 3. Opens an async `httpx` HTTP session against the integrated **Mailgun V3** REST API, handling authorization natively. Includes transient error trapping capable of failing out in a way that respects Kafka's natural message retry architecture. From b7ba1b50860e7bb396b67eea4cae993f2cc870ab Mon Sep 17 00:00:00 2001 From: aniebietafia Date: Thu, 14 May 2026 12:15:57 +0100 Subject: [PATCH 2/2] docs: fixing documentation and cleanup Signed-off-by: aniebietafia --- app/modules/meeting/ws_router.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/modules/meeting/ws_router.py b/app/modules/meeting/ws_router.py index 949ceef..c371a10 100644 --- a/app/modules/meeting/ws_router.py +++ b/app/modules/meeting/ws_router.py @@ -164,7 +164,10 @@ async def ingest_task() -> None: try: data = base64.b64decode(message["text"]) except Exception as exc: - logger.warning(f"Failed to decode base64 audio text frame. Skipping frame. Error: {exc}") + logger.warning( + f"Failed to decode base64 audio text frame. " + f"Skipping frame. Error: {exc}" + ) continue elif "bytes" in message and message["bytes"] is not None: data = message["bytes"]