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',
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 })
});

View File

@ -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' });
}

View File

@ -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'));