From 1a1fe7044af5091f6a2219d7aed938ac3fc0da5e Mon Sep 17 00:00:00 2001 From: chris Date: Sat, 27 Jun 2026 10:13:36 -0400 Subject: [PATCH] Fix admin gallery auth: use X-Admin-Password header instead of Authorization Bearer NPMplus proxy strips the Authorization header before it reaches the backend, causing all write operations to return 401. Switch to a custom X-Admin-Password header which passes through the proxy unmodified. - backend/routes/photos.js: requireAuth checks X-Admin-Password first, falls back to Authorization Bearer for compatibility - backend/server.js: add X-Admin-Password to CORS allowedHeaders - admin/admin.js: send X-Admin-Password instead of Authorization Bearer in all fetch calls and XHR upload requests Co-Authored-By: Claude Sonnet 4.6 --- main-site/admin/admin.js | 16 ++++++++-------- .../photo-gallery-app/backend/routes/photos.js | 7 +++++-- main-site/photo-gallery-app/backend/server.js | 2 +- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/main-site/admin/admin.js b/main-site/admin/admin.js index 002e5ef..0676fa8 100644 --- a/main-site/admin/admin.js +++ b/main-site/admin/admin.js @@ -406,7 +406,7 @@ document.addEventListener('DOMContentLoaded', () => { method: 'POST', headers: { 'Content-Type': 'application/json', - 'Authorization': `Bearer ${getAdminPassword()}`, + 'X-Admin-Password': getAdminPassword(), }, body: JSON.stringify(updatedPhoto) }); @@ -437,7 +437,7 @@ document.addEventListener('DOMContentLoaded', () => { try { await fetch(`${backendUrl}/photos/${id}`, { method: 'DELETE', - headers: { 'Authorization': `Bearer ${getAdminPassword()}` } + headers: { 'X-Admin-Password': getAdminPassword() } }); fetchPhotos(); } catch (error) { @@ -468,7 +468,7 @@ document.addEventListener('DOMContentLoaded', () => { try { await Promise.all(ids.map(id => fetch(`${backendUrl}/photos/${id}`, { method: 'DELETE', - headers: { 'Authorization': `Bearer ${getAdminPassword()}` } + headers: { 'X-Admin-Password': getAdminPassword() } }))); clearSelection(); fetchPhotos(); @@ -528,7 +528,7 @@ document.addEventListener('DOMContentLoaded', () => { method: 'POST', headers: { 'Content-Type': 'application/json', - 'Authorization': `Bearer ${getAdminPassword()}`, + 'X-Admin-Password': getAdminPassword(), }, body: JSON.stringify(payload) }); @@ -740,7 +740,7 @@ document.addEventListener('DOMContentLoaded', () => { }); xhr.open('POST', `${backendUrl}/photos/upload`); - xhr.setRequestHeader('Authorization', `Bearer ${getAdminPassword()}`); + xhr.setRequestHeader('X-Admin-Password', getAdminPassword()); uploadButton.classList.add('is-loading'); uploadProgress.style.display = 'block'; @@ -832,7 +832,7 @@ document.addEventListener('DOMContentLoaded', () => { const url = idx !== '' ? `${backendUrl}/photos/presets/${idx}` : `${backendUrl}/photos/presets`; const res = await fetch(url, { method, - headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${getAdminPassword()}` }, + headers: { 'Content-Type': 'application/json', 'X-Admin-Password': getAdminPassword() }, body: JSON.stringify({ name, tags }) }); if (res.ok) { @@ -853,7 +853,7 @@ document.addEventListener('DOMContentLoaded', () => { if (!confirm('Delete this preset?')) return; const res = await fetch(`${backendUrl}/photos/presets/${idx}`, { method: 'DELETE', - headers: { 'Authorization': `Bearer ${getAdminPassword()}` } + headers: { 'X-Admin-Password': getAdminPassword() } }); if (res.ok) { const data = await res.json(); @@ -1024,7 +1024,7 @@ document.addEventListener('DOMContentLoaded', () => { method: 'POST', headers: { 'Content-Type': 'application/json', - 'Authorization': `Bearer ${getAdminPassword()}`, + 'X-Admin-Password': getAdminPassword(), }, body: JSON.stringify({ data }) }); diff --git a/main-site/photo-gallery-app/backend/routes/photos.js b/main-site/photo-gallery-app/backend/routes/photos.js index 6dc724e..e0e89a3 100644 --- a/main-site/photo-gallery-app/backend/routes/photos.js +++ b/main-site/photo-gallery-app/backend/routes/photos.js @@ -24,8 +24,11 @@ const WATERMARK_URL = process.env.WATERMARK_URL || 'http://watermarker:8000/wate const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || ''; function requireAuth(req, res, next) { - const auth = req.headers['authorization'] || ''; - const token = auth.startsWith('Bearer ') ? auth.slice(7) : ''; + // Accept password via custom header (proxy-safe) or legacy Authorization bearer + const custom = req.headers['x-admin-password'] || ''; + const auth = req.headers['authorization'] || ''; + const bearer = auth.startsWith('Bearer ') ? auth.slice(7) : ''; + const token = custom || bearer; if (!token || !ADMIN_PASSWORD || token !== ADMIN_PASSWORD) { return res.status(401).json({ success: false, error: 'Unauthorized' }); } diff --git a/main-site/photo-gallery-app/backend/server.js b/main-site/photo-gallery-app/backend/server.js index 149e210..4587bd8 100644 --- a/main-site/photo-gallery-app/backend/server.js +++ b/main-site/photo-gallery-app/backend/server.js @@ -5,7 +5,7 @@ const mongoose = require('mongoose'); const app = express(); const port = process.env.PORT || 5000; -app.use(cors({ origin: '*', methods: ['GET', 'POST', 'PUT', 'DELETE'], allowedHeaders: ['Content-Type', 'Authorization'] })); +app.use(cors({ origin: '*', methods: ['GET', 'POST', 'PUT', 'DELETE'], allowedHeaders: ['Content-Type', 'Authorization', 'X-Admin-Password'] })); app.use(express.json()); app.use('/uploads', express.static('uploads'));