From 8908c7f6cf6df523bef95df27316efa10044ca34 Mon Sep 17 00:00:00 2001 From: Richard Dern Date: Fri, 28 Nov 2025 13:40:20 +0100 Subject: [PATCH] Extraction des secrets --- .gitignore | 3 +- deploy.sh | 11 +++++- tools/add_lego.js | 9 +---- tools/config.json | 22 +++++------ tools/generate_stats.js | 3 ++ tools/lib/config.js | 50 +++++++++++++++++++++++- tools/lib/env.js | 33 ++++++++++++++++ tools/lib/weather/config.js | 7 +++- tools/stats/goaccess_monthly.js | 5 ++- tools/stats/top_requests.py | 26 +++++++++++- tools/stats/unique_visitors_per_month.js | 5 ++- 11 files changed, 148 insertions(+), 26 deletions(-) create mode 100644 tools/lib/env.js diff --git a/.gitignore b/.gitignore index 601ae13e..b04e82d9 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ data/stats.json content/stats/data/stats.json content/stats/images .venv -__pycache__ \ No newline at end of file +__pycache__ +.env diff --git a/deploy.sh b/deploy.sh index e03e30e4..29bcf8b0 100755 --- a/deploy.sh +++ b/deploy.sh @@ -3,9 +3,16 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +if [ -f "$SCRIPT_DIR/.env" ]; then + # Export all variables from .env for the duration of the script + set -a + . "$SCRIPT_DIR/.env" + set +a +fi + # Configuration -DEST_USER="root" -DEST_HOST="server-main.home.arpa" +DEST_USER="${DEPLOY_DEST_USER:?DEPLOY_DEST_USER manquant}" +DEST_HOST="${DEPLOY_DEST_HOST:?DEPLOY_DEST_HOST manquant}" DEST_DIR="/var/lib/www/richard-dern.fr/" HUGO_ENV="production" TARGET_OWNER="caddy:caddy" diff --git a/tools/add_lego.js b/tools/add_lego.js index 2ef1d36c..0787b24e 100644 --- a/tools/add_lego.js +++ b/tools/add_lego.js @@ -21,18 +21,13 @@ const fsSync = require('fs'); const path = require('path'); const https = require('https'); const readline = require('readline'); +const { loadToolsConfig } = require('./lib/config'); const ROOT = process.cwd(); const LEGO_ROOT = path.join(ROOT, 'content', 'collections', 'lego'); -const CONFIG_PATH = path.join(ROOT, 'tools', 'config.json'); const CACHE_ROOT = path.join(ROOT, 'tools', 'cache', 'rebrickable'); const ONE_WEEK_MS = 7 * 24 * 60 * 60 * 1000; -function loadConfig() { - const raw = fsSync.readFileSync(CONFIG_PATH, 'utf8'); - return JSON.parse(raw); -} - function slugify(input) { return input .normalize('NFD') @@ -222,7 +217,7 @@ async function main() { process.exit(1); } - const config = loadConfig(); + const config = await loadToolsConfig(); const apiKey = config.rebrickable.apiKey; const host = 'rebrickable.com'; diff --git a/tools/config.json b/tools/config.json index 17d439e5..5feb94fa 100644 --- a/tools/config.json +++ b/tools/config.json @@ -1,6 +1,6 @@ { "rebrickable": { - "apiKey": "" + "apiKey": null }, "externalLinks": { "cacheDir": "tools/cache", @@ -24,10 +24,10 @@ "precipitationThreshold": 0.1, "providers": { "influxdb": { - "url": "", + "url": null, "org": "Dern", "bucket": "weather", - "token": "==", + "token": null, "windowMinutes": 60, "precipitationThreshold": 0.1, "sensors": { @@ -85,17 +85,17 @@ } }, "openMeteo": { - "latitude": , - "longitude": , + "latitude": null, + "longitude": null, "timezone": "Europe/Paris", - "pressureOffset": 40, - "illuminanceToLuxFactor": 126.7, - "windowMinutes": 90, - "precipitationThreshold": 0.1 + "pressureOffset": 40, + "illuminanceToLuxFactor": 126.7, + "windowMinutes": 90, + "precipitationThreshold": 0.1 + } } }, "goaccess": { - "url": "" + "url": null } } -} diff --git a/tools/generate_stats.js b/tools/generate_stats.js index 28ca43ef..d4854c69 100644 --- a/tools/generate_stats.js +++ b/tools/generate_stats.js @@ -2,6 +2,9 @@ const fs = require("fs/promises"); const path = require("path"); +const { loadEnv } = require("./lib/env"); + +loadEnv(); const DEFAULT_CONFIG_PATH = "tools/stats.json"; const DEFAULT_DATA_OUTPUT = "content/stats/data/stats.json"; diff --git a/tools/lib/config.js b/tools/lib/config.js index 9c03f2f6..c6a5e8f5 100644 --- a/tools/lib/config.js +++ b/tools/lib/config.js @@ -1,20 +1,68 @@ const fs = require("fs/promises"); const path = require("path"); +const { loadEnv } = require("./env"); let cached = null; +function parseNumber(value) { + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : null; +} + +function applyEnvOverrides(config = {}) { + const merged = { ...config }; + + merged.rebrickable = { ...(config.rebrickable || {}) }; + if (process.env.REBRICKABLE_API_KEY) { + merged.rebrickable.apiKey = process.env.REBRICKABLE_API_KEY; + } + + const weather = config.weather || {}; + const providers = weather.providers || {}; + + merged.weather = { ...weather, providers: { ...providers } }; + merged.weather.providers.influxdb = { ...(providers.influxdb || {}) }; + merged.weather.providers.openMeteo = { ...(providers.openMeteo || {}) }; + + if (process.env.WEATHER_INFLUXDB_URL) { + merged.weather.providers.influxdb.url = process.env.WEATHER_INFLUXDB_URL; + } + if (process.env.WEATHER_INFLUXDB_TOKEN) { + merged.weather.providers.influxdb.token = process.env.WEATHER_INFLUXDB_TOKEN; + } + + const envLatitude = parseNumber(process.env.WEATHER_OPEN_METEO_LATITUDE); + if (envLatitude !== null) { + merged.weather.providers.openMeteo.latitude = envLatitude; + } + + const envLongitude = parseNumber(process.env.WEATHER_OPEN_METEO_LONGITUDE); + if (envLongitude !== null) { + merged.weather.providers.openMeteo.longitude = envLongitude; + } + + merged.goaccess = { ...(config.goaccess || {}) }; + if (process.env.GOACCESS_URL) { + merged.goaccess.url = process.env.GOACCESS_URL; + } + + return merged; +} + async function loadToolsConfig(configPath = "tools/config.json") { const resolved = path.resolve(configPath); if (cached && cached.path === resolved) { return cached.data; } + loadEnv(); const raw = await fs.readFile(resolved, "utf8"); - const data = JSON.parse(raw); + const data = applyEnvOverrides(JSON.parse(raw)); cached = { path: resolved, data }; return data; } module.exports = { + applyEnvOverrides, loadToolsConfig, }; diff --git a/tools/lib/env.js b/tools/lib/env.js new file mode 100644 index 00000000..eb0bf5dc --- /dev/null +++ b/tools/lib/env.js @@ -0,0 +1,33 @@ +const fs = require("fs"); +const path = require("path"); + +let envLoaded = false; + +function loadEnv(envPath = path.resolve(__dirname, "..", "..", ".env")) { + if (envLoaded) return; + envLoaded = true; + + if (!fs.existsSync(envPath)) return; + + const content = fs.readFileSync(envPath, "utf8"); + const lines = content.split(/\r?\n/); + + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) continue; + + const separator = trimmed.indexOf("="); + if (separator === -1) continue; + + const key = trimmed.slice(0, separator).trim(); + const value = trimmed.slice(separator + 1); + + if (!key || process.env[key] !== undefined) continue; + + process.env[key] = value; + } +} + +module.exports = { + loadEnv, +}; diff --git a/tools/lib/weather/config.js b/tools/lib/weather/config.js index 06ebce57..0e7cc37a 100644 --- a/tools/lib/weather/config.js +++ b/tools/lib/weather/config.js @@ -1,5 +1,7 @@ const fs = require("fs"); const path = require("path"); +const { applyEnvOverrides } = require("../config"); +const { loadEnv } = require("../env"); const DEFAULT_WEATHER_CONFIG = { timezone: "Europe/Paris", @@ -20,6 +22,8 @@ const DEFAULT_WEATHER_CONFIG = { }; function loadWeatherConfig(configPath = path.resolve(__dirname, "..", "..", "config.json")) { + loadEnv(); + let raw = {}; if (fs.existsSync(configPath)) { @@ -30,7 +34,8 @@ function loadWeatherConfig(configPath = path.resolve(__dirname, "..", "..", "con } } - const weather = raw.weather || {}; + const withEnv = applyEnvOverrides(raw); + const weather = withEnv.weather || {}; const providers = { ...DEFAULT_WEATHER_CONFIG.providers, diff --git a/tools/stats/goaccess_monthly.js b/tools/stats/goaccess_monthly.js index aff8857a..f48d954e 100644 --- a/tools/stats/goaccess_monthly.js +++ b/tools/stats/goaccess_monthly.js @@ -19,7 +19,10 @@ function latestMonthEntry(months) { async function run({ stat }) { const toolsConfig = await loadToolsConfig(); - const url = stat.url || toolsConfig.goaccess?.url || ""; + const url = stat.url || toolsConfig.goaccess?.url; + if (!url) { + throw new Error("URL GoAccess manquante (GOACCESS_URL ou goaccess.url dans tools/config.json)"); + } const metric = stat.metric || "hits"; const windowDays = Number.isFinite(stat.days) ? stat.days : 30; const data = await loadData(url); diff --git a/tools/stats/top_requests.py b/tools/stats/top_requests.py index 3439a951..0c686876 100644 --- a/tools/stats/top_requests.py +++ b/tools/stats/top_requests.py @@ -14,6 +14,25 @@ if TOOLS_DIR not in sys.path: sys.path.append(TOOLS_DIR) +def load_env(env_path=None): + path = env_path or os.path.join(ROOT_DIR, ".env") + if not os.path.exists(path): + return + try: + with open(path, "r", encoding="utf-8") as handle: + for line in handle: + stripped = line.strip() + if not stripped or stripped.startswith("#") or "=" not in stripped: + continue + key, value = stripped.split("=", 1) + key = key.strip() + if not key or key in os.environ: + continue + os.environ[key] = value + except Exception as exc: # noqa: BLE001 + print(f"Failed to load .env: {exc}", file=sys.stderr) + + def load_config(): cfg_path = os.path.join(ROOT_DIR, "tools", "config.json") try: @@ -63,8 +82,13 @@ def main(): public_path = payload.get("publicPath") url = payload.get("stat", {}).get("url") + load_env() cfg = load_config() - goaccess_url = url or (cfg.get("goaccess") or {}).get("url") or "" + goaccess_url = url or os.environ.get("GOACCESS_URL") or (cfg.get("goaccess") or {}).get("url") + + if not goaccess_url: + print("Missing GoAccess URL (set GOACCESS_URL or goaccess.url in tools/config.json)", file=sys.stderr) + sys.exit(1) try: data = fetch_goaccess(goaccess_url) diff --git a/tools/stats/unique_visitors_per_month.js b/tools/stats/unique_visitors_per_month.js index 40d01808..a0984a6b 100644 --- a/tools/stats/unique_visitors_per_month.js +++ b/tools/stats/unique_visitors_per_month.js @@ -11,7 +11,10 @@ async function run({ stat, outputPath, publicPath }) { } const toolsConfig = await loadToolsConfig(); - const url = stat.url || toolsConfig.goaccess?.url || ""; + const url = stat.url || toolsConfig.goaccess?.url; + if (!url) { + throw new Error("URL GoAccess manquante (GOACCESS_URL ou goaccess.url dans tools/config.json)"); + } const data = await fetchGoAccessJson(url); const months = groupVisitsByMonth(data, { adjustCrawlers: true });