Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 | import path from "node:path"; import { validateEnv } from "@ontrack/backend-common"; export type GpxStorageDriver = "local" | "r2"; export type GpxR2Config = { endpoint: string; region: string; bucket: string; accessKeyId: string; secretAccessKey: string; keyPrefix: string; /** Бакет для удалённых треков (soft-delete). Если не задан — удаление физически стирает файл. */ trashBucket?: string; }; export type AppConfig = { port: number; nodeEnv: string; databaseUrl: string; serviceToken: string; gpxStorageDriver: GpxStorageDriver; /** Абсолютный путь к каталогу с файлами `.gpx` (для `local`). */ gpxLocalRoot: string; /** Параметры S3-совместимого хранилища (для `r2`). */ gpxR2?: GpxR2Config; nominatimBaseUrl: string; nominatimUserAgent: string; /** Internal geo-service base URL (GPX simplification on upload). */ geoServiceUrl: string; /** Timeout (ms) for the best-effort geo-service call. */ geoTimeoutMs: number; /** Путь к JSON service-account для Firebase Admin SDK (опц.); без него — список юзеров из БД. */ firebaseServiceAccountPath?: string; /** Email супер-админов (lowercase): их нельзя удалить/разжаловать из админки. */ superAdminEmails: string[]; }; const envSchema = { type: "object", properties: { NODE_ENV: { type: "string", enum: ["development", "production", "test"], default: "development", }, PORT: { type: "string", default: "3001" }, DATABASE_URL: { type: "string", minLength: 1 }, SERVICE_TOKEN: { type: "string", minLength: 16 }, GPX_STORAGE_DRIVER: { type: "string", enum: ["local", "r2"], default: "local" }, GPX_LOCAL_ROOT: { type: "string" }, GPX_R2_ENDPOINT: { type: "string" }, GPX_R2_REGION: { type: "string", default: "auto" }, GPX_R2_BUCKET: { type: "string" }, GPX_R2_ACCESS_KEY_ID: { type: "string" }, GPX_R2_SECRET_ACCESS_KEY: { type: "string" }, GPX_R2_KEY_PREFIX: { type: "string", default: "" }, GPX_R2_TRASH_BUCKET: { type: "string" }, NOMINATIM_BASE_URL: { type: "string", default: "https://nominatim.openstreetmap.org", }, NOMINATIM_USER_AGENT: { type: "string", minLength: 1 }, GEO_SERVICE_URL: { type: "string", minLength: 1, default: "http://localhost:3003" }, GEO_TIMEOUT_MS: { type: "string", default: "5000" }, FIREBASE_SERVICE_ACCOUNT_PATH: { type: "string" }, SUPER_ADMIN_EMAILS: { type: "string", default: "se.knysh@gmail.com" }, }, required: ["DATABASE_URL", "SERVICE_TOKEN", "NOMINATIM_USER_AGENT"], additionalProperties: true, } as const; function requireEnv(value: string | undefined, name: string): string { if (!value || value.trim().length === 0) { throw new Error(`${name} is required when GPX_STORAGE_DRIVER=r2`); } return value; } export function loadConfig(env: NodeJS.ProcessEnv = process.env): AppConfig { const data = validateEnv<NodeJS.ProcessEnv & Record<string, string>>(envSchema, env); const portRaw = data.PORT ?? "3001"; const port = Number.parseInt(portRaw, 10); if (!Number.isFinite(port) || port <= 0 || port > 65535) { throw new Error(`Invalid PORT: ${String(portRaw)}`); } const gpxStorageDriver = (data.GPX_STORAGE_DRIVER ?? "local") as GpxStorageDriver; if (gpxStorageDriver !== "local" && gpxStorageDriver !== "r2") { throw new Error(`Unsupported GPX_STORAGE_DRIVER: ${String(data.GPX_STORAGE_DRIVER)}`); } if (gpxStorageDriver === "local" && (!data.GPX_LOCAL_ROOT || data.GPX_LOCAL_ROOT.trim() === "")) { throw new Error("GPX_LOCAL_ROOT is required when GPX_STORAGE_DRIVER=local"); } const gpxR2 = gpxStorageDriver === "r2" ? { endpoint: requireEnv(data.GPX_R2_ENDPOINT, "GPX_R2_ENDPOINT"), region: data.GPX_R2_REGION ?? "auto", bucket: requireEnv(data.GPX_R2_BUCKET, "GPX_R2_BUCKET"), accessKeyId: requireEnv(data.GPX_R2_ACCESS_KEY_ID, "GPX_R2_ACCESS_KEY_ID"), secretAccessKey: requireEnv(data.GPX_R2_SECRET_ACCESS_KEY, "GPX_R2_SECRET_ACCESS_KEY"), keyPrefix: data.GPX_R2_KEY_PREFIX ?? "", ...(data.GPX_R2_TRASH_BUCKET && data.GPX_R2_TRASH_BUCKET.trim().length > 0 ? { trashBucket: data.GPX_R2_TRASH_BUCKET.trim() } : {}), } : undefined; return { port, nodeEnv: data.NODE_ENV ?? "development", databaseUrl: data.DATABASE_URL ?? "", serviceToken: data.SERVICE_TOKEN ?? "", gpxStorageDriver, gpxLocalRoot: path.resolve(data.GPX_LOCAL_ROOT ?? ""), ...(gpxR2 ? { gpxR2 } : {}), nominatimBaseUrl: (data.NOMINATIM_BASE_URL ?? "https://nominatim.openstreetmap.org").replace( /\/$/, "", ), nominatimUserAgent: data.NOMINATIM_USER_AGENT ?? "", geoServiceUrl: new URL(data.GEO_SERVICE_URL ?? "http://localhost:3003") .toString() .replace(/\/$/, ""), geoTimeoutMs: parseGeoTimeout(data.GEO_TIMEOUT_MS ?? "5000"), ...(data.FIREBASE_SERVICE_ACCOUNT_PATH && data.FIREBASE_SERVICE_ACCOUNT_PATH.trim().length > 0 ? { firebaseServiceAccountPath: data.FIREBASE_SERVICE_ACCOUNT_PATH.trim() } : {}), superAdminEmails: parseEmailList(data.SUPER_ADMIN_EMAILS ?? "se.knysh@gmail.com"), }; } /** Список email через запятую → нормализованный массив (lowercase, без пустых). */ function parseEmailList(raw: string): string[] { return raw .split(",") .map((e) => e.trim().toLowerCase()) .filter((e) => e.length > 0); } function parseGeoTimeout(raw: string): number { const parsed = Number.parseInt(raw, 10); if (!Number.isFinite(parsed) || parsed < 100 || parsed > 30000) { throw new Error(`Invalid GEO_TIMEOUT_MS: ${raw}`); } return parsed; } |