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