From 2dc374cc7b8218f8bb79d3bad0493f2e9441dbc8 Mon Sep 17 00:00:00 2001 From: npeter83 Date: Fri, 19 Jun 2026 13:19:06 +0200 Subject: [PATCH] feat(config): move youtube_api_key to DB (encrypted, admin-editable) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Register youtube_api_key in the sysconfig registry as a secret and resolve it from the DB-override-or-env layer in YouTubeClient (self.db is in scope). Adds a YouTube API group to the Configuration page (EN/HU/DE). Google OAuth client id/secret stay env-only for now: the authlib client is registered at module load (auth.py), and the token-refresh reads them too, so a runtime DB override needs a lazy/re-registerable OAuth client — that belongs to the install-wizard epic (first-boot OAuth config without restart). Admin-role-via-UI moves to the auth-overhaul epic (user management lives there). --- backend/app/sysconfig.py | 2 ++ backend/app/youtube/client.py | 7 ++++--- frontend/src/components/ConfigPanel.tsx | 2 +- frontend/src/i18n/locales/de/config.json | 4 +++- frontend/src/i18n/locales/en/config.json | 4 +++- frontend/src/i18n/locales/hu/config.json | 4 +++- 6 files changed, 16 insertions(+), 7 deletions(-) diff --git a/backend/app/sysconfig.py b/backend/app/sysconfig.py index 439de1a..abe117f 100644 --- a/backend/app/sysconfig.py +++ b/backend/app/sysconfig.py @@ -48,6 +48,8 @@ SPECS: tuple[ConfigSpec, ...] = ( # --- Batch sizes --- ConfigSpec("enrich_batch_size", "int", "batch", "enrich_batch_size", min=1, max=50), ConfigSpec("autotag_title_sample", "int", "batch", "autotag_title_sample", min=1, max=1_000), + # --- YouTube Data API key (secret; optional — public reads prefer it over OAuth) --- + ConfigSpec("youtube_api_key", "str", "youtube", "youtube_api_key", secret=True), ) _BY_KEY: dict[str, ConfigSpec] = {s.key: s for s in SPECS} diff --git a/backend/app/youtube/client.py b/backend/app/youtube/client.py index 4b084e8..cc7ce09 100644 --- a/backend/app/youtube/client.py +++ b/backend/app/youtube/client.py @@ -11,7 +11,7 @@ from datetime import datetime, timedelta, timezone import httpx -from app import quota +from app import quota, sysconfig from app.config import settings from app.models import User from app.security import decrypt @@ -86,8 +86,9 @@ class YouTubeClient: def _get(self, path: str, params: dict, cost: int = 1, allow_key: bool = True) -> dict: p = dict(params) headers = {} - if allow_key and settings.youtube_api_key: - p["key"] = settings.youtube_api_key + api_key = sysconfig.get_str(self.db, "youtube_api_key") + if allow_key and api_key: + p["key"] = api_key else: headers["Authorization"] = f"Bearer {self._access_token()}" resp = self._http.get(f"{API_BASE}/{path}", params=p, headers=headers) diff --git a/frontend/src/components/ConfigPanel.tsx b/frontend/src/components/ConfigPanel.tsx index ae2686a..66d1d4e 100644 --- a/frontend/src/components/ConfigPanel.tsx +++ b/frontend/src/components/ConfigPanel.tsx @@ -11,7 +11,7 @@ import Tooltip from "./Tooltip"; // applied together via one Save/Discard bar (consistent with the Settings page); nothing hits // the server until Save. Group order is fixed where known so logically related settings (e.g. // Email/SMTP) stay together. -const GROUP_ORDER = ["email", "quota", "backfill", "shorts", "batch"]; +const GROUP_ORDER = ["email", "youtube", "quota", "backfill", "shorts", "batch"]; type SaveState = "idle" | "saving" | "saved" | "error"; export default function ConfigPanel() { diff --git a/frontend/src/i18n/locales/de/config.json b/frontend/src/i18n/locales/de/config.json index 30dbd67..e9913cd 100644 --- a/frontend/src/i18n/locales/de/config.json +++ b/frontend/src/i18n/locales/de/config.json @@ -3,6 +3,7 @@ "loading": "Konfiguration wird geladen…", "groups": { "email": "E-Mail / SMTP", + "youtube": "YouTube-API", "quota": "Kontingent", "backfill": "Nachladen (Backfill)", "shorts": "Shorts-Prüfung", @@ -21,7 +22,8 @@ "shorts_probe_max_seconds": { "label": "Shorts-Prüfung — max. Dauer (s)", "hint": "Nur Videos bis zu dieser Länge werden als mögliche Shorts geprüft." }, "shorts_probe_batch": { "label": "Shorts-Prüfung — Stapelgröße", "hint": "Wie viele Videos pro Lauf klassifiziert werden." }, "enrich_batch_size": { "label": "Anreicherungs-Stapelgröße", "hint": "videos.list-IDs pro Aufruf (YouTube begrenzt auf 50)." }, - "autotag_title_sample": { "label": "Auto-Tag-Titelstichprobe", "hint": "Pro Kanal abgetastete aktuelle Videotitel zur Spracherkennung." } + "autotag_title_sample": { "label": "Auto-Tag-Titelstichprobe", "hint": "Pro Kanal abgetastete aktuelle Videotitel zur Spracherkennung." }, + "youtube_api_key": { "label": "YouTube-API-Schlüssel", "hint": "Optional. Für öffentliche Lesezugriffe (Kanäle/Videos), damit gemeinsames Nachladen nicht vom OAuth eines Nutzers abhängt. Verschlüsselt gespeichert; nur schreibbar." } }, "save": "Speichern", "saving": "Speichern…", diff --git a/frontend/src/i18n/locales/en/config.json b/frontend/src/i18n/locales/en/config.json index 8d0c903..c189cea 100644 --- a/frontend/src/i18n/locales/en/config.json +++ b/frontend/src/i18n/locales/en/config.json @@ -3,6 +3,7 @@ "loading": "Loading configuration…", "groups": { "email": "Email / SMTP", + "youtube": "YouTube API", "quota": "Quota", "backfill": "Backfill", "shorts": "Shorts probe", @@ -21,7 +22,8 @@ "shorts_probe_max_seconds": { "label": "Shorts probe — max duration (s)", "hint": "Only videos at or below this length are probed as possible Shorts." }, "shorts_probe_batch": { "label": "Shorts probe — batch size", "hint": "How many videos to classify per run." }, "enrich_batch_size": { "label": "Enrichment batch size", "hint": "videos.list ids per call (YouTube caps this at 50)." }, - "autotag_title_sample": { "label": "Auto-tag title sample", "hint": "Recent video titles sampled per channel for language detection." } + "autotag_title_sample": { "label": "Auto-tag title sample", "hint": "Recent video titles sampled per channel for language detection." }, + "youtube_api_key": { "label": "YouTube API key", "hint": "Optional. Used for public reads (channels/videos) so shared backfill doesn't depend on a user's OAuth. Stored encrypted; write-only." } }, "save": "Save", "saving": "Saving…", diff --git a/frontend/src/i18n/locales/hu/config.json b/frontend/src/i18n/locales/hu/config.json index 555d698..21db136 100644 --- a/frontend/src/i18n/locales/hu/config.json +++ b/frontend/src/i18n/locales/hu/config.json @@ -3,6 +3,7 @@ "loading": "Konfiguráció betöltése…", "groups": { "email": "E-mail / SMTP", + "youtube": "YouTube API", "quota": "Kvóta", "backfill": "Letöltés (backfill)", "shorts": "Shorts-vizsgálat", @@ -21,7 +22,8 @@ "shorts_probe_max_seconds": { "label": "Shorts-vizsgálat — max hossz (mp)", "hint": "Csak az ennél nem hosszabb videókat vizsgáljuk lehetséges Shortsként." }, "shorts_probe_batch": { "label": "Shorts-vizsgálat — kötegméret", "hint": "Futásonként ennyi videót osztályozunk." }, "enrich_batch_size": { "label": "Gazdagítási kötegméret", "hint": "videos.list azonosítók hívásonként (a YouTube 50-ben maximálja)." }, - "autotag_title_sample": { "label": "Auto-címke címmintavétel", "hint": "Csatornánként ennyi friss videócímet mintázunk a nyelvfelismeréshez." } + "autotag_title_sample": { "label": "Auto-címke címmintavétel", "hint": "Csatornánként ennyi friss videócímet mintázunk a nyelvfelismeréshez." }, + "youtube_api_key": { "label": "YouTube API-kulcs", "hint": "Opcionális. Publikus olvasásokhoz (csatornák/videók), hogy a megosztott letöltés ne függjön egy felhasználó OAuth-jától. Titkosítva tárolva; csak írható." } }, "save": "Mentés", "saving": "Mentés…",