From 48df41f27cac53eb953ba69ee40cfb5fbbe129cf Mon Sep 17 00:00:00 2001 From: Alexander Bocken Date: Fri, 2 Jan 2026 12:13:36 +0100 Subject: [PATCH] add admin token authentication for migration script Allow migration to run without browser session by using ADMIN_SECRET_TOKEN environment variable. This enables running the migration directly on the server via SSH. Changes: - Add ADMIN_SECRET_TOKEN support to migration endpoint - Update shell script to read token from environment - Improve script with better error handling and token validation - Update documentation with admin token setup instructions The endpoint now accepts authentication via either: - Valid user session (browser-based) - ADMIN_SECRET_TOKEN from environment (server-based) Usage on server: source .env && ./scripts/migrate-image-hashes.sh --- MIGRATION_IMAGE_HASHES.md | 71 ++++++++++++++----- scripts/migrate-image-hashes.sh | 29 +++++--- .../api/admin/migrate-image-hashes/+server.ts | 9 ++- 3 files changed, 81 insertions(+), 28 deletions(-) diff --git a/MIGRATION_IMAGE_HASHES.md b/MIGRATION_IMAGE_HASHES.md index 48dfb3d..38b5589 100644 --- a/MIGRATION_IMAGE_HASHES.md +++ b/MIGRATION_IMAGE_HASHES.md @@ -27,11 +27,25 @@ The migration will: ## Prerequisites -- Must be logged in as admin +- **Authentication**: Either be logged in as admin OR have `ADMIN_SECRET_TOKEN` set - Only runs in production (when `IMAGE_DIR=/var/lib/www/static`) - Requires confirmation token to prevent accidental runs - Backup your database before running (recommended) +### Setting Up Admin Token (Production Server) + +Add `ADMIN_SECRET_TOKEN` to your production `.env` file: + +```bash +# Generate a secure random token +openssl rand -hex 32 + +# Add to .env (production only!) +echo "ADMIN_SECRET_TOKEN=your-generated-token-here" >> .env +``` + +**Important**: Keep this token secret and only set it on the production server. Do NOT commit it to git. + ## Step 1: Deploy Code Changes Deploy the updated codebase to production. The changes include: @@ -66,7 +80,41 @@ sudo nginx -t && sudo nginx -s reload ## Step 3: Run Migration -### Option 1: Using curl (Recommended) +### Option 1: Using Shell Script (Recommended for Server) + +SSH into your production server and run: + +```bash +cd /path/to/homepage + +# Source your .env to get ADMIN_SECRET_TOKEN +source .env + +# Make script executable (first time only) +chmod +x scripts/migrate-image-hashes.sh + +# Run migration +./scripts/migrate-image-hashes.sh +``` + +The script will: +- Check that `ADMIN_SECRET_TOKEN` is set +- Ask for confirmation +- Call the API endpoint with the admin token +- Pretty-print the results + +### Option 2: Using curl with Admin Token + +```bash +# On production server with .env sourced +source .env + +curl -X POST https://bocken.org/api/admin/migrate-image-hashes \ + -H "Content-Type: application/json" \ + -d "{\"confirm\": \"MIGRATE_IMAGES\", \"adminToken\": \"$ADMIN_SECRET_TOKEN\"}" +``` + +### Option 3: Using curl with Session Cookie (Browser) ```bash # Get your session cookie from browser DevTools @@ -78,22 +126,7 @@ curl -X POST https://bocken.org/api/admin/migrate-image-hashes \ -d '{"confirm": "MIGRATE_IMAGES"}' ``` -### Option 2: Using the Shell Script - -```bash -cd /path/to/homepage - -# Save your session cookie to a file (from browser DevTools) -echo "your-session-cookie-value" > .prod-session-cookie - -# Make script executable -chmod +x scripts/migrate-image-hashes.sh - -# Run migration -./scripts/migrate-image-hashes.sh -``` - -### Option 3: Using Browser (Postman, Insomnia, etc.) +### Option 4: Using Browser (Postman, Insomnia, etc.) 1. Make sure you're logged in to bocken.org in your browser 2. Send POST request to: `https://bocken.org/api/admin/migrate-image-hashes` @@ -186,7 +219,7 @@ If something goes wrong: 1. ✅ **Production-only**: Won't run unless `IMAGE_DIR=/var/lib/www/static` 2. ✅ **Confirmation token**: Requires `{"confirm": "MIGRATE_IMAGES"}` in request body -3. ✅ **Authentication**: Requires logged-in user +3. ✅ **Authentication**: Requires either logged-in user OR valid `ADMIN_SECRET_TOKEN` 4. ✅ **Non-destructive**: Copies files (keeps originals) 5. ✅ **Skip already migrated**: Won't re-process files that already have hashes 6. ✅ **Detailed logging**: Returns detailed report of what was changed diff --git a/scripts/migrate-image-hashes.sh b/scripts/migrate-image-hashes.sh index ac28cac..3dc670d 100644 --- a/scripts/migrate-image-hashes.sh +++ b/scripts/migrate-image-hashes.sh @@ -5,7 +5,7 @@ # It will: # 1. Find all images without hashes (shortname.webp) # 2. Generate content hashes for them -# 3. Rename them to shortname.{hash}.webp +# 3. Copy them to shortname.{hash}.webp (keeps originals) # 4. Update the database set -e @@ -15,10 +15,24 @@ echo "Image Hash Migration Script" echo "======================================" echo "" echo "This will migrate all existing images to use content-based hashes." -echo "Images will be renamed from 'shortname.webp' to 'shortname.{hash}.webp'" +echo "Images will be copied from 'shortname.webp' to 'shortname.{hash}.webp'" +echo "Original unhashed files will be kept for graceful degradation." echo "" -echo "⚠️ WARNING: This operation will rename files on disk!" +echo "⚠️ WARNING: This operation will modify the database and create new files!" echo "" + +# Check for admin token +if [ -z "$ADMIN_SECRET_TOKEN" ]; then + echo "Error: ADMIN_SECRET_TOKEN environment variable not set." + echo "" + echo "Please set it first:" + echo " export ADMIN_SECRET_TOKEN='your-secret-token'" + echo "" + echo "Or source your .env file:" + echo " source .env" + exit 1 +fi + read -p "Are you sure you want to continue? (yes/no): " confirm if [ "$confirm" != "yes" ]; then @@ -31,17 +45,16 @@ echo "Starting migration..." echo "" # Get the production URL (modify this to your production URL) -PROD_URL="https://bocken.org" +PROD_URL="${PROD_URL:-https://bocken.org}" -# Make the API call +# Make the API call with admin token response=$(curl -s -X POST \ -H "Content-Type: application/json" \ - -H "Cookie: $(cat .prod-session-cookie 2>/dev/null || echo '')" \ - -d '{"confirm": "MIGRATE_IMAGES"}' \ + -d "{\"confirm\": \"MIGRATE_IMAGES\", \"adminToken\": \"$ADMIN_SECRET_TOKEN\"}" \ "${PROD_URL}/api/admin/migrate-image-hashes") # Pretty print the response -echo "$response" | jq '.' +echo "$response" | jq '.' 2>/dev/null || echo "$response" echo "" echo "======================================" diff --git a/src/routes/api/admin/migrate-image-hashes/+server.ts b/src/routes/api/admin/migrate-image-hashes/+server.ts index ea95bfb..6709c3f 100644 --- a/src/routes/api/admin/migrate-image-hashes/+server.ts +++ b/src/routes/api/admin/migrate-image-hashes/+server.ts @@ -1,6 +1,7 @@ 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 { Recipe } from '$models/Recipe'; import { connectDB } from '$utils/db'; import { generateImageHash, getHashedFilename } from '$utils/imageHash'; @@ -15,6 +16,7 @@ export const POST = (async ({ locals, request }) => { // Require confirmation token to prevent accidental runs const data = await request.json(); const confirmToken = data?.confirm; + const adminToken = data?.adminToken; if (!isProd) { throw error(403, 'This endpoint only runs in production (IMAGE_DIR must be /var/lib/www)'); @@ -24,8 +26,13 @@ export const POST = (async ({ locals, request }) => { throw error(400, 'Missing or invalid confirmation token. Send {"confirm": "MIGRATE_IMAGES"}'); } + // Check authentication: either valid session OR admin token from env const auth = await locals.auth(); - if (!auth) throw error(401, 'Need to be logged in'); + const isAdminToken = adminToken && env.ADMIN_SECRET_TOKEN && adminToken === env.ADMIN_SECRET_TOKEN; + + if (!auth && !isAdminToken) { + throw error(401, 'Need to be logged in or provide valid admin token'); + } await connectDB();