diff --git a/.env.example b/.env.example index 7fd69f1..8dad93c 100644 --- a/.env.example +++ b/.env.example @@ -56,4 +56,66 @@ TWITCH_CLIENT_SECRET=your_twitch_client_secret_here # Défaut serveur : youtube,dailymotion,twitch,peertube,odysee,rumble # Exemple restrictif : DOWNLOAD_PROVIDERS=peertube,odysee DOWNLOAD_PROVIDERS=youtube,dailymotion,twitch,peertube,odysee,rumble +# Nombre de téléchargements simultanés (défaut serveur : 2) DOWNLOAD_MAX_CONCURRENT=2 +# Quota disque des téléchargements, en octets (défaut serveur : 5 Gio) +# DOWNLOAD_STORAGE_QUOTA_BYTES=5368709120 +# Fenêtre glissante de réinitialisation du quota, en ms (défaut : 30 jours) +# DOWNLOAD_QUOTA_WINDOW_MS=2592000000 + +# --- Gemini (résumé IA des commentaires / du transcript) --- +# La clé reste côté serveur ; le front interroge /api/ai/status. +# https://aistudio.google.com/apikey +# GEMINI_API_KEY=your_gemini_api_key_here + +# --- Runtime / session --- +PORT=4000 +NODE_ENV=production +# OBLIGATOIRE en production : secret de signature des JWT (rotations = sessions +# invalides). Générer avec : openssl rand -hex 32 +JWT_SECRET=change_me_generate_a_long_random_secret +# Durées de vie des sessions +ACCESS_TTL_MIN=15 +REFRESH_TTL_DAYS=2 +REMEMBER_TTL_DAYS=30 +# Forcer le flag Secure sur les cookies de session (auto-détecté si absent). +# À laisser à 'false' en local HTTP, 'true' derrière HTTPS. +# COOKIE_SECURE=false +# Fichier SQLite (défaut : ./db/newtube.db) +# NEWTUBE_DB_FILE=./db/newtube.db + +# --- Cache & rate limits serveur --- +# TTL du cache de recherche YouTube en ms (défaut serveur : 5 min ; 30 min en prod) +YT_CACHE_TTL_MS=1800000 +# Nombre max d'entrées en cache avant éviction +# YT_CACHE_MAX_ENTRIES=500 +# Durée de blacklist d'une clé YouTube qui échoue, en ms (défaut : 6 h) +# YT_KEY_BAN_MS=21600000 +# TTL du cache des suggestions, en ms (défaut : 5 min) +# SUGGEST_CACHE_TTL_MS=300000 +# Suggestions web (favicon/duckduckgo) : '0' pour désactiver +# SUGGEST_WEB_ENABLED=1 +# Rate limit des suggestions, requêtes par fenêtre +# SUGGEST_RATE_LIMIT=60 +# TTL du cache transcript, en ms (défaut : 24 h) +# TRANSCRIPT_CACHE_TTL=86400000 +# Rate limit du transcript, requêtes par fenêtre +# TRANSCRIPT_RATE_LIMIT=10 +# TTL du cache des fiches chaînes, en ms (défaut : 6 h) +# CHANNEL_TTL_MS=21600000 +# Timeout des fetches de contenu chaîne (ms) +# CHANNEL_FETCH_TIMEOUT_MS=15000 +# CHANNEL_CONTENT_TIMEOUT_MS=15000 +# Mémoire max du cache scrape yt-dlp, en Mo (défaut : 300) +# YT_SCRAPE_MEM_MAX=300 +# TTL du cache des vidéos recommandées, en ms (défaut : 1 h) +# YT_RELATED_TTL_MS=3600000 + +# --- Docker (uniquement pour docker-compose/.env) --- +# TZ=America/Montreal +# Hostname du conteneur (docker-compose.yml: hostname) +# NGINX_HOSTNAME=newtube-angular +# Répertoire hôte des volumes persistants (db, assets, tmp/downloads) +# DIR_NEWTUBE=/DOCKER_CONFIG/newtube-angular +# Port hôte publié (docker-compose.local.yml uniquement, défaut : 4200) +# HOST_PORT=4200 diff --git a/.gitignore b/.gitignore index 8a3b0a5..62aa5ab 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,9 @@ tmp/ # Exports Takeout extraits localement (volumineux, données personnelles) takeout-*/ takeout-*.zip + +# Env de deploiement (contient des secrets en clair) +docker-compose/.env + +# Identifiants du registre Docker (deploy-img.sh) +docker/.registry.env diff --git a/docker-compose/.env b/docker-compose/.env index c693fae..73b690c 100644 --- a/docker-compose/.env +++ b/docker-compose/.env @@ -1,16 +1,16 @@ -NGINX_HOSTNAME=AsusVivobook -NGINX_SERVER_IP=172.26.11.25 +NGINX_HOSTNAME=newtube-angular DIR_NEWTUBE=/DOCKER_CONFIG/newtube-angular GEMINI_API_KEY=AIzaSyALfJiYQb6QYUXyUM2AKd7sV1nWV0JALJw YOUTUBE_API_KEY=AIzaSyBXL5y8bRMO5WJwIhPK-fhrEPu2aROuKag YOUTUBE_API_KEYS=["AIzaSyBXL5y8bRMO5WJwIhPK-fhrEPu2aROuKag", "AIzaSyC1rV2Er1n1FHZQoGgQhOxFc3nSlQWhgks", "AIzaSyBZxgB2s1kp9qZhfGRrp69Mdf_9krwppVI"] -VIMEO_ACCESS_TOKEN=40e09bf643b2ecddf68ed0f5d1574c72 TWITCH_CLIENT_ID=ikxrqgjivwn4rurhjy2mcfkcq62y3m TWITCH_CLIENT_SECRET=kezgpj5vqrk96tu2x3ghmhwipg980p # Google OAuth (connexion + import abonnements/favoris YouTube) GOOGLE_CLIENT_ID=585708432771-in6nulj6mu0hus59m2huo7k6qsse78lq.apps.googleusercontent.com GOOGLE_CLIENT_SECRET=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 OAUTH_APP_BASE_URL=http://localhost:4200 +TWITCH_REDIRECT_URI=http://localhost:4200/api/oauth/twitch/callback +GOOGLE_REDIRECT_URI=http://localhost:4200/api/oauth/google/callback YT_CACHE_TTL_MS=1800000 JWT_SECRET=Ceci-est-mon-programme-newtube-je-sais-que-c-est-moche ACCESS_TTL_MIN=15 diff --git a/docker-compose/docker-compose.local.yml b/docker-compose/docker-compose.local.yml index 659d404..eba9058 100644 --- a/docker-compose/docker-compose.local.yml +++ b/docker-compose/docker-compose.local.yml @@ -22,7 +22,6 @@ services: - GEMINI_API_KEY=${GEMINI_API_KEY} - YOUTUBE_API_KEY=${YOUTUBE_API_KEY} - YOUTUBE_API_KEYS=${YOUTUBE_API_KEYS} - - VIMEO_ACCESS_TOKEN=${VIMEO_ACCESS_TOKEN} - TWITCH_CLIENT_ID=${TWITCH_CLIENT_ID} - TWITCH_CLIENT_SECRET=${TWITCH_CLIENT_SECRET} - TWITCH_REDIRECT_URI=${TWITCH_REDIRECT_URI} diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index dcf09a0..b1ecd3a 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -16,7 +16,6 @@ services: - GEMINI_API_KEY=${GEMINI_API_KEY} - YOUTUBE_API_KEY=${YOUTUBE_API_KEY} - YOUTUBE_API_KEYS=${YOUTUBE_API_KEYS} - - VIMEO_ACCESS_TOKEN=${VIMEO_ACCESS_TOKEN} - TWITCH_CLIENT_ID=${TWITCH_CLIENT_ID} - TWITCH_CLIENT_SECRET=${TWITCH_CLIENT_SECRET} - DOWNLOAD_PROVIDERS=${DOWNLOAD_PROVIDERS:-youtube,dailymotion,twitch,peertube,odysee,rumble} @@ -32,6 +31,11 @@ services: - YT_COOKIES_FILE=${YT_COOKIES_FILE:-} - YT_PO_TOKEN=${YT_PO_TOKEN:-} - YT_EGRESS_PROXY=${YT_EGRESS_PROXY:-} + - TWITCH_REDIRECT_URI=${TWITCH_REDIRECT_URI} + - GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID} + - GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET} + - GOOGLE_REDIRECT_URI=${GOOGLE_REDIRECT_URI} + - OAUTH_APP_BASE_URL=${OAUTH_APP_BASE_URL} - TZ=${TZ} volumes: - ${DIR_NEWTUBE}/assets:/app/assets diff --git a/docker/.registry.env.example b/docker/.registry.env.example new file mode 100644 index 0000000..3374c2e --- /dev/null +++ b/docker/.registry.env.example @@ -0,0 +1,14 @@ +# Identifiants du registre Docker distant — NE PAS VERSIONNER. +# Copiez ce fichier en `.registry.env` (ignoré par git) et remplissez les valeurs. +# cp .registry.env.example .registry.env +# +# Priorité de lecture dans deploy-img.sh : +# 1. variables d'environnement DOCKER_USERNAME / DOCKER_PASSWORD +# 2. ce fichier +# +# Sous Windows, deploy-img.ps1 exécute le script dans WSL : WSL n'hérite pas des +# variables de Windows, ce fichier est donc la voie la plus fiable. Le .ps1 +# exporte aussi les variables Windows qu'il trouve, en repli. + +DOCKER_USERNAME=votre_utilisateur +DOCKER_PASSWORD=votre_mot_de_passe diff --git a/docker/deploy-img.ps1 b/docker/deploy-img.ps1 index 34a0ecc..76fa705 100644 --- a/docker/deploy-img.ps1 +++ b/docker/deploy-img.ps1 @@ -29,7 +29,19 @@ try { wsl -d Debian -- chmod +x "$wslScriptDir/deploy-img.sh" Write-Verbose "[Étape 4] Construction de la commande Bash à exécuter." + # WSL n'hérite PAS des variables d'environnement de Windows : on les + # exporte explicitement dans la commande bash. Sans cela, un DOCKER_USERNAME + # défini dans PowerShell n'arriverait jamais dans deploy-img.sh et le push + # échouerait en "push access denied". $innerCmd = "cd '$wslScriptDir' && ./deploy-img.sh" + foreach ($var in @('DOCKER_USERNAME', 'DOCKER_PASSWORD')) { + $value = [Environment]::GetEnvironmentVariable($var) + if ($value) { + # Échappement des caractères spéciaux bash (simple quotes) + $escaped = $value -replace "'", "'\''" + $innerCmd = "export $var='$escaped'; " + $innerCmd + } + } Write-Verbose "[Étape 5] Exécution du script deploy-img.sh dans WSL Debian." wsl -d Debian bash -lc $innerCmd diff --git a/docker/deploy-img.sh b/docker/deploy-img.sh index 72ead4b..2d7f942 100644 --- a/docker/deploy-img.sh +++ b/docker/deploy-img.sh @@ -2,7 +2,7 @@ # Script: deploy-img.sh # Description: Pousse l'image Docker locale vers un registre HTTP (insecure), gère le versioning semver et la rétention # Date: 2025-04-18 -# Mise à jour: 2025-09-19 - Vérification stricte du daemon (insecure-registries), compatibilité Windows/Linux +# Mise à jour: 2026-09-28 - Lecture des identifiants depuis .registry.env, échec rapide si le registre est protégé set -euo pipefail @@ -29,6 +29,16 @@ JQ_BIN="${JQ_BIN:-jq}" # possibilité de surcharger via env # Headers pour obtenir le digest (manifest v2) ACCEPT_MANIFEST='application/vnd.docker.distribution.manifest.v2+json' +# Auth du registre. Ordre de priorité : +# 1. variables déjà exports (DOCKER_USERNAME / DOCKER_PASSWORD) +# 2. fichier .registry.env à côté du script (non versionné, cf. .gitignore) +# Le fichier permet à deploy-img.ps1 (qui exécute ce script dans WSL) de trouver +# les identifiants : WSL n'hérite pas des variables d'environnement de Windows. +# (Ce bloc est exécuté plus bas, après la définition de info/warn/error.) +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REGISTRY_ENV_FILE="${REGISTRY_ENV_FILE:-${SCRIPT_DIR}/.registry.env}" +AUTH_ARGS=() + ##################################### # UTILITAIRES # ##################################### @@ -55,6 +65,23 @@ require_cmd() { command -v "$1" >/dev/null 2>&1 || { error "Commande requise manquante: $1"; exit 1; } } +# Chargement des identifiants (voir commentaire en section CONFIG) +if [[ -f "${REGISTRY_ENV_FILE}" ]]; then + info "Lecture des identifiants depuis ${REGISTRY_ENV_FILE}" + set -a + # shellcheck disable=SC1090 + source "${REGISTRY_ENV_FILE}" + set +a +fi + +if [[ -n "${DOCKER_USERNAME:-}" && -n "${DOCKER_PASSWORD:-}" ]]; then + AUTH_ARGS=(-u "${DOCKER_USERNAME}:${DOCKER_PASSWORD}") +else + warn "Aucun identifiant registre (DOCKER_USERNAME / DOCKER_PASSWORD)." + warn "Créez ${REGISTRY_ENV_FILE} avec :" + printf '%s\n' " DOCKER_USERNAME=votre_utilisateur" " DOCKER_PASSWORD=votre_mot_de_passe" +fi + ##################################### # PRÉ-VÉRIFICATIONS # ##################################### @@ -96,20 +123,39 @@ if ! docker info 2>/dev/null \ exit 1 fi -# Ping HTTP direct du registre (API v2) +# Ping HTTP direct du registre (API v2). +# Un registre protégé répond 401 : cela prouve qu'il EST joignable, donc on +# accepte 200 comme 401 (et seulement eux). `-f` seul faisait échouer le script +# sur un registre authentifié. info "Vérification HTTP directe du registre (http://${REGISTRY}/v2/)…" -if ! ${CURL} "http://${REGISTRY}/v2/" >/dev/null 2>&1; then - error "Impossible de joindre http://${REGISTRY}/v2/. Le registre écoute-t-il bien en HTTP sur :${REGISTRY_PORT} ?" +HTTP_CODE="$(curl -s -o /dev/null -w '%{http_code}' ${AUTH_ARGS[@]+"${AUTH_ARGS[@]}"} \ + --connect-timeout 10 -m 20 "http://${REGISTRY}/v2/" 2>/dev/null || echo "000")" +if [[ "${HTTP_CODE}" != "200" && "${HTTP_CODE}" != "401" ]]; then + error "Impossible de joindre http://${REGISTRY}/v2/ (code HTTP: ${HTTP_CODE})." + error "Le registre écoute-t-il bien en HTTP sur :${REGISTRY_PORT} ?" exit 1 fi +# Un 401 sur un registre protégé SANS identifiants est bloquant : continuer +# produirait un push qui échoue en fin de course ("push access denied") après +# avoir déjà tagué l'image. On s'arrête ici avec un message actionnable. +if [[ "${HTTP_CODE}" == "401" && ${#AUTH_ARGS[@]} -eq 0 ]]; then + error "Le registre ${REGISTRY} exige une authentification et aucun identifiant n'est fourni." + error "Créez ${REGISTRY_ENV_FILE} :" + printf '%s\n' " DOCKER_USERNAME=votre_utilisateur" " DOCKER_PASSWORD=votre_mot_de_passe" + exit 1 +fi +[[ "${HTTP_CODE}" == "401" ]] && info "Registre protégé (401) : authentification avec ${DOCKER_USERNAME}." -# Auth facultative via env (DOCKER_USERNAME / DOCKER_PASSWORD) -if [[ -n "${DOCKER_USERNAME:-}" && -n "${DOCKER_PASSWORD:-}" ]]; then +# Auth auprès du daemon (utile pour le push) +if [[ ${#AUTH_ARGS[@]} -gt 0 ]]; then info "Authentification au registre (docker login)…" # NB: docker login respecte le mode HTTP si daemon configuré en insecure-registries - echo "${DOCKER_PASSWORD}" | docker login "${REGISTRY}" --username "${DOCKER_USERNAME}" --password-stdin >/dev/null + if ! echo "${DOCKER_PASSWORD}" | docker login "${REGISTRY}" --username "${DOCKER_USERNAME}" --password-stdin >/dev/null 2>&1; then + error "docker login a échoué sur ${REGISTRY}. Vérifiez DOCKER_USERNAME / DOCKER_PASSWORD." + exit 1 + fi else - info "Aucun identifiant Docker fourni (DOCKER_USERNAME/DOCKER_PASSWORD). Tentative sans authentification." + info "Aucun identifiant : le registre est ouvert, push sans authentification." fi mkdir -p "${TEMP_DIR}" @@ -121,7 +167,7 @@ get_all_tags() { # Renvoie la liste des tags (un par ligne) ou rien si vide/erreur # Utilise jq si dispo, sinon un parseur simple. local tags_json - if ! tags_json="$(${CURL} "http://${REGISTRY}/v2/${IMAGE_NAME}/tags/list" 2>/dev/null)"; then + if ! tags_json="$(${CURL} ${AUTH_ARGS[@]+"${AUTH_ARGS[@]}"} "http://${REGISTRY}/v2/${IMAGE_NAME}/tags/list" 2>/dev/null)"; then return 0 fi @@ -190,7 +236,7 @@ delete_by_tag() { local tag="$1" local digest # On demande le digest via HEAD + Accept manifest v2 - digest="$(curl -fsSI -H "Accept: ${ACCEPT_MANIFEST}" "http://${REGISTRY}/v2/${IMAGE_NAME}/manifests/${tag}" \ + digest="$(curl -fsSI ${AUTH_ARGS[@]+"${AUTH_ARGS[@]}"} -H "Accept: ${ACCEPT_MANIFEST}" "http://${REGISTRY}/v2/${IMAGE_NAME}/manifests/${tag}" \ | tr -d '\r' \ | awk -F': ' 'tolower($1)=="docker-content-digest"{print $2}' \ | tail -n1)" @@ -201,7 +247,7 @@ delete_by_tag() { info "Suppression manifest ${tag} (digest: ${digest})…" # La delete API ne renvoie rien en cas de succès (204) - if ! curl -fsS -X DELETE "http://${REGISTRY}/v2/${IMAGE_NAME}/manifests/${digest}" >/dev/null; then + if ! curl -fsS ${AUTH_ARGS[@]+"${AUTH_ARGS[@]}"} -X DELETE "http://${REGISTRY}/v2/${IMAGE_NAME}/manifests/${digest}" >/dev/null; then warn "Échec suppression manifest pour ${tag} (digest: ${digest})." fi } diff --git a/src/components/themes/themes-nav.component.html b/src/components/themes/themes-nav.component.html index cb9d1a9..5355e2a 100644 --- a/src/components/themes/themes-nav.component.html +++ b/src/components/themes/themes-nav.component.html @@ -39,18 +39,7 @@ » - - - +