# 07 — API Server (.NET) Map > Surface: `apps/api-server` (`D3ROVoice.Api`), tests in `apps/api-server.Tests` > Stack: ASP.NET Core 10 + EF Core + SQLite > Role: AI proxy (LLM/STT) + admin back-office backend for the .NET identity side --- ## 1. Composition root (`Program.cs`) - **DI:** `AddControllers`, `AddHttpClient` (proxies), Swagger (`v1`), rate limiter policy `auth` (fixed window 10/min per IP, 429), `AddDbContext` (SQLite; `DATA_DIR`/`DB_PATH`), scoped `IAuthService`, `ILlmProxyService`, `ISttProxyService`, `IAdminOperationService`. - **Auth:** JWT Bearer HS256; startup hard-fails unless `JWT_SECRET` (≥32 bytes), `JWT_ISSUER`, `JWT_AUDIENCE` are set. Zero clock skew. - **Policies:** `ManagerOrAbove` (manager/admin/superadmin), `AdminOrAbove` (admin/superadmin), `SuperAdminOnly` (superadmin). Role normalization strips `_`/`-`, lowercases. - **CORS/Hosts:** strict origin validation (`CORS_ALLOWED_ORIGINS`), `ALLOWED_HOSTS` required outside Development. - **Startup DB init:** `EnsureCreated()`, raw `CREATE TABLE IF NOT EXISTS` for admin operation/audit tables, legacy SHA-256 password lockdown (`IsActive=false`, `Role="LegacyDisabled"`), idempotent env admin provisioning (`ADMIN_EMAIL`/`ADMIN_PASSWORD`, only when no active user), default LLM/STT endpoints seeded. - **Middleware order:** Swagger (dev) → CORS → invite-page hardening (CSP/no-store) → default files → mobile/legacy asset block (404 for `.apk`/`.aab`/signed zips + legacy bundles) → static files → rate limiter → authentication → authorization. - **Health:** `GET /health`, `GET /api/health` → `{status, service, version, uptimeSeconds, database, timestamp}`. - **Fallbacks:** `/accept-invite` → `accept-invite.html`; `/admin/{*path}` → legacy embedded admin SPA in `wwwroot/admin`. --- ## 2. Controllers & routes ### `AuthController` (`/api/auth`) | Route | Auth | Behavior | |---|---|---| | `POST /register` | anonymous + `X-D3RO-Bootstrap-Token` | Rate-limited, ≤16KB; requires `ADMIN_BOOTSTRAP_TOKEN`; fixed-time compare; `409` if registered | | `POST /login` | anonymous | Rate-limited; returns JWT; `401` invalid | | `GET /me` | `[Authorize]` | `UserInfoDto` or `404` | ### `LlmController` (`/api/llm`, class `[Authorize]`) | Route | Behavior | |---|---| | `POST /generate` | Requires prompt; `GenerateAsync` | | `POST /chat` | `ChatAsync` (last message = prompt) | ### `SttController` (`/api/stt`, class `[Authorize]`) | Route | Auth | Behavior | |---|---|---| | `POST /transcribe` | any auth | **Always `410 Gone` `stt_edge_gateway_required`** — user transcription is Edge-only | | `POST /internal/transcribe` | anonymous + `X-D3RO-STT-Gateway-Token` | multipart, ≤26MB; requires `D3RO_API_TOKEN`; fixed-time compare; provider orchestration; error map `503/400/502` | | `GET /providers` | ManagerOrAbove | List STT endpoints | | `POST /test?endpointId=` | ManagerOrAbove | Test endpoint | ### `AdminController` (`/api/admin`, class `ManagerOrAbove`) All mutations flow through `ExecuteAdminMutationAsync` (idempotent + audited). | Route | Effective policy | |---|---| | `GET /stats` | Manager+ | | `GET /users` | Manager+ | | `GET/POST/PUT/DELETE /endpoints[/{id}]` | Manager+ read; Admin+ write | | `GET /stt-endpoints`, `/stt-endpoints/{id}` | Manager+ | | `POST/PUT/DELETE /stt-endpoints[/{id}]`, `POST .../set-default` | Admin+ | | `POST /stt-endpoints/{id}/test` | Manager+ | | `POST /stt-endpoints/test-direct` | Admin+ | | `GET /stt-usage`, `GET /usage` | Manager+ | | `POST /license-audit` | **SuperAdminOnly** | --- ## 3. Services | Service | Purpose | |---|---| | `AuthService` | Register (one-time SuperAdmin bootstrap only, `PasswordHasher`, serialized), login (timing-safe dummy verify, inactive rejection, rehash), JWT gen (8h, claims id/email/role) | | `LlmProxyService` | Resolve endpoint by model → fallback → Mock echo; OpenAI-style POST; parse content+usage; token cost → `ApiUsageLog`; errors → `ServerErrorLog` + fallback text | | `SttProxyService` (~1182 lines) | Provider adapters (groq/openai/custom, deepgram, google, assemblyai, azure, local-sidecar), candidate resolution + fallback, content-type/duration detection, `SttUsageLog` cost, synthetic-tone endpoint test, CRUD with exclusive default, usage report | | `AdminOperationService` | Validates actor/idempotency key/memo, SHA-256 request hash, `Serializable` transaction, idempotent replay, before/after audit entry | --- ## 4. Data (`Data/AppDbContext.cs`) | DbSet / table | Key fields | |---|---| | `Users` | Id, unique Email, PasswordHash, Role (default "User"), IsActive, CreatedAt, LastLoginAt | | `ModelEndpoints` | Id, unique ModelId, ModelName, Provider, EndpointUrl, ApiKey, per-1k costs, IsActive | | `UsageLogs` | UserId/Email, model, tokens, cost, duration, status | | `ErrorLogs` | ErrorType, Message, StackTrace, Endpoint | | `SttProviderEndpoints` | Name, ProviderType, URL, ApiKey, ModelId, Method, Language, Prompt, Temperature, per-minute/second cost, IsDefault, FallbackPriority, ExtraHeadersJson | | `SttUsageLogs` | User, endpoint, provider, duration, cost, latency, status, transcript preview | | `AdminOperationRequests` | ActorEmail + IdempotencyKey (unique), Operation, RequestHash, ResponseJson | | `AdminAuditEntries` | ActorEmail, Action, TargetType/Id, BeforeJson, AfterJson, Memo, IdempotencyKey | DTOs (`Dtos/Dtos.cs`): auth, license audit, LLM, admin/model endpoints, STT (transcribe, endpoints, test, usage reports). --- ## 5. Tests (`apps/api-server.Tests/`, xUnit) `AdminAuthorizationE2ETests`, `AdminOperationServiceTests`, `AuthBootstrapControllerTests`, `AuthSecurityTests`, `SttControllerSecurityTests`, `SttFailClosedTests`, `SttGatewayAuthorizationE2ETests` (26/26 per SSOT). --- ## 6. API server status summary - Auth, login, JWT, role policies, rate limiting, CORS, host restrictions: **implemented**. - LLM proxy: **implemented** (with local Mock echo fallback for standalone testing). - STT proxy: **implemented** for internal gateway; public `/transcribe` intentionally `410` (Edge-only by design). - Admin API: users, endpoints (LLM + STT), usage, license audit: **implemented** with idempotency + audit. - Known intentional states (not bugs): - Legacy SHA-256 users force-disabled. - No hardcoded/seeded admin credentials. - Mobile/legacy release assets blocked with 404. - `LlmProxyService` Mock fallback. - This backend holds a **separate identity** from Supabase; see `11-gap-backlog.md` `ID-01`.