fix: secure admin API endpoints with Bearer token auth
- main-site/server.js: add requireAuth middleware to POST /api/update-status - gallery-backend/routes/photos.js: add requireAuth to upload, delete, and update routes - admin/admin.js: send Authorization: Bearer header on all mutating requests (fetch + XHR upload); handle 401 on update-status and photo save - docker-compose.yml: pass ADMIN_PASSWORD to gallery-backend; remove MongoDB public port mapping (27017:27017) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c40db43c04
commit
3330c47af2
@ -37,6 +37,7 @@ services:
|
||||
environment:
|
||||
MONGO_URI: mongodb://mongodb:27017/photogallery
|
||||
WATERMARK_URL: http://watermarker:8000/watermark
|
||||
ADMIN_PASSWORD: ${MAIN_ADMIN_PASSWORD}
|
||||
volumes:
|
||||
- ./main-site/photo-gallery-app/backend/uploads:/usr/src/app/uploads
|
||||
depends_on:
|
||||
@ -54,8 +55,6 @@ services:
|
||||
mongodb:
|
||||
image: mongo:latest
|
||||
container_name: bpb-mongodb
|
||||
ports:
|
||||
- "27017:27017"
|
||||
volumes:
|
||||
- ./mongodb_data:/data/db
|
||||
restart: always
|
||||
|
||||
@ -344,13 +344,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${getAdminPassword()}`,
|
||||
},
|
||||
body: JSON.stringify(updatedPhoto)
|
||||
});
|
||||
|
||||
if (response.status === 401) { handleUnauthorized(); return; }
|
||||
if (response.ok) {
|
||||
closeEditModal();
|
||||
fetchPhotos(); // Refresh the gallery
|
||||
fetchPhotos();
|
||||
fetchTagMeta();
|
||||
} else {
|
||||
alert('Failed to save changes.');
|
||||
@ -364,7 +366,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
async function deletePhoto(id) {
|
||||
if (confirm('Are you sure you want to delete this photo?')) {
|
||||
try {
|
||||
await fetch(`${backendUrl}/photos/${id}`, { method: 'DELETE' });
|
||||
await fetch(`${backendUrl}/photos/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Authorization': `Bearer ${getAdminPassword()}` }
|
||||
});
|
||||
fetchPhotos();
|
||||
} catch (error) {
|
||||
console.error('Error deleting photo:', error);
|
||||
@ -392,7 +397,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
confirmBulkDeleteBtn.classList.add('is-loading');
|
||||
try {
|
||||
await Promise.all(ids.map(id => fetch(`${backendUrl}/photos/${id}`, { method: 'DELETE' })));
|
||||
await Promise.all(ids.map(id => fetch(`${backendUrl}/photos/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Authorization': `Bearer ${getAdminPassword()}` }
|
||||
})));
|
||||
clearSelection();
|
||||
fetchPhotos();
|
||||
closeBulkDeleteModal();
|
||||
@ -449,7 +457,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
};
|
||||
await fetch(`${backendUrl}/photos/update/${id}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${getAdminPassword()}`,
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
}));
|
||||
@ -613,6 +624,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
|
||||
xhr.open('POST', `${backendUrl}/photos/upload`);
|
||||
xhr.setRequestHeader('Authorization', `Bearer ${getAdminPassword()}`);
|
||||
|
||||
uploadButton.classList.add('is-loading');
|
||||
uploadProgress.style.display = 'block';
|
||||
@ -758,11 +770,17 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const response = await fetch('/api/update-status', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${getAdminPassword()}`,
|
||||
},
|
||||
body: JSON.stringify({ data })
|
||||
});
|
||||
|
||||
if (response.status === 401) {
|
||||
handleUnauthorized();
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
|
||||
@ -21,6 +21,16 @@ const {
|
||||
} = require('../lib/tagConfig');
|
||||
|
||||
const WATERMARK_URL = process.env.WATERMARK_URL || 'http://watermarker:8000/watermark';
|
||||
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) : '';
|
||||
if (!token || !ADMIN_PASSWORD || token !== ADMIN_PASSWORD) {
|
||||
return res.status(401).json({ success: false, error: 'Unauthorized' });
|
||||
}
|
||||
next();
|
||||
}
|
||||
// We now use a visible diagonal watermark only. Invisible watermarking is disabled by default.
|
||||
const DISABLE_WM = true;
|
||||
|
||||
@ -110,7 +120,7 @@ const parseIncomingTags = (tagsInput) => {
|
||||
};
|
||||
|
||||
// POST new photo(s) with WebP conversion + duplicate hash checks
|
||||
router.route('/upload').post(upload.array('photos'), async (req, res) => {
|
||||
router.route('/upload').post(requireAuth, upload.array('photos'), async (req, res) => {
|
||||
const files = (req.files && req.files.length) ? req.files : (req.file ? [req.file] : []);
|
||||
if (!files.length) {
|
||||
return res.status(400).json({ success: false, error: 'No file uploaded. Please select at least one file.' });
|
||||
@ -343,7 +353,7 @@ router.route('/:id').get((req, res) => {
|
||||
});
|
||||
|
||||
// DELETE a photo by ID
|
||||
router.route('/:id').delete((req, res) => {
|
||||
router.route('/:id').delete(requireAuth, (req, res) => {
|
||||
console.log('DELETE request received for photo ID:', req.params.id);
|
||||
console.log('Request headers:', req.headers);
|
||||
console.log('Request IP:', req.ip);
|
||||
@ -369,7 +379,7 @@ router.route('/:id').delete((req, res) => {
|
||||
});
|
||||
|
||||
// UPDATE a photo by ID
|
||||
router.route('/update/:id').post((req, res) => {
|
||||
router.route('/update/:id').post(requireAuth, (req, res) => {
|
||||
Photo.findById(req.params.id)
|
||||
.then(photo => {
|
||||
const incomingCaption = req.body.caption;
|
||||
|
||||
@ -35,7 +35,16 @@ app.use(bodyParser.json());
|
||||
// --- API Routes ---
|
||||
const apiRouter = express.Router();
|
||||
|
||||
apiRouter.post('/update-status', (req, res) => {
|
||||
function requireAuth(req, res, next) {
|
||||
const auth = req.headers['authorization'] || '';
|
||||
const token = auth.startsWith('Bearer ') ? auth.slice(7) : '';
|
||||
if (!token || token !== ADMIN_PASSWORD) {
|
||||
return res.status(401).json({ success: false, message: 'Unauthorized' });
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
apiRouter.post('/update-status', requireAuth, (req, res) => {
|
||||
console.log(`[${new Date().toISOString()}] Received request for /api/update-status`);
|
||||
const { data } = req.body;
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user