From f40dfd177483591ea56c13f1e1c3771a77ec2f24 Mon Sep 17 00:00:00 2001 From: Alexander Bocken Date: Tue, 9 Dec 2025 11:35:12 +0100 Subject: [PATCH] refactor: move environment variables to runtime for secure containerized builds Change from $env/static/private to $env/dynamic/private for all environment variables. This allows building in CI without embedding secrets in build artifacts, while keeping secrets secure on the server at runtime. Changes: - Refactor auth configuration to use dynamic env vars - Move database connection string to runtime - Update image API routes to read IMAGE_DIR at runtime - Add .env.example for documentation This enables the containerized build workflow to succeed without requiring a .env file during build, as secrets are only needed when the application starts on the server. --- .env.example | 24 ++++++++++++++++++++ src/auth.ts | 8 +++---- src/hooks.server.ts | 1 - src/routes/api/cospend/upload/+server.ts | 10 ++++---- src/routes/api/rezepte/img/add/+server.ts | 8 +++---- src/routes/api/rezepte/img/delete/+server.ts | 4 ++-- src/routes/api/rezepte/img/mv/+server.ts | 6 ++--- src/utils/db.ts | 4 ++-- 8 files changed, 44 insertions(+), 21 deletions(-) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..db4b2b2 --- /dev/null +++ b/.env.example @@ -0,0 +1,24 @@ +# Database Configuration +MONGO_URL="mongodb://user:password@host:port/database?authSource=admin" + +# Authentication Secrets (runtime only - not embedded in build) +AUTHENTIK_ID="your-authentik-client-id" +AUTHENTIK_SECRET="your-authentik-client-secret" + +# Static Configuration (embedded in build - OK to be public) +AUTHENTIK_ISSUER="https://sso.example.com/application/o/your-app/" + +# File Storage +IMAGE_DIR="/path/to/static/files" + +# Optional: Development Settings +# DEV_DISABLE_AUTH="true" +# ORIGIN="http://127.0.0.1:3000" + +# Optional: Additional Configuration +# BEARER_TOKEN="your-bearer-token" +# COOKIE_SECRET="your-cookie-secret" +# PEPPER="your-pepper-value" +# ALLOW_REGISTRATION="1" +# AUTH_SECRET="your-auth-secret" +# USDA_API_KEY="your-usda-api-key" diff --git a/src/auth.ts b/src/auth.ts index c6c784b..ae6db87 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -1,13 +1,13 @@ import { SvelteKitAuth } from "@auth/sveltekit" import Authentik from "@auth/core/providers/authentik" -import { AUTHENTIK_ID, AUTHENTIK_SECRET, AUTHENTIK_ISSUER } from "$env/static/private"; +import { env } from "$env/dynamic/private"; export const { handle, signIn, signOut } = SvelteKitAuth({ providers: [ Authentik({ - clientId: AUTHENTIK_ID, - clientSecret: AUTHENTIK_SECRET, - issuer: AUTHENTIK_ISSUER, + clientId: env.AUTHENTIK_ID, + clientSecret: env.AUTHENTIK_SECRET, + issuer: env.AUTHENTIK_ISSUER, })], callbacks: { // this feels like an extremely hacky way to get nickname and groups into the session object diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 3c19186..e60cae0 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -3,7 +3,6 @@ import { redirect } from "@sveltejs/kit" import { error } from "@sveltejs/kit" import { SvelteKitAuth } from "@auth/sveltekit" import Authentik from "@auth/core/providers/authentik" -import { AUTHENTIK_ID, AUTHENTIK_SECRET, AUTHENTIK_ISSUER } from "$env/static/private"; import { sequence } from "@sveltejs/kit/hooks" import * as auth from "./auth" import { initializeScheduler } from "./lib/server/scheduler" diff --git a/src/routes/api/cospend/upload/+server.ts b/src/routes/api/cospend/upload/+server.ts index cc23f98..43f607c 100644 --- a/src/routes/api/cospend/upload/+server.ts +++ b/src/routes/api/cospend/upload/+server.ts @@ -3,7 +3,7 @@ import { error, json } from '@sveltejs/kit'; import { writeFileSync, mkdirSync } from 'fs'; import { join } from 'path'; import { randomUUID } from 'crypto'; -import { IMAGE_DIR } from '$env/static/private'; +import { env } from '$env/dynamic/private'; export const POST: RequestHandler = async ({ request, locals }) => { const auth = await locals.auth(); @@ -30,13 +30,13 @@ export const POST: RequestHandler = async ({ request, locals }) => { const extension = image.type.split('/')[1]; const filename = `${randomUUID()}.${extension}`; - - if (!IMAGE_DIR) { + + if (!env.IMAGE_DIR) { throw error(500, 'IMAGE_DIR environment variable not configured'); } - + // Ensure cospend directory exists in IMAGE_DIR - const uploadsDir = join(IMAGE_DIR, 'cospend'); + const uploadsDir = join(env.IMAGE_DIR, 'cospend'); try { mkdirSync(uploadsDir, { recursive: true }); } catch (err) { diff --git a/src/routes/api/rezepte/img/add/+server.ts b/src/routes/api/rezepte/img/add/+server.ts index 7924ac1..74af09d 100644 --- a/src/routes/api/rezepte/img/add/+server.ts +++ b/src/routes/api/rezepte/img/add/+server.ts @@ -1,7 +1,7 @@ import path from 'path' import type { RequestHandler } from '@sveltejs/kit'; import { error } from '@sveltejs/kit'; -import { IMAGE_DIR } from '$env/static/private' +import { env } from '$env/dynamic/private' import sharp from 'sharp'; export const POST = (async ({ request, locals}) => { @@ -20,21 +20,21 @@ export const POST = (async ({ request, locals}) => { //} await sharp(full_res) .toFormat('webp') - .toFile(path.join(IMAGE_DIR, + .toFile(path.join(env.IMAGE_DIR, "rezepte", "full", data.name + ".webp")) await sharp(full_res) .resize({ width: 800}) .toFormat('webp') - .toFile(path.join(IMAGE_DIR, + .toFile(path.join(env.IMAGE_DIR, "rezepte", "thumb", data.name + ".webp")) await sharp(full_res) .resize({ width: 20}) .toFormat('webp') - .toFile(path.join(IMAGE_DIR, + .toFile(path.join(env.IMAGE_DIR, "rezepte", "placeholder", data.name + ".webp")) diff --git a/src/routes/api/rezepte/img/delete/+server.ts b/src/routes/api/rezepte/img/delete/+server.ts index a98e9e8..082a91c 100644 --- a/src/routes/api/rezepte/img/delete/+server.ts +++ b/src/routes/api/rezepte/img/delete/+server.ts @@ -1,6 +1,6 @@ import path from 'path' import type { RequestHandler } from '@sveltejs/kit'; -import { IMAGE_DIR } from '$env/static/private' +import { env } from '$env/dynamic/private' import { unlink } from 'node:fs'; import { error } from '@sveltejs/kit'; @@ -10,7 +10,7 @@ export const POST = (async ({ request, locals}) => { if(!auth) throw error(401, "You need to be logged in") [ "full", "thumb", "placeholder"].forEach((folder) => { - unlink(path.join(IMAGE_DIR, "rezepte", folder, data.name + ".webp"), (e) => { + unlink(path.join(env.IMAGE_DIR, "rezepte", folder, data.name + ".webp"), (e) => { if(e) error(404, "could not delete: " + folder + "/" + data.name + ".webp" + e) }) }) diff --git a/src/routes/api/rezepte/img/mv/+server.ts b/src/routes/api/rezepte/img/mv/+server.ts index 840bcd8..57b4021 100644 --- a/src/routes/api/rezepte/img/mv/+server.ts +++ b/src/routes/api/rezepte/img/mv/+server.ts @@ -1,6 +1,6 @@ import path from 'path' import type { RequestHandler } from '@sveltejs/kit'; -import { IMAGE_DIR } from '$env/static/private' +import { env } from '$env/dynamic/private' import { rename } from 'node:fs'; import { error } from '@sveltejs/kit'; @@ -10,8 +10,8 @@ export const POST = (async ({ request, locals}) => { if(!auth ) throw error(401, "need to be logged in") [ "full", "thumb", "placeholder"].forEach((folder) => { - const old_path = path.join(IMAGE_DIR, "rezepte", folder, data.old_name + ".webp") - rename(old_path, path.join(IMAGE_DIR, "rezepte", folder, data.new_name + ".webp"), (e) => { + const old_path = path.join(env.IMAGE_DIR, "rezepte", folder, data.old_name + ".webp") + rename(old_path, path.join(env.IMAGE_DIR, "rezepte", folder, data.new_name + ".webp"), (e) => { console.log(e) if(e) throw error(500, "could not mv: " + old_path) }) diff --git a/src/utils/db.ts b/src/utils/db.ts index 783d3ac..25cd2e7 100644 --- a/src/utils/db.ts +++ b/src/utils/db.ts @@ -1,5 +1,5 @@ import mongoose from 'mongoose'; -import { MONGO_URL } from '$env/static/private'; +import { env } from '$env/dynamic/private'; let isConnected = false; @@ -17,7 +17,7 @@ export const dbConnect = async () => { socketTimeoutMS: 45000, // Close sockets after 45 seconds of inactivity }; - const connection = await mongoose.connect(MONGO_URL ?? '', options); + const connection = await mongoose.connect(env.MONGO_URL ?? '', options); isConnected = true; console.log('MongoDB connected with persistent connection');