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 <noreply@anthropic.com>
This commit is contained in:
chris 2026-06-27 10:13:36 -04:00
parent 95e88780ab
commit 1a1fe7044a
3 changed files with 14 additions and 11 deletions

View File

@ -406,7 +406,7 @@ document.addEventListener('DOMContentLoaded', () => {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': `Bearer ${getAdminPassword()}`, 'X-Admin-Password': getAdminPassword(),
}, },
body: JSON.stringify(updatedPhoto) body: JSON.stringify(updatedPhoto)
}); });
@ -437,7 +437,7 @@ document.addEventListener('DOMContentLoaded', () => {
try { try {
await fetch(`${backendUrl}/photos/${id}`, { await fetch(`${backendUrl}/photos/${id}`, {
method: 'DELETE', method: 'DELETE',
headers: { 'Authorization': `Bearer ${getAdminPassword()}` } headers: { 'X-Admin-Password': getAdminPassword() }
}); });
fetchPhotos(); fetchPhotos();
} catch (error) { } catch (error) {
@ -468,7 +468,7 @@ document.addEventListener('DOMContentLoaded', () => {
try { try {
await Promise.all(ids.map(id => fetch(`${backendUrl}/photos/${id}`, { await Promise.all(ids.map(id => fetch(`${backendUrl}/photos/${id}`, {
method: 'DELETE', method: 'DELETE',
headers: { 'Authorization': `Bearer ${getAdminPassword()}` } headers: { 'X-Admin-Password': getAdminPassword() }
}))); })));
clearSelection(); clearSelection();
fetchPhotos(); fetchPhotos();
@ -528,7 +528,7 @@ document.addEventListener('DOMContentLoaded', () => {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': `Bearer ${getAdminPassword()}`, 'X-Admin-Password': getAdminPassword(),
}, },
body: JSON.stringify(payload) body: JSON.stringify(payload)
}); });
@ -740,7 +740,7 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
xhr.open('POST', `${backendUrl}/photos/upload`); xhr.open('POST', `${backendUrl}/photos/upload`);
xhr.setRequestHeader('Authorization', `Bearer ${getAdminPassword()}`); xhr.setRequestHeader('X-Admin-Password', getAdminPassword());
uploadButton.classList.add('is-loading'); uploadButton.classList.add('is-loading');
uploadProgress.style.display = 'block'; uploadProgress.style.display = 'block';
@ -832,7 +832,7 @@ document.addEventListener('DOMContentLoaded', () => {
const url = idx !== '' ? `${backendUrl}/photos/presets/${idx}` : `${backendUrl}/photos/presets`; const url = idx !== '' ? `${backendUrl}/photos/presets/${idx}` : `${backendUrl}/photos/presets`;
const res = await fetch(url, { const res = await fetch(url, {
method, method,
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${getAdminPassword()}` }, headers: { 'Content-Type': 'application/json', 'X-Admin-Password': getAdminPassword() },
body: JSON.stringify({ name, tags }) body: JSON.stringify({ name, tags })
}); });
if (res.ok) { if (res.ok) {
@ -853,7 +853,7 @@ document.addEventListener('DOMContentLoaded', () => {
if (!confirm('Delete this preset?')) return; if (!confirm('Delete this preset?')) return;
const res = await fetch(`${backendUrl}/photos/presets/${idx}`, { const res = await fetch(`${backendUrl}/photos/presets/${idx}`, {
method: 'DELETE', method: 'DELETE',
headers: { 'Authorization': `Bearer ${getAdminPassword()}` } headers: { 'X-Admin-Password': getAdminPassword() }
}); });
if (res.ok) { if (res.ok) {
const data = await res.json(); const data = await res.json();
@ -1024,7 +1024,7 @@ document.addEventListener('DOMContentLoaded', () => {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': `Bearer ${getAdminPassword()}`, 'X-Admin-Password': getAdminPassword(),
}, },
body: JSON.stringify({ data }) body: JSON.stringify({ data })
}); });

View File

@ -24,8 +24,11 @@ const WATERMARK_URL = process.env.WATERMARK_URL || 'http://watermarker:8000/wate
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || ''; const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || '';
function requireAuth(req, res, next) { function requireAuth(req, res, next) {
// Accept password via custom header (proxy-safe) or legacy Authorization bearer
const custom = req.headers['x-admin-password'] || '';
const auth = req.headers['authorization'] || ''; const auth = req.headers['authorization'] || '';
const token = auth.startsWith('Bearer ') ? auth.slice(7) : ''; const bearer = auth.startsWith('Bearer ') ? auth.slice(7) : '';
const token = custom || bearer;
if (!token || !ADMIN_PASSWORD || token !== ADMIN_PASSWORD) { if (!token || !ADMIN_PASSWORD || token !== ADMIN_PASSWORD) {
return res.status(401).json({ success: false, error: 'Unauthorized' }); return res.status(401).json({ success: false, error: 'Unauthorized' });
} }

View File

@ -5,7 +5,7 @@ const mongoose = require('mongoose');
const app = express(); const app = express();
const port = process.env.PORT || 5000; 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(express.json());
app.use('/uploads', express.static('uploads')); app.use('/uploads', express.static('uploads'));