Extraction des secrets
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -10,3 +10,4 @@ content/stats/data/stats.json
|
|||||||
content/stats/images
|
content/stats/images
|
||||||
.venv
|
.venv
|
||||||
__pycache__
|
__pycache__
|
||||||
|
.env
|
||||||
|
|||||||
11
deploy.sh
11
deploy.sh
@@ -3,9 +3,16 @@ set -euo pipefail
|
|||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
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
|
# Configuration
|
||||||
DEST_USER="root"
|
DEST_USER="${DEPLOY_DEST_USER:?DEPLOY_DEST_USER manquant}"
|
||||||
DEST_HOST="server-main.home.arpa"
|
DEST_HOST="${DEPLOY_DEST_HOST:?DEPLOY_DEST_HOST manquant}"
|
||||||
DEST_DIR="/var/lib/www/richard-dern.fr/"
|
DEST_DIR="/var/lib/www/richard-dern.fr/"
|
||||||
HUGO_ENV="production"
|
HUGO_ENV="production"
|
||||||
TARGET_OWNER="caddy:caddy"
|
TARGET_OWNER="caddy:caddy"
|
||||||
|
|||||||
@@ -21,18 +21,13 @@ const fsSync = require('fs');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const https = require('https');
|
const https = require('https');
|
||||||
const readline = require('readline');
|
const readline = require('readline');
|
||||||
|
const { loadToolsConfig } = require('./lib/config');
|
||||||
|
|
||||||
const ROOT = process.cwd();
|
const ROOT = process.cwd();
|
||||||
const LEGO_ROOT = path.join(ROOT, 'content', 'collections', 'lego');
|
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 CACHE_ROOT = path.join(ROOT, 'tools', 'cache', 'rebrickable');
|
||||||
const ONE_WEEK_MS = 7 * 24 * 60 * 60 * 1000;
|
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) {
|
function slugify(input) {
|
||||||
return input
|
return input
|
||||||
.normalize('NFD')
|
.normalize('NFD')
|
||||||
@@ -222,7 +217,7 @@ async function main() {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = loadConfig();
|
const config = await loadToolsConfig();
|
||||||
const apiKey = config.rebrickable.apiKey;
|
const apiKey = config.rebrickable.apiKey;
|
||||||
const host = 'rebrickable.com';
|
const host = 'rebrickable.com';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"rebrickable": {
|
"rebrickable": {
|
||||||
"apiKey": ""
|
"apiKey": null
|
||||||
},
|
},
|
||||||
"externalLinks": {
|
"externalLinks": {
|
||||||
"cacheDir": "tools/cache",
|
"cacheDir": "tools/cache",
|
||||||
@@ -24,10 +24,10 @@
|
|||||||
"precipitationThreshold": 0.1,
|
"precipitationThreshold": 0.1,
|
||||||
"providers": {
|
"providers": {
|
||||||
"influxdb": {
|
"influxdb": {
|
||||||
"url": "",
|
"url": null,
|
||||||
"org": "Dern",
|
"org": "Dern",
|
||||||
"bucket": "weather",
|
"bucket": "weather",
|
||||||
"token": "==",
|
"token": null,
|
||||||
"windowMinutes": 60,
|
"windowMinutes": 60,
|
||||||
"precipitationThreshold": 0.1,
|
"precipitationThreshold": 0.1,
|
||||||
"sensors": {
|
"sensors": {
|
||||||
@@ -85,17 +85,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"openMeteo": {
|
"openMeteo": {
|
||||||
"latitude": ,
|
"latitude": null,
|
||||||
"longitude": ,
|
"longitude": null,
|
||||||
"timezone": "Europe/Paris",
|
"timezone": "Europe/Paris",
|
||||||
"pressureOffset": 40,
|
"pressureOffset": 40,
|
||||||
"illuminanceToLuxFactor": 126.7,
|
"illuminanceToLuxFactor": 126.7,
|
||||||
"windowMinutes": 90,
|
"windowMinutes": 90,
|
||||||
"precipitationThreshold": 0.1
|
"precipitationThreshold": 0.1
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"goaccess": {
|
"goaccess": {
|
||||||
"url": ""
|
"url": null
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
const fs = require("fs/promises");
|
const fs = require("fs/promises");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
const { loadEnv } = require("./lib/env");
|
||||||
|
|
||||||
|
loadEnv();
|
||||||
|
|
||||||
const DEFAULT_CONFIG_PATH = "tools/stats.json";
|
const DEFAULT_CONFIG_PATH = "tools/stats.json";
|
||||||
const DEFAULT_DATA_OUTPUT = "content/stats/data/stats.json";
|
const DEFAULT_DATA_OUTPUT = "content/stats/data/stats.json";
|
||||||
|
|||||||
@@ -1,20 +1,68 @@
|
|||||||
const fs = require("fs/promises");
|
const fs = require("fs/promises");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
const { loadEnv } = require("./env");
|
||||||
|
|
||||||
let cached = null;
|
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") {
|
async function loadToolsConfig(configPath = "tools/config.json") {
|
||||||
const resolved = path.resolve(configPath);
|
const resolved = path.resolve(configPath);
|
||||||
if (cached && cached.path === resolved) {
|
if (cached && cached.path === resolved) {
|
||||||
return cached.data;
|
return cached.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loadEnv();
|
||||||
const raw = await fs.readFile(resolved, "utf8");
|
const raw = await fs.readFile(resolved, "utf8");
|
||||||
const data = JSON.parse(raw);
|
const data = applyEnvOverrides(JSON.parse(raw));
|
||||||
cached = { path: resolved, data };
|
cached = { path: resolved, data };
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
applyEnvOverrides,
|
||||||
loadToolsConfig,
|
loadToolsConfig,
|
||||||
};
|
};
|
||||||
|
|||||||
33
tools/lib/env.js
Normal file
33
tools/lib/env.js
Normal file
@@ -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,
|
||||||
|
};
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
const { applyEnvOverrides } = require("../config");
|
||||||
|
const { loadEnv } = require("../env");
|
||||||
|
|
||||||
const DEFAULT_WEATHER_CONFIG = {
|
const DEFAULT_WEATHER_CONFIG = {
|
||||||
timezone: "Europe/Paris",
|
timezone: "Europe/Paris",
|
||||||
@@ -20,6 +22,8 @@ const DEFAULT_WEATHER_CONFIG = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function loadWeatherConfig(configPath = path.resolve(__dirname, "..", "..", "config.json")) {
|
function loadWeatherConfig(configPath = path.resolve(__dirname, "..", "..", "config.json")) {
|
||||||
|
loadEnv();
|
||||||
|
|
||||||
let raw = {};
|
let raw = {};
|
||||||
|
|
||||||
if (fs.existsSync(configPath)) {
|
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 = {
|
const providers = {
|
||||||
...DEFAULT_WEATHER_CONFIG.providers,
|
...DEFAULT_WEATHER_CONFIG.providers,
|
||||||
|
|||||||
@@ -19,7 +19,10 @@ function latestMonthEntry(months) {
|
|||||||
|
|
||||||
async function run({ stat }) {
|
async function run({ stat }) {
|
||||||
const toolsConfig = await loadToolsConfig();
|
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 metric = stat.metric || "hits";
|
||||||
const windowDays = Number.isFinite(stat.days) ? stat.days : 30;
|
const windowDays = Number.isFinite(stat.days) ? stat.days : 30;
|
||||||
const data = await loadData(url);
|
const data = await loadData(url);
|
||||||
|
|||||||
@@ -14,6 +14,25 @@ if TOOLS_DIR not in sys.path:
|
|||||||
sys.path.append(TOOLS_DIR)
|
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():
|
def load_config():
|
||||||
cfg_path = os.path.join(ROOT_DIR, "tools", "config.json")
|
cfg_path = os.path.join(ROOT_DIR, "tools", "config.json")
|
||||||
try:
|
try:
|
||||||
@@ -63,8 +82,13 @@ def main():
|
|||||||
public_path = payload.get("publicPath")
|
public_path = payload.get("publicPath")
|
||||||
url = payload.get("stat", {}).get("url")
|
url = payload.get("stat", {}).get("url")
|
||||||
|
|
||||||
|
load_env()
|
||||||
cfg = load_config()
|
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:
|
try:
|
||||||
data = fetch_goaccess(goaccess_url)
|
data = fetch_goaccess(goaccess_url)
|
||||||
|
|||||||
@@ -11,7 +11,10 @@ async function run({ stat, outputPath, publicPath }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const toolsConfig = await loadToolsConfig();
|
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 data = await fetchGoAccessJson(url);
|
||||||
const months = groupVisitsByMonth(data, { adjustCrawlers: true });
|
const months = groupVisitsByMonth(data, { adjustCrawlers: true });
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user