add admin token authentication for migration script
All checks were successful
CI / update (push) Successful in 12s

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
This commit is contained in:
2026-01-02 12:13:36 +01:00
parent ccf3fd7ea2
commit 48df41f27c
3 changed files with 81 additions and 28 deletions

View File

@@ -27,11 +27,25 @@ The migration will:
## Prerequisites ## 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`) - Only runs in production (when `IMAGE_DIR=/var/lib/www/static`)
- Requires confirmation token to prevent accidental runs - Requires confirmation token to prevent accidental runs
- Backup your database before running (recommended) - 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 ## Step 1: Deploy Code Changes
Deploy the updated codebase to production. The changes include: Deploy the updated codebase to production. The changes include:
@@ -66,7 +80,41 @@ sudo nginx -t && sudo nginx -s reload
## Step 3: Run Migration ## 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 ```bash
# Get your session cookie from browser DevTools # 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"}' -d '{"confirm": "MIGRATE_IMAGES"}'
``` ```
### Option 2: Using the Shell Script ### Option 4: Using Browser (Postman, Insomnia, etc.)
```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.)
1. Make sure you're logged in to bocken.org in your browser 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` 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` 1. ✅ **Production-only**: Won't run unless `IMAGE_DIR=/var/lib/www/static`
2. ✅ **Confirmation token**: Requires `{"confirm": "MIGRATE_IMAGES"}` in request body 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) 4. ✅ **Non-destructive**: Copies files (keeps originals)
5. ✅ **Skip already migrated**: Won't re-process files that already have hashes 5. ✅ **Skip already migrated**: Won't re-process files that already have hashes
6. ✅ **Detailed logging**: Returns detailed report of what was changed 6. ✅ **Detailed logging**: Returns detailed report of what was changed

View File

@@ -5,7 +5,7 @@
# It will: # It will:
# 1. Find all images without hashes (shortname.webp) # 1. Find all images without hashes (shortname.webp)
# 2. Generate content hashes for them # 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 # 4. Update the database
set -e set -e
@@ -15,10 +15,24 @@ echo "Image Hash Migration Script"
echo "======================================" echo "======================================"
echo "" echo ""
echo "This will migrate all existing images to use content-based hashes." 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 ""
echo "⚠️ WARNING: This operation will rename files on disk!" echo "⚠️ WARNING: This operation will modify the database and create new files!"
echo "" 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 read -p "Are you sure you want to continue? (yes/no): " confirm
if [ "$confirm" != "yes" ]; then if [ "$confirm" != "yes" ]; then
@@ -31,17 +45,16 @@ echo "Starting migration..."
echo "" echo ""
# Get the production URL (modify this to your production URL) # 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 \ response=$(curl -s -X POST \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-H "Cookie: $(cat .prod-session-cookie 2>/dev/null || echo '')" \ -d "{\"confirm\": \"MIGRATE_IMAGES\", \"adminToken\": \"$ADMIN_SECRET_TOKEN\"}" \
-d '{"confirm": "MIGRATE_IMAGES"}' \
"${PROD_URL}/api/admin/migrate-image-hashes") "${PROD_URL}/api/admin/migrate-image-hashes")
# Pretty print the response # Pretty print the response
echo "$response" | jq '.' echo "$response" | jq '.' 2>/dev/null || echo "$response"
echo "" echo ""
echo "======================================" echo "======================================"

View File

@@ -1,6 +1,7 @@
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
import { error } from '@sveltejs/kit'; import { error } from '@sveltejs/kit';
import { IMAGE_DIR } from '$env/static/private'; import { IMAGE_DIR } from '$env/static/private';
import { env } from '$env/dynamic/private';
import { Recipe } from '$models/Recipe'; import { Recipe } from '$models/Recipe';
import { connectDB } from '$utils/db'; import { connectDB } from '$utils/db';
import { generateImageHash, getHashedFilename } from '$utils/imageHash'; import { generateImageHash, getHashedFilename } from '$utils/imageHash';
@@ -15,6 +16,7 @@ export const POST = (async ({ locals, request }) => {
// Require confirmation token to prevent accidental runs // Require confirmation token to prevent accidental runs
const data = await request.json(); const data = await request.json();
const confirmToken = data?.confirm; const confirmToken = data?.confirm;
const adminToken = data?.adminToken;
if (!isProd) { if (!isProd) {
throw error(403, 'This endpoint only runs in production (IMAGE_DIR must be /var/lib/www)'); 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"}'); 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(); 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(); await connectDB();