feat: add multi-administrator support with RBAC and fix backup/restore for S3

## Multi-Administrator System
- Add role-based access control (RBAC) with predefined roles (Super Admin, Admin, Editor, Viewer)
- Add granular permissions system for all admin operations
- Add admin user management page with invite functionality
- Add email invitation system for new administrators
- Add permission middleware protecting all admin routes
- Add PermissionGate component for frontend permission checks
- Track event creator (created_by) for audit purposes

## Backup & Restore Fixes
- Fix S3 backup: endpoint URL handling, manifest loading, field name compatibility
- Fix S3 restore: add list-backups endpoint, transform S3 config from frontend format
- Fix PostgreSQL compatibility: add .returning('id') for insert operations
- Fix disk space check: use df command, handle unknown space gracefully
- Fix dry-run validation to not block on warnings
- Fix req.user → req.admin in restore routes

## Database Migrations
- 054: Add roles table with predefined roles
- 055: Add permissions table
- 056: Add role_permissions junction table
- 057: Add role_id to admin_users
- 058: Add admin_invitations table
- 059: Add admin email templates
- 060: Add created_by to events table

## Other Improvements
- Update .gitignore to exclude planning docs and local backup directory
- Remove SQLite database file from tracking
- Add i18n translations for user management (EN/DE)
This commit is contained in:
Paul Nothaft
2026-01-07 17:10:46 +01:00
parent b706eeb5d3
commit 892e47d017
65 changed files with 4854 additions and 493 deletions
+75
View File
@@ -0,0 +1,75 @@
/**
* Accept Invitation Routes (Public)
* Handles invitation token validation and account creation
*/
const express = require('express');
const { body, param } = require('express-validator');
const { handleAsync, validateRequest, successResponse } = require('../utils/routeHelpers');
const { validatePasswordStrength } = require('../utils/passwordGenerator');
const userManagementService = require('../services/userManagementService');
const router = express.Router();
/**
* GET /:token
* Validate invitation token
* Public endpoint - no auth required
*/
router.get('/:token', [
param('token').isLength({ min: 64, max: 64 }).withMessage('Invalid invitation token')
], handleAsync(async (req, res) => {
validateRequest(req);
const invitation = await userManagementService.validateInvitationToken(req.params.token);
if (!invitation) {
return res.status(404).json({ error: 'Invalid or expired invitation' });
}
res.json({
valid: true,
email: invitation.email,
role: invitation.role_name,
expiresAt: invitation.expires_at
});
}));
/**
* POST /:token
* Accept invitation and create account
* Public endpoint - no auth required
*/
router.post('/:token', [
param('token').isLength({ min: 64, max: 64 }).withMessage('Invalid invitation token'),
body('username')
.trim()
.isLength({ min: 3, max: 50 })
.withMessage('Username must be 3-50 characters')
.matches(/^[a-zA-Z0-9_-]+$/)
.withMessage('Username can only contain letters, numbers, underscores, and hyphens'),
body('password')
.isLength({ min: 12 })
.withMessage('Password must be at least 12 characters')
.custom((value) => {
const validation = validatePasswordStrength(value);
if (!validation.isValid) {
throw new Error(validation.messages.join(', '));
}
return true;
})
], handleAsync(async (req, res) => {
validateRequest(req);
const result = await userManagementService.acceptInvitation({
token: req.params.token,
username: req.body.username,
password: req.body.password
});
successResponse(res, {
message: 'Account created successfully. You can now log in.',
email: result.email
}, 201);
}));
module.exports = router;
+6 -5
View File
@@ -4,12 +4,13 @@ const fs = require('fs').promises;
const { db } = require('../database/db');
const { formatBoolean } = require('../utils/dbCompat');
const { adminAuth } = require('../middleware/auth');
const { requirePermission } = require('../middleware/permissions');
const archiver = require('archiver');
const AdmZip = require('adm-zip');
const router = express.Router();
// Get all archived events
router.get('/', adminAuth, async (req, res) => {
router.get('/', adminAuth, requirePermission('archives.view'), async (req, res) => {
try {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 20;
@@ -81,7 +82,7 @@ router.get('/', adminAuth, async (req, res) => {
});
// Get single archive details
router.get('/:id', adminAuth, async (req, res) => {
router.get('/:id', adminAuth, requirePermission('archives.view'), async (req, res) => {
try {
const archive = await db('events')
.where('id', req.params.id)
@@ -137,7 +138,7 @@ router.get('/:id', adminAuth, async (req, res) => {
});
// Restore archive
router.post('/:id/restore', adminAuth, async (req, res) => {
router.post('/:id/restore', adminAuth, requirePermission('archives.restore'), async (req, res) => {
try {
const archive = await db('events')
.where('id', req.params.id)
@@ -300,7 +301,7 @@ router.post('/:id/restore', adminAuth, async (req, res) => {
});
// Download archive
router.get('/:id/download', adminAuth, async (req, res) => {
router.get('/:id/download', adminAuth, requirePermission('archives.download'), async (req, res) => {
try {
const archive = await db('events')
.where('id', req.params.id)
@@ -349,7 +350,7 @@ router.get('/:id/download', adminAuth, async (req, res) => {
});
// Delete archive permanently
router.delete('/:id', adminAuth, async (req, res) => {
router.delete('/:id', adminAuth, requirePermission('archives.delete'), async (req, res) => {
try {
const archive = await db('events')
.where('id', req.params.id)
+22 -21
View File
@@ -1,6 +1,7 @@
const express = require('express');
const { db } = require('../database/db');
const { adminAuth } = require('../middleware/auth');
const { requirePermission } = require('../middleware/permissions');
const { triggerManualBackup, getBackupStatus, cleanupOldBackupRuns, getBackupManifest, validateBackupManifest } = require('../services/backupService');
const logger = require('../utils/logger');
const fs = require('fs').promises;
@@ -12,7 +13,7 @@ const S3StorageAdapter = require('../services/storage/s3Storage');
const router = express.Router();
// Get backup configuration
router.get('/config', adminAuth, async (req, res) => {
router.get('/config', adminAuth, requirePermission('backup.view'), async (req, res) => {
try {
const settings = await db('app_settings')
.where('setting_type', 'backup')
@@ -35,7 +36,7 @@ router.get('/config', adminAuth, async (req, res) => {
});
// Update backup configuration
router.put('/config', adminAuth, async (req, res) => {
router.put('/config', adminAuth, requirePermission('backup.create'), async (req, res) => {
try {
const updates = req.body;
@@ -97,7 +98,7 @@ router.put('/config', adminAuth, async (req, res) => {
});
// Get backup status and history
router.get('/status', adminAuth, async (req, res) => {
router.get('/status', adminAuth, requirePermission('backup.view'), async (req, res) => {
try {
const limit = parseInt(req.query.limit) || 10;
const status = await getBackupStatus(limit);
@@ -110,7 +111,7 @@ router.get('/status', adminAuth, async (req, res) => {
});
// Trigger manual backup
router.post('/run', adminAuth, async (req, res) => {
router.post('/run', adminAuth, requirePermission('backup.create'), async (req, res) => {
try {
// Check if backup is already running
const status = await getBackupStatus();
@@ -131,7 +132,7 @@ router.post('/run', adminAuth, async (req, res) => {
});
// Get backup run details
router.get('/runs/:id', adminAuth, async (req, res) => {
router.get('/runs/:id', adminAuth, requirePermission('backup.view'), async (req, res) => {
try {
const { id } = req.params;
@@ -160,7 +161,7 @@ router.get('/runs/:id', adminAuth, async (req, res) => {
});
// Get file states (for debugging/monitoring)
router.get('/files', adminAuth, async (req, res) => {
router.get('/files', adminAuth, requirePermission('backup.view'), async (req, res) => {
try {
const { page = 1, limit = 50, search = '' } = req.query;
const offset = (page - 1) * limit;
@@ -195,7 +196,7 @@ router.get('/files', adminAuth, async (req, res) => {
});
// Clean up old backup runs
router.delete('/cleanup', adminAuth, async (req, res) => {
router.delete('/cleanup', adminAuth, requirePermission('backup.delete'), async (req, res) => {
try {
const { days = 30 } = req.body;
@@ -209,7 +210,7 @@ router.delete('/cleanup', adminAuth, async (req, res) => {
});
// Test backup destination connectivity
router.post('/test-connection', adminAuth, async (req, res) => {
router.post('/test-connection', adminAuth, requirePermission('backup.create'), async (req, res) => {
try {
const { destination_type, ...config } = req.body;
@@ -334,7 +335,7 @@ router.post('/test-connection', adminAuth, async (req, res) => {
});
// Get backup manifest for a specific backup run
router.get('/manifest/:backupRunId', adminAuth, async (req, res) => {
router.get('/manifest/:backupRunId', adminAuth, requirePermission('backup.view'), async (req, res) => {
try {
const { backupRunId } = req.params;
const result = await getBackupManifest(backupRunId);
@@ -351,7 +352,7 @@ router.get('/manifest/:backupRunId', adminAuth, async (req, res) => {
});
// Validate a backup manifest
router.post('/manifest/validate', adminAuth, async (req, res) => {
router.post('/manifest/validate', adminAuth, requirePermission('backup.view'), async (req, res) => {
try {
const { manifestPath } = req.body;
@@ -373,7 +374,7 @@ router.post('/manifest/validate', adminAuth, async (req, res) => {
});
// Download backup manifest
router.get('/manifest/:backupRunId/download', adminAuth, async (req, res) => {
router.get('/manifest/:backupRunId/download', adminAuth, requirePermission('backup.view'), async (req, res) => {
try {
const { backupRunId } = req.params;
const { format = 'json' } = req.query;
@@ -404,7 +405,7 @@ router.get('/manifest/:backupRunId/download', adminAuth, async (req, res) => {
});
// Get manifest for specific backup
router.get('/manifests/:backupId', adminAuth, async (req, res) => {
router.get('/manifests/:backupId', adminAuth, requirePermission('backup.view'), async (req, res) => {
try {
const { backupId } = req.params;
const result = await getBackupManifest(backupId);
@@ -421,7 +422,7 @@ router.get('/manifests/:backupId', adminAuth, async (req, res) => {
});
// Download manifest file
router.get('/manifests/:backupId/download', adminAuth, async (req, res) => {
router.get('/manifests/:backupId/download', adminAuth, requirePermission('backup.view'), async (req, res) => {
try {
const { backupId } = req.params;
const { format = 'json' } = req.query;
@@ -452,7 +453,7 @@ router.get('/manifests/:backupId/download', adminAuth, async (req, res) => {
});
// Validate a manifest
router.post('/manifests/validate', adminAuth, async (req, res) => {
router.post('/manifests/validate', adminAuth, requirePermission('backup.view'), async (req, res) => {
try {
const { manifestPath, manifestData } = req.body;
@@ -481,7 +482,7 @@ router.post('/manifests/validate', adminAuth, async (req, res) => {
});
// List S3 buckets
router.get('/s3/buckets', adminAuth, async (req, res) => {
router.get('/s3/buckets', adminAuth, requirePermission('backup.view'), async (req, res) => {
try {
const config = await getBackupConfig();
@@ -513,7 +514,7 @@ router.get('/s3/buckets', adminAuth, async (req, res) => {
});
// List files in S3 backup location
router.get('/s3/files', adminAuth, async (req, res) => {
router.get('/s3/files', adminAuth, requirePermission('backup.view'), async (req, res) => {
try {
const { prefix = '', maxKeys = 100, continuationToken } = req.query;
const config = await getBackupConfig();
@@ -550,7 +551,7 @@ router.get('/s3/files', adminAuth, async (req, res) => {
});
// Clean up old S3 backups
router.delete('/s3/cleanup', adminAuth, async (req, res) => {
router.delete('/s3/cleanup', adminAuth, requirePermission('backup.delete'), async (req, res) => {
try {
const { retentionDays = 30, dryRun = false } = req.body;
const config = await getBackupConfig();
@@ -612,7 +613,7 @@ router.delete('/s3/cleanup', adminAuth, async (req, res) => {
});
// Test S3 upload functionality
router.post('/s3/test-upload', adminAuth, async (req, res) => {
router.post('/s3/test-upload', adminAuth, requirePermission('backup.create'), async (req, res) => {
try {
const config = await getBackupConfig();
@@ -664,7 +665,7 @@ router.post('/s3/test-upload', adminAuth, async (req, res) => {
});
// Download entire backup
router.get('/download/:backupId', adminAuth, async (req, res) => {
router.get('/download/:backupId', adminAuth, requirePermission('backup.view'), async (req, res) => {
try {
const { backupId } = req.params;
@@ -752,7 +753,7 @@ router.get('/download/:backupId', adminAuth, async (req, res) => {
});
// Get current file checksums
router.get('/checksums', adminAuth, async (req, res) => {
router.get('/checksums', adminAuth, requirePermission('backup.view'), async (req, res) => {
try {
const { path: targetPath = '', recursive = true } = req.query;
const checksums = {};
@@ -821,7 +822,7 @@ router.get('/checksums', adminAuth, async (req, res) => {
});
// Estimate backup size before running
router.post('/estimate', adminAuth, async (req, res) => {
router.post('/estimate', adminAuth, requirePermission('backup.view'), async (req, res) => {
try {
const { includeArchived = true } = req.body;
+4 -3
View File
@@ -2,10 +2,11 @@ const express = require('express');
const { body, validationResult } = require('express-validator');
const { db, logActivity } = require('../database/db');
const { adminAuth } = require('../middleware/auth');
const { requirePermission } = require('../middleware/permissions');
const router = express.Router();
// Get all CMS pages
router.get('/pages', adminAuth, async (req, res) => {
router.get('/pages', adminAuth, requirePermission('cms.view'), async (req, res) => {
try {
const pages = await db('cms_pages').select('*').orderBy('slug', 'asc');
res.json(pages);
@@ -16,7 +17,7 @@ router.get('/pages', adminAuth, async (req, res) => {
});
// Get a single CMS page
router.get('/pages/:slug', adminAuth, async (req, res) => {
router.get('/pages/:slug', adminAuth, requirePermission('cms.view'), async (req, res) => {
try {
const { slug } = req.params;
const page = await db('cms_pages').where('slug', slug).first();
@@ -33,7 +34,7 @@ router.get('/pages/:slug', adminAuth, async (req, res) => {
});
// Update a CMS page
router.put('/pages/:slug', adminAuth, [
router.put('/pages/:slug', adminAuth, requirePermission('cms.edit'), [
body('title_en').optional().isString(),
body('title_de').optional().isString(),
body('content_en').optional().isString(),
+6 -5
View File
@@ -3,10 +3,11 @@ const { body, validationResult } = require('express-validator');
const { db, logActivity } = require('../database/db');
const { formatBoolean } = require('../utils/dbCompat');
const { adminAuth } = require('../middleware/auth');
const { requirePermission } = require('../middleware/permissions');
const router = express.Router();
// Get all global categories
router.get('/global', adminAuth, async (req, res) => {
router.get('/global', adminAuth, requirePermission('settings.view'), async (req, res) => {
try {
const categories = await db('photo_categories')
.where('is_global', formatBoolean(true))
@@ -20,7 +21,7 @@ router.get('/global', adminAuth, async (req, res) => {
});
// Get categories for a specific event (global + event-specific)
router.get('/event/:eventId', adminAuth, async (req, res) => {
router.get('/event/:eventId', adminAuth, requirePermission('settings.view'), async (req, res) => {
try {
const { eventId } = req.params;
@@ -40,7 +41,7 @@ router.get('/event/:eventId', adminAuth, async (req, res) => {
});
// Create a new category
router.post('/', adminAuth, [
router.post('/', adminAuth, requirePermission('settings.edit'), [
body('name').notEmpty().withMessage('Category name is required'),
body('slug').optional(),
body('is_global').optional().isBoolean(),
@@ -104,7 +105,7 @@ router.post('/', adminAuth, [
});
// Update a category
router.put('/:id', adminAuth, [
router.put('/:id', adminAuth, requirePermission('settings.edit'), [
body('name').notEmpty().withMessage('Category name is required')
], async (req, res) => {
try {
@@ -149,7 +150,7 @@ router.put('/:id', adminAuth, [
});
// Delete a category
router.delete('/:id', adminAuth, async (req, res) => {
router.delete('/:id', adminAuth, requirePermission('settings.edit'), async (req, res) => {
try {
const { id } = req.params;
+6 -5
View File
@@ -8,6 +8,7 @@ const router = express.Router();
const { body, param, validationResult } = require('express-validator');
const { db, withRetry } = require('../database/db');
const { adminAuth } = require('../middleware/auth');
const { requirePermission } = require('../middleware/permissions');
const { sanitizeCSS, validateCSS, MAX_CSS_SIZE } = require('../utils/cssSanitizer');
const { DEFAULT_CSS_TEMPLATE } = require('../../migrations/core/052_add_css_templates');
@@ -15,7 +16,7 @@ const { DEFAULT_CSS_TEMPLATE } = require('../../migrations/core/052_add_css_temp
* GET /admin/css-templates
* Get all CSS templates
*/
router.get('/', adminAuth, async (req, res) => {
router.get('/', adminAuth, requirePermission('branding.view'), async (req, res) => {
try {
const templates = await withRetry(() =>
db('css_templates').orderBy('slot_number')
@@ -31,7 +32,7 @@ router.get('/', adminAuth, async (req, res) => {
* GET /admin/css-templates/enabled
* Get only enabled templates (for event form dropdown)
*/
router.get('/enabled', adminAuth, async (req, res) => {
router.get('/enabled', adminAuth, requirePermission('branding.view'), async (req, res) => {
try {
const templates = await withRetry(() =>
db('css_templates')
@@ -50,7 +51,7 @@ router.get('/enabled', adminAuth, async (req, res) => {
* GET /admin/css-templates/:slotNumber
* Get a specific template by slot number
*/
router.get('/:slotNumber', adminAuth, [
router.get('/:slotNumber', adminAuth, requirePermission('branding.view'), [
param('slotNumber').isInt({ min: 1, max: 3 })
], async (req, res) => {
try {
@@ -81,7 +82,7 @@ router.get('/:slotNumber', adminAuth, [
* PUT /admin/css-templates/:slotNumber
* Update a template
*/
router.put('/:slotNumber', adminAuth, [
router.put('/:slotNumber', adminAuth, requirePermission('branding.edit'), [
param('slotNumber').isInt({ min: 1, max: 3 }),
body('name').optional().isString().isLength({ max: 50 }),
body('css_content').optional().isString(),
@@ -158,7 +159,7 @@ router.put('/:slotNumber', adminAuth, [
* POST /admin/css-templates/:slotNumber/reset
* Reset template to default (only for slot 1)
*/
router.post('/:slotNumber/reset', adminAuth, [
router.post('/:slotNumber/reset', adminAuth, requirePermission('branding.edit'), [
param('slotNumber').isInt({ min: 1, max: 1 }).withMessage('Only template 1 can be reset to default')
], async (req, res) => {
try {
+5 -4
View File
@@ -1,12 +1,13 @@
const express = require('express');
const { db } = require('../database/db');
const { adminAuth } = require('../middleware/auth');
const { requirePermission } = require('../middleware/permissions');
const { sanitizeDays, addDateRangeCondition } = require('../utils/sqlSecurity');
const { formatBoolean } = require('../utils/dbCompat');
const router = express.Router();
// Get dashboard statistics
router.get('/stats', adminAuth, async (req, res) => {
router.get('/stats', adminAuth, requirePermission('analytics.view'), async (req, res) => {
try {
// Get active events count
const activeEvents = await db('events')
@@ -106,7 +107,7 @@ router.get('/stats', adminAuth, async (req, res) => {
});
// Get recent activity
router.get('/activity', adminAuth, async (req, res) => {
router.get('/activity', adminAuth, requirePermission('analytics.view'), async (req, res) => {
try {
const limit = parseInt(req.query.limit) || 10;
@@ -144,7 +145,7 @@ router.get('/activity', adminAuth, async (req, res) => {
});
// Get system health status
router.get('/health', adminAuth, async (req, res) => {
router.get('/health', adminAuth, requirePermission('settings.view'), async (req, res) => {
try {
const os = require('os');
@@ -216,7 +217,7 @@ router.get('/health', adminAuth, async (req, res) => {
});
// Get analytics data for charts
router.get('/analytics', adminAuth, async (req, res) => {
router.get('/analytics', adminAuth, requirePermission('analytics.view'), async (req, res) => {
try {
const days = sanitizeDays(req.query.days || 7);
+9 -8
View File
@@ -1,6 +1,7 @@
const express = require('express');
const router = express.Router();
const { adminAuth } = require('../middleware/auth');
const { requirePermission } = require('../middleware/permissions');
const { databaseBackupService } = require('../services/databaseBackup');
const { db } = require('../database/db');
const logger = require('../utils/logger');
@@ -11,7 +12,7 @@ router.use(adminAuth);
/**
* Get database backup status and configuration
*/
router.get('/status', async (req, res) => {
router.get('/status', requirePermission('backup.view'), async (req, res) => {
try {
// Get configuration
const config = await databaseBackupService.getBackupConfig();
@@ -45,7 +46,7 @@ router.get('/status', async (req, res) => {
/**
* Update database backup configuration
*/
router.put('/config', async (req, res) => {
router.put('/config', requirePermission('backup.create'), async (req, res) => {
try {
const allowedSettings = [
'database_backup_enabled',
@@ -108,7 +109,7 @@ router.put('/config', async (req, res) => {
/**
* Trigger manual database backup
*/
router.post('/backup', async (req, res) => {
router.post('/backup', requirePermission('backup.create'), async (req, res) => {
try {
if (databaseBackupService.isRunning) {
return res.status(409).json({ error: 'Backup already in progress' });
@@ -134,7 +135,7 @@ router.post('/backup', async (req, res) => {
/**
* Get current backup progress
*/
router.get('/progress', async (req, res) => {
router.get('/progress', requirePermission('backup.view'), async (req, res) => {
try {
const progress = databaseBackupService.getProgress();
@@ -151,7 +152,7 @@ router.get('/progress', async (req, res) => {
/**
* Get backup history with pagination
*/
router.get('/history', async (req, res) => {
router.get('/history', requirePermission('backup.view'), async (req, res) => {
try {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 20;
@@ -183,7 +184,7 @@ router.get('/history', async (req, res) => {
/**
* Delete old backup files
*/
router.delete('/cleanup', async (req, res) => {
router.delete('/cleanup', requirePermission('backup.delete'), async (req, res) => {
try {
const { retentionDays = 30 } = req.body;
@@ -202,7 +203,7 @@ router.delete('/cleanup', async (req, res) => {
/**
* Test database backup configuration
*/
router.post('/test', async (req, res) => {
router.post('/test', requirePermission('backup.create'), async (req, res) => {
try {
const config = await databaseBackupService.getBackupConfig();
@@ -255,7 +256,7 @@ router.post('/test', async (req, res) => {
/**
* Get table checksums
*/
router.get('/checksums', async (req, res) => {
router.get('/checksums', requirePermission('backup.view'), async (req, res) => {
try {
const checksums = await databaseBackupService.getTableChecksums();
+8 -5
View File
@@ -3,10 +3,11 @@ const nodemailer = require('nodemailer');
const { body, validationResult } = require('express-validator');
const { db, logActivity } = require('../database/db');
const { adminAuth } = require('../middleware/auth');
const { requirePermission } = require('../middleware/permissions');
const router = express.Router();
// Get email configuration
router.get('/config', adminAuth, async (req, res) => {
router.get('/config', adminAuth, requirePermission('email.view'), async (req, res) => {
try {
const config = await db('email_configs').first();
@@ -37,6 +38,7 @@ router.get('/config', adminAuth, async (req, res) => {
// Update email configuration
router.post('/config', [
adminAuth,
requirePermission('email.edit'),
body('smtp_host').notEmpty().withMessage('SMTP host is required'),
body('smtp_port').isInt({ min: 1, max: 65535 }).withMessage('Invalid port number'),
body('from_email').isEmail().withMessage('Invalid from email address')
@@ -100,7 +102,7 @@ router.post('/config', [
});
// Test email configuration
router.post('/test', adminAuth, async (req, res) => {
router.post('/test', adminAuth, requirePermission('email.send'), async (req, res) => {
try {
const { test_email } = req.body;
@@ -236,7 +238,7 @@ router.post('/test', adminAuth, async (req, res) => {
});
// Get email templates
router.get('/templates', adminAuth, async (req, res) => {
router.get('/templates', adminAuth, requirePermission('email.view'), async (req, res) => {
try {
const templates = await db('email_templates')
.select('*')
@@ -290,7 +292,7 @@ router.get('/templates', adminAuth, async (req, res) => {
});
// Get single template
router.get('/templates/:key', adminAuth, async (req, res) => {
router.get('/templates/:key', adminAuth, requirePermission('email.view'), async (req, res) => {
try {
const template = await db('email_templates')
.where('template_key', req.params.key)
@@ -346,6 +348,7 @@ router.get('/templates/:key', adminAuth, async (req, res) => {
// Update email template
router.put('/templates/:key', [
adminAuth,
requirePermission('email.edit'),
body('subject_en').optional().notEmpty().withMessage('English subject cannot be empty'),
body('subject_de').optional().notEmpty().withMessage('German subject cannot be empty'),
body('body_html_en').optional().notEmpty().withMessage('English HTML body cannot be empty'),
@@ -424,7 +427,7 @@ router.put('/templates/:key', [
});
// Preview email template
router.post('/templates/:key/preview', adminAuth, async (req, res) => {
router.post('/templates/:key/preview', adminAuth, requirePermission('email.view'), async (req, res) => {
try {
const template = await db('email_templates')
.where('template_key', req.params.key)
+3 -2
View File
@@ -6,6 +6,7 @@
const express = require('express');
const { body, validationResult } = require('express-validator');
const { adminAuth } = require('../middleware/auth');
const { requirePermission } = require('../middleware/permissions');
const eventRenameService = require('../services/eventRenameService');
const router = express.Router();
@@ -13,7 +14,7 @@ const router = express.Router();
* POST /api/admin/events/:eventId/rename
* Rename an event
*/
router.post('/:eventId/rename', adminAuth, [
router.post('/:eventId/rename', adminAuth, requirePermission('events.edit'), [
body('newEventName')
.trim()
.isLength({ min: 3, max: 100 })
@@ -58,7 +59,7 @@ router.post('/:eventId/rename', adminAuth, [
* POST /api/admin/events/:eventId/validate-rename
* Validate a potential rename without executing it
*/
router.post('/:eventId/validate-rename', adminAuth, [
router.post('/:eventId/validate-rename', adminAuth, requirePermission('events.edit'), [
body('newEventName')
.trim()
.isLength({ min: 3, max: 100 })
+2 -1
View File
@@ -3,9 +3,10 @@
const { validatePasswordInContext, getBcryptRounds } = require('../utils/passwordValidation');
const { buildShareLinkVariants } = require('../services/shareLinkService');
const { requirePermission } = require('../middleware/permissions');
// Enhanced event creation with password validation
router.post('/', adminAuth, [
router.post('/', adminAuth, requirePermission('events.create'), [
body('event_type').isIn(['wedding', 'birthday', 'corporate', 'other']),
body('event_name').notEmpty().trim(),
body('event_date').isDate(),
+53 -23
View File
@@ -3,6 +3,7 @@ const { body, query, validationResult } = require('express-validator');
const { db, logActivity } = require('../database/db');
const { formatBoolean } = require('../utils/dbCompat');
const { adminAuth } = require('../middleware/auth');
const { requirePermission } = require('../middleware/permissions');
const router = express.Router();
const bcrypt = require('bcrypt');
const crypto = require('crypto');
@@ -102,7 +103,7 @@ const hasCustomerContactColumns = async () => {
};
// Create new event
router.post('/', adminAuth, [
router.post('/', adminAuth, requirePermission('events.create'), [
body('event_type').isIn(['wedding', 'birthday', 'corporate', 'other']),
body('event_name').notEmpty().trim(),
body('event_date').isDate(),
@@ -296,6 +297,7 @@ router.post('/', adminAuth, [
share_token: shareToken,
expires_at: expires_at.toISOString(),
created_at: new Date().toISOString(),
created_by: req.admin.id,
allow_user_uploads,
upload_category_id,
allow_downloads: formatBoolean(allow_downloads !== undefined ? allow_downloads : true),
@@ -375,7 +377,7 @@ router.post('/', adminAuth, [
});
// Get all events with pagination and filters
router.get('/', adminAuth, async (req, res) => {
router.get('/', adminAuth, requirePermission('events.view'), async (req, res) => {
try {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 20;
@@ -387,7 +389,12 @@ router.get('/', adminAuth, async (req, res) => {
// Build query
let query = db('events');
// Editor role can only see their own events
if (req.admin.roleName === 'editor') {
query = query.where('created_by', req.admin.id);
}
// Apply search filter
if (search) {
const escapedSearch = escapeLikePattern(search);
@@ -465,13 +472,18 @@ router.get('/', adminAuth, async (req, res) => {
});
// Get single event details
router.get('/:id', adminAuth, async (req, res) => {
router.get('/:id', adminAuth, requirePermission('events.view'), async (req, res) => {
try {
const { id } = req.params;
const event = await db('events')
.where('id', id)
.first();
let query = db('events').where('id', id);
// Editor role can only see their own events
if (req.admin.roleName === 'editor') {
query = query.where('created_by', req.admin.id);
}
const event = await query.first();
if (!event) {
return res.status(404).json({ error: 'Event not found' });
@@ -525,7 +537,7 @@ router.get('/:id', adminAuth, async (req, res) => {
});
// Update event
router.put('/:id', adminAuth, [
router.put('/:id', adminAuth, requirePermission('events.edit'), [
body('event_name').optional().trim().notEmpty(),
body('admin_email').optional().isEmail(),
body('is_active').optional().isBoolean(),
@@ -659,7 +671,12 @@ router.put('/:id', adminAuth, [
});
// Check if event exists
const event = await db('events').where('id', id).first();
let eventQuery = db('events').where('id', id);
// Editor role can only edit their own events
if (req.admin.roleName === 'editor') {
eventQuery = eventQuery.where('created_by', req.admin.id);
}
const event = await eventQuery.first();
if (!event) {
return res.status(404).json({ error: 'Event not found' });
}
@@ -696,7 +713,7 @@ router.put('/:id', adminAuth, [
});
// Delete event
router.delete('/:id', adminAuth, async (req, res) => {
router.delete('/:id', adminAuth, requirePermission('events.delete'), async (req, res) => {
try {
const { id } = req.params;
@@ -777,11 +794,16 @@ router.delete('/:id', adminAuth, async (req, res) => {
});
// Toggle event status
router.post('/:id/toggle-status', adminAuth, async (req, res) => {
router.post('/:id/toggle-status', adminAuth, requirePermission('events.edit'), async (req, res) => {
try {
const { id } = req.params;
const event = await db('events').where('id', id).first();
let eventQuery = db('events').where('id', id);
// Editor role can only edit their own events
if (req.admin.roleName === 'editor') {
eventQuery = eventQuery.where('created_by', req.admin.id);
}
const event = await eventQuery.first();
if (!event) {
return res.status(404).json({ error: 'Event not found' });
}
@@ -812,12 +834,17 @@ router.post('/:id/toggle-status', adminAuth, async (req, res) => {
});
// Reset event password
router.post('/:id/reset-password', adminAuth, async (req, res) => {
router.post('/:id/reset-password', adminAuth, requirePermission('events.edit'), async (req, res) => {
try {
const { id } = req.params;
const { sendEmail = true } = req.body;
const event = await db('events').where('id', id).first();
let eventQuery = db('events').where('id', id);
// Editor role can only edit their own events
if (req.admin.roleName === 'editor') {
eventQuery = eventQuery.where('created_by', req.admin.id);
}
const event = await eventQuery.first();
if (!event) {
return res.status(404).json({ error: 'Event not found' });
}
@@ -874,15 +901,18 @@ router.post('/:id/reset-password', adminAuth, async (req, res) => {
});
// Resend creation email
router.post('/:id/resend-email', adminAuth, async (req, res) => {
router.post('/:id/resend-email', adminAuth, requirePermission('events.edit'), async (req, res) => {
try {
const { id } = req.params;
// Get event details
const event = await db('events')
.where('id', id)
.first();
let eventQuery = db('events').where('id', id);
// Editor role can only edit their own events
if (req.admin.roleName === 'editor') {
eventQuery = eventQuery.where('created_by', req.admin.id);
}
const event = await eventQuery.first();
if (!event) {
return res.status(404).json({ error: 'Event not found' });
}
@@ -954,7 +984,7 @@ router.post('/:id/resend-email', adminAuth, async (req, res) => {
});
// Archive event
router.post('/:id/archive', adminAuth, async (req, res) => {
router.post('/:id/archive', adminAuth, requirePermission('events.archive'), async (req, res) => {
try {
const { id } = req.params;
@@ -985,7 +1015,7 @@ router.post('/:id/archive', adminAuth, async (req, res) => {
});
// Bulk archive events
router.post('/bulk-archive', adminAuth, [
router.post('/bulk-archive', adminAuth, requirePermission('events.archive'), [
body('eventIds').isArray().withMessage('eventIds must be an array'),
body('eventIds.*').isInt().withMessage('Each eventId must be an integer')
], async (req, res) => {
+3 -2
View File
@@ -2,6 +2,7 @@ const express = require('express');
const path = require('path');
const fs = require('fs').promises;
const { adminAuth } = require('../middleware/auth');
const { requirePermission } = require('../middleware/permissions');
const { list, resolveExternalPath, getExternalMediaRoot } = require('../services/externalMediaService');
const { db, logActivity } = require('../database/db');
const logger = require('../utils/logger');
@@ -9,7 +10,7 @@ const logger = require('../utils/logger');
const router = express.Router();
// GET /api/admin/external-media/list?path=relative/dir
router.get('/list', adminAuth, async (req, res) => {
router.get('/list', adminAuth, requirePermission('photos.view'), async (req, res) => {
try {
const relPath = (req.query.path || '').replace(/^\/+/, '');
const result = await list(relPath);
@@ -45,7 +46,7 @@ async function walkDir(dir, baseDir) {
// POST /api/admin/events/:id/import-external
// Body: { external_path: string, recursive?: boolean, map?: { individual?: string, collages?: string } }
router.post('/events/:id/import-external', adminAuth, async (req, res) => {
router.post('/events/:id/import-external', adminAuth, requirePermission('photos.upload'), async (req, res) => {
try {
const eventId = parseInt(req.params.id);
const { external_path, recursive = true, map = { individual: 'individual', collages: 'collages' } } = req.body || {};
+14 -1
View File
@@ -1,6 +1,7 @@
const express = require('express');
const router = express.Router();
const { adminAuth } = require('../middleware/auth');
const { requirePermission } = require('../middleware/permissions');
const feedbackService = require('../services/feedbackService');
const feedbackModeration = require('../services/feedbackModeration');
const { db, logActivity } = require('../database/db');
@@ -13,8 +14,9 @@ const {
} = require('../utils/feedbackValidation');
// Get event feedback settings
router.get('/events/:eventId/feedback-settings',
router.get('/events/:eventId/feedback-settings',
adminAuth,
requirePermission('events.view'),
validateEventId,
checkValidation,
async (req, res) => {
@@ -39,6 +41,7 @@ router.get('/events/:eventId/feedback-settings',
// Update event feedback settings
router.put('/events/:eventId/feedback-settings',
adminAuth,
requirePermission('events.edit'),
validateEventId,
validateFeedbackSettings,
checkValidation,
@@ -75,6 +78,7 @@ router.put('/events/:eventId/feedback-settings',
// Get feedback for an event (with filters)
router.get('/events/:eventId/feedback',
adminAuth,
requirePermission('events.view'),
validateEventId,
checkValidation,
async (req, res) => {
@@ -159,6 +163,7 @@ router.get('/events/:eventId/feedback',
// Moderate feedback (approve/hide/reject)
router.put('/feedback/:feedbackId/:action',
adminAuth,
requirePermission('events.edit'),
async (req, res) => {
try {
const { feedbackId, action } = req.params;
@@ -180,6 +185,7 @@ router.put('/feedback/:feedbackId/:action',
// Delete feedback
router.delete('/feedback/:feedbackId',
adminAuth,
requirePermission('events.delete'),
async (req, res) => {
try {
const { feedbackId } = req.params;
@@ -197,6 +203,7 @@ router.delete('/feedback/:feedbackId',
// Get feedback analytics for an event
router.get('/events/:eventId/feedback-analytics',
adminAuth,
requirePermission('events.view'),
validateEventId,
checkValidation,
async (req, res) => {
@@ -296,6 +303,7 @@ router.get('/events/:eventId/feedback-analytics',
// Export feedback data
router.get('/events/:eventId/feedback/export',
adminAuth,
requirePermission('events.view'),
validateEventId,
checkValidation,
async (req, res) => {
@@ -324,6 +332,7 @@ router.get('/events/:eventId/feedback/export',
// Get pending moderation items (across all events)
router.get('/feedback/pending-moderation',
adminAuth,
requirePermission('events.view'),
async (req, res) => {
try {
const pending = await feedbackService.getPendingModeration();
@@ -338,6 +347,7 @@ router.get('/feedback/pending-moderation',
// Word filter management
router.get('/word-filters',
adminAuth,
requirePermission('settings.view'),
async (req, res) => {
try {
const filters = await feedbackModeration.getAllWordFilters();
@@ -351,6 +361,7 @@ router.get('/word-filters',
router.post('/word-filters',
adminAuth,
requirePermission('settings.edit'),
validateWordFilter,
checkValidation,
async (req, res) => {
@@ -378,6 +389,7 @@ router.post('/word-filters',
router.put('/word-filters/:id',
adminAuth,
requirePermission('settings.edit'),
async (req, res) => {
try {
const { id } = req.params;
@@ -395,6 +407,7 @@ router.put('/word-filters/:id',
router.delete('/word-filters/:id',
adminAuth,
requirePermission('settings.edit'),
async (req, res) => {
try {
const { id } = req.params;
+9 -8
View File
@@ -1,6 +1,7 @@
const express = require('express');
const { db } = require('../database/db');
const { adminAuth } = require('../middleware/auth');
const { requirePermission } = require('../middleware/permissions');
const secureImageMiddleware = require('../middleware/secureImageMiddleware');
const logger = require('../utils/logger');
@@ -9,7 +10,7 @@ const router = express.Router();
/**
* Get image security settings
*/
router.get('/settings', adminAuth, async (req, res) => {
router.get('/settings', adminAuth, requirePermission('settings.view'), async (req, res) => {
try {
const settings = await db('app_settings')
.whereIn('setting_key', [
@@ -46,7 +47,7 @@ router.get('/settings', adminAuth, async (req, res) => {
/**
* Update image security settings
*/
router.put('/settings', adminAuth, async (req, res) => {
router.put('/settings', adminAuth, requirePermission('settings.edit'), async (req, res) => {
try {
const updates = req.body;
@@ -97,7 +98,7 @@ router.put('/settings', adminAuth, async (req, res) => {
/**
* Get security monitoring dashboard data
*/
router.get('/dashboard', adminAuth, async (req, res) => {
router.get('/dashboard', adminAuth, requirePermission('settings.view'), async (req, res) => {
try {
const { timeframe = '24h' } = req.query;
@@ -202,7 +203,7 @@ router.get('/dashboard', adminAuth, async (req, res) => {
/**
* Get detailed security logs
*/
router.get('/logs', adminAuth, async (req, res) => {
router.get('/logs', adminAuth, requirePermission('settings.view'), async (req, res) => {
try {
const {
page = 1,
@@ -271,7 +272,7 @@ router.get('/logs', adminAuth, async (req, res) => {
/**
* Get image access logs for a specific event
*/
router.get('/events/:eventId/access-logs', adminAuth, async (req, res) => {
router.get('/events/:eventId/access-logs', adminAuth, requirePermission('settings.view'), async (req, res) => {
try {
const { eventId } = req.params;
const { page = 1, limit = 50 } = req.query;
@@ -321,7 +322,7 @@ router.get('/events/:eventId/access-logs', adminAuth, async (req, res) => {
/**
* Block/unblock suspicious IPs
*/
router.post('/block-ip', adminAuth, async (req, res) => {
router.post('/block-ip', adminAuth, requirePermission('settings.edit'), async (req, res) => {
try {
const { ip, action = 'block' } = req.body;
@@ -367,7 +368,7 @@ router.post('/block-ip', adminAuth, async (req, res) => {
/**
* Clear security logs older than specified time
*/
router.delete('/logs/cleanup', adminAuth, async (req, res) => {
router.delete('/logs/cleanup', adminAuth, requirePermission('settings.edit'), async (req, res) => {
try {
const { olderThan = '30d' } = req.body;
@@ -424,7 +425,7 @@ router.delete('/logs/cleanup', adminAuth, async (req, res) => {
/**
* Export security data for analysis
*/
router.get('/export', adminAuth, async (req, res) => {
router.get('/export', adminAuth, requirePermission('settings.view'), async (req, res) => {
try {
const { format = 'json', timeframe = '7d' } = req.query;
+5 -4
View File
@@ -1,10 +1,11 @@
const express = require('express');
const { db, logActivity } = require('../database/db');
const { adminAuth } = require('../middleware/auth');
const { requirePermission } = require('../middleware/permissions');
const router = express.Router();
// Get notifications (unread activity logs)
router.get('/', adminAuth, async (req, res) => {
router.get('/', adminAuth, requirePermission('settings.view'), async (req, res) => {
try {
const { limit = 20, includeRead = false } = req.query;
@@ -64,7 +65,7 @@ router.get('/', adminAuth, async (req, res) => {
});
// Mark notification as read
router.put('/:id/read', adminAuth, async (req, res) => {
router.put('/:id/read', adminAuth, requirePermission('settings.edit'), async (req, res) => {
try {
const { id } = req.params;
@@ -82,7 +83,7 @@ router.put('/:id/read', adminAuth, async (req, res) => {
});
// Mark all notifications as read
router.put('/read-all', adminAuth, async (req, res) => {
router.put('/read-all', adminAuth, requirePermission('settings.edit'), async (req, res) => {
try {
await db('activity_logs')
.whereNull('read_at')
@@ -98,7 +99,7 @@ router.put('/read-all', adminAuth, async (req, res) => {
});
// Delete old notifications (older than 30 days and read)
router.delete('/clear-old', adminAuth, async (req, res) => {
router.delete('/clear-old', adminAuth, requirePermission('settings.edit'), async (req, res) => {
try {
// Use database-agnostic date calculation
const thirtyDaysAgo = new Date();
+5 -4
View File
@@ -8,6 +8,7 @@ const router = express.Router();
const { body, query, validationResult } = require('express-validator');
const { db, withRetry } = require('../database/db');
const { adminAuth } = require('../middleware/auth');
const { requirePermission } = require('../middleware/permissions');
const { PhotoFilterBuilder } = require('../utils/photoFilterBuilder');
const { PhotoExportService } = require('../services/photoExportService');
@@ -17,7 +18,7 @@ const exportService = new PhotoExportService();
* GET /admin/photos/:eventId/filtered
* Get filtered photos with pagination
*/
router.get('/:eventId/filtered', adminAuth, [
router.get('/:eventId/filtered', adminAuth, requirePermission('photos.view'), [
query('min_rating').optional().isFloat({ min: 0, max: 5 }),
query('max_rating').optional().isFloat({ min: 0, max: 5 }),
query('has_likes').optional().isBoolean(),
@@ -131,7 +132,7 @@ router.get('/:eventId/filtered', adminAuth, [
* GET /admin/photos/:eventId/filter-summary
* Get just the summary counts for filter UI
*/
router.get('/:eventId/filter-summary', adminAuth, async (req, res) => {
router.get('/:eventId/filter-summary', adminAuth, requirePermission('photos.view'), async (req, res) => {
try {
const eventId = parseInt(req.params.eventId);
@@ -153,7 +154,7 @@ router.get('/:eventId/filter-summary', adminAuth, async (req, res) => {
* POST /admin/photos/:eventId/export
* Export selected or filtered photos
*/
router.post('/:eventId/export', adminAuth, [
router.post('/:eventId/export', adminAuth, requirePermission('photos.download'), [
body('photo_ids').optional().isArray(),
body('photo_ids.*').optional().isInt(),
body('filter').optional().isObject(),
@@ -213,7 +214,7 @@ router.post('/:eventId/export', adminAuth, [
* GET /admin/photos/export-formats
* Get available export format options
*/
router.get('/export-formats', adminAuth, (req, res) => {
router.get('/export-formats', adminAuth, requirePermission('photos.view'), (req, res) => {
res.json({
success: true,
data: PhotoExportService.getFormatOptions()
+16 -15
View File
@@ -4,6 +4,7 @@ const path = require('path');
const fs = require('fs').promises;
const { db, logActivity } = require('../database/db');
const { adminAuth } = require('../middleware/auth');
const { requirePermission } = require('../middleware/permissions');
const { generateThumbnail, ensureThumbnail } = require('../services/imageProcessor');
const { generatePhotoFilename } = require('../utils/filenameSanitizer');
const { escapeLikePattern } = require('../utils/sqlSecurity');
@@ -109,7 +110,7 @@ const uploadTimeout = (timeout = 300000) => { // 5 minutes default
// Upload photos for an event
// Max file count is configurable via general settings
router.post('/:eventId/upload', adminAuth, uploadTimeout(600000), async (req, res, next) => { // 10 minute timeout
router.post('/:eventId/upload', adminAuth, requirePermission('photos.upload'), uploadTimeout(600000), async (req, res, next) => { // 10 minute timeout
let maxFilesPerUpload;
try {
maxFilesPerUpload = await getMaxFilesPerUpload();
@@ -420,7 +421,7 @@ router.post('/:eventId/upload', adminAuth, uploadTimeout(600000), async (req, re
});
// Delete a photo
router.delete('/:eventId/photos/:photoId', adminAuth, async (req, res) => {
router.delete('/:eventId/photos/:photoId', adminAuth, requirePermission('photos.delete'), async (req, res) => {
try {
const { eventId, photoId } = req.params;
@@ -477,7 +478,7 @@ router.delete('/:eventId/photos/:photoId', adminAuth, async (req, res) => {
});
// Update a photo (e.g., change category)
router.patch('/:eventId/photos/:photoId', adminAuth, async (req, res) => {
router.patch('/:eventId/photos/:photoId', adminAuth, requirePermission('photos.edit'), async (req, res) => {
try {
const { eventId, photoId } = req.params;
const { category_id } = req.body;
@@ -525,7 +526,7 @@ router.patch('/:eventId/photos/:photoId', adminAuth, async (req, res) => {
});
// Bulk delete photos
router.post('/:eventId/photos/bulk-delete', adminAuth, async (req, res) => {
router.post('/:eventId/photos/bulk-delete', adminAuth, requirePermission('photos.delete'), async (req, res) => {
try {
const { eventId } = req.params;
const { photoIds } = req.body;
@@ -593,7 +594,7 @@ router.post('/:eventId/photos/bulk-delete', adminAuth, async (req, res) => {
});
// Bulk update photos
router.post('/:eventId/photos/bulk-update', adminAuth, async (req, res) => {
router.post('/:eventId/photos/bulk-update', adminAuth, requirePermission('photos.edit'), async (req, res) => {
try {
const { eventId } = req.params;
const { photoIds, updates } = req.body;
@@ -651,7 +652,7 @@ router.post('/:eventId/photos/bulk-update', adminAuth, async (req, res) => {
});
// Download a photo
router.get('/:eventId/photos/:photoId/download', adminAuth, async (req, res) => {
router.get('/:eventId/photos/:photoId/download', adminAuth, requirePermission('photos.download'), async (req, res) => {
try {
const { eventId, photoId } = req.params;
@@ -683,7 +684,7 @@ router.get('/:eventId/photos/:photoId/download', adminAuth, async (req, res) =>
});
// Get all photos for an event
router.get('/:eventId/photos', adminAuth, async (req, res) => {
router.get('/:eventId/photos', adminAuth, requirePermission('photos.view'), async (req, res) => {
try {
const { eventId } = req.params;
const { category_id, type, search, sort = 'date', order = 'desc' } = req.query;
@@ -767,7 +768,7 @@ router.get('/:eventId/photos', adminAuth, async (req, res) => {
});
// Serve photo with admin authentication
router.get('/:eventId/photo/:photoId', adminAuth, async (req, res) => {
router.get('/:eventId/photo/:photoId', adminAuth, requirePermission('photos.view'), async (req, res) => {
try {
const { eventId, photoId } = req.params;
@@ -804,7 +805,7 @@ router.get('/:eventId/photo/:photoId', adminAuth, async (req, res) => {
});
// Serve thumbnail with admin authentication
router.get('/:eventId/thumbnail/:photoId', adminAuth, async (req, res) => {
router.get('/:eventId/thumbnail/:photoId', adminAuth, requirePermission('photos.view'), async (req, res) => {
try {
const { eventId, photoId } = req.params;
@@ -844,7 +845,7 @@ router.get('/:eventId/thumbnail/:photoId', adminAuth, async (req, res) => {
});
// Debug endpoint to check photo existence
router.get('/:eventId/debug', adminAuth, async (req, res) => {
router.get('/:eventId/debug', adminAuth, requirePermission('photos.view'), async (req, res) => {
try {
const { eventId } = req.params;
@@ -870,7 +871,7 @@ router.get('/:eventId/debug', adminAuth, async (req, res) => {
// ============================================
// Initialize a chunked upload
router.post('/:eventId/chunked-upload/init', adminAuth, async (req, res) => {
router.post('/:eventId/chunked-upload/init', adminAuth, requirePermission('photos.upload'), async (req, res) => {
try {
const { eventId } = req.params;
const { filename, fileSize, mimeType, totalChunks } = req.body;
@@ -908,7 +909,7 @@ router.post('/:eventId/chunked-upload/init', adminAuth, async (req, res) => {
});
// Upload a chunk
router.post('/:eventId/chunked-upload/:uploadId/chunk/:chunkIndex', adminAuth, async (req, res) => {
router.post('/:eventId/chunked-upload/:uploadId/chunk/:chunkIndex', adminAuth, requirePermission('photos.upload'), async (req, res) => {
try {
const { uploadId, chunkIndex } = req.params;
@@ -929,7 +930,7 @@ router.post('/:eventId/chunked-upload/:uploadId/chunk/:chunkIndex', adminAuth, a
});
// Complete chunked upload and process the file
router.post('/:eventId/chunked-upload/:uploadId/complete', adminAuth, async (req, res) => {
router.post('/:eventId/chunked-upload/:uploadId/complete', adminAuth, requirePermission('photos.upload'), async (req, res) => {
try {
const { eventId, uploadId } = req.params;
const { category_id } = req.body;
@@ -971,7 +972,7 @@ router.post('/:eventId/chunked-upload/:uploadId/complete', adminAuth, async (req
});
// Get upload status
router.get('/:eventId/chunked-upload/:uploadId/status', adminAuth, async (req, res) => {
router.get('/:eventId/chunked-upload/:uploadId/status', adminAuth, requirePermission('photos.view'), async (req, res) => {
try {
const { uploadId } = req.params;
@@ -989,7 +990,7 @@ router.get('/:eventId/chunked-upload/:uploadId/status', adminAuth, async (req, r
});
// Abort chunked upload
router.delete('/:eventId/chunked-upload/:uploadId', adminAuth, async (req, res) => {
router.delete('/:eventId/chunked-upload/:uploadId', adminAuth, requirePermission('photos.delete'), async (req, res) => {
try {
const { uploadId } = req.params;
+149 -17
View File
@@ -2,6 +2,7 @@ const express = require('express');
const router = express.Router();
const { restoreService } = require('../services/restoreService');
const { adminAuth } = require('../middleware/auth');
const { requirePermission } = require('../middleware/permissions');
const { body, query, validationResult } = require('express-validator');
const logger = require('../utils/logger');
const { db } = require('../database/db');
@@ -16,10 +17,36 @@ const fs = require('fs').promises;
// Apply admin authentication to all routes
router.use(adminAuth);
/**
* Transform frontend S3 config to backend format
* Frontend sends: s3Endpoint, s3Bucket, s3AccessKey, s3SecretKey, s3Region
* Backend expects: endpoint, bucket, accessKeyId, secretAccessKey, region
*/
function transformS3Config(body) {
if (body.s3Config) {
// Already in correct format
return body.s3Config;
}
// Check if frontend sent flat S3 config fields
if (body.s3Endpoint || body.s3Bucket || body.s3AccessKey || body.s3SecretKey) {
return {
endpoint: body.s3Endpoint,
bucket: body.s3Bucket,
accessKeyId: body.s3AccessKey,
secretAccessKey: body.s3SecretKey,
region: body.s3Region || 'us-east-1',
forcePathStyle: body.s3ForcePathStyle !== false
};
}
return null;
}
/**
* Get restore service status and history
*/
router.get('/status', async (req, res) => {
router.get('/status', requirePermission('backup.view'), async (req, res) => {
try {
const limit = parseInt(req.query.limit) || 10;
const history = await restoreService.getRestoreHistory(limit);
@@ -47,7 +74,7 @@ router.get('/status', async (req, res) => {
/**
* Validate restore request
*/
router.post('/validate', [
router.post('/validate', requirePermission('backup.restore'), [
body('source').notEmpty().withMessage('Backup source is required'),
body('manifestPath').notEmpty().withMessage('Manifest path is required'),
body('restoreType').isIn(['full', 'database', 'files', 'selective']).withMessage('Invalid restore type'),
@@ -63,18 +90,38 @@ router.post('/validate', [
}
try {
// Transform S3 config from frontend format
const s3Config = transformS3Config(req.body);
// Perform dry run validation
const result = await restoreService.restore({
...req.body,
source: req.body.source,
manifestPath: req.body.manifestPath,
restoreType: req.body.restoreType,
selectedItems: req.body.selectedItems,
s3Config,
dryRun: true,
force: false
});
// Transform spaceCheck to match frontend expected format
const spaceCheck = result.spaceCheck ? {
sufficient: result.spaceCheck.hasEnoughSpace,
required: result.spaceCheck.requiredBytes,
available: result.spaceCheck.availableBytes,
requiredFormatted: result.spaceCheck.requiredFormatted,
availableFormatted: result.spaceCheck.availableFormatted,
// Keep original fields for backwards compatibility
hasEnoughSpace: result.spaceCheck.hasEnoughSpace,
requiredBytes: result.spaceCheck.requiredBytes,
availableBytes: result.spaceCheck.availableBytes
} : null;
res.json({
success: true,
data: {
validation: result.validation,
spaceCheck: result.spaceCheck,
spaceCheck,
logs: result.logs
}
});
@@ -82,7 +129,7 @@ router.post('/validate', [
logger.error('Restore validation failed:', error);
res.status(400).json({
success: false,
error: 'Restore validation failed',
error: error.message || 'Restore validation failed',
logs: restoreService.restoreLog
});
}
@@ -91,7 +138,7 @@ router.post('/validate', [
/**
* Start restore operation
*/
router.post('/start', [
router.post('/start', requirePermission('backup.restore'), [
body('source').notEmpty().withMessage('Backup source is required'),
body('manifestPath').notEmpty().withMessage('Manifest path is required'),
body('restoreType').isIn(['full', 'database', 'files', 'selective']).withMessage('Invalid restore type'),
@@ -135,19 +182,28 @@ router.post('/start', [
// Log restore attempt
logger.warn('Restore operation started', {
user: req.user.email,
user: req.admin.email,
ip: req.ip,
restoreType: req.body.restoreType,
source: req.body.source
});
// Transform S3 config from frontend format
const s3Config = transformS3Config(req.body);
// Start restore in background
restoreService.restore({
...req.body,
source: req.body.source,
manifestPath: req.body.manifestPath,
restoreType: req.body.restoreType,
selectedItems: req.body.selectedItems,
skipPreBackup: req.body.skipPreBackup,
force: req.body.force,
s3Config,
dryRun: false,
operator: {
type: 'manual',
userId: req.user.id,
userId: req.admin.id,
ip: req.ip
}
}).catch(error => {
@@ -160,9 +216,10 @@ router.post('/start', [
});
} catch (error) {
logger.error('Failed to start restore:', error);
logger.error('Error stack:', error.stack);
res.status(500).json({
success: false,
error: 'Failed to start restore operation'
error: error.message || 'Failed to start restore operation'
});
}
});
@@ -170,7 +227,7 @@ router.post('/start', [
/**
* Get current restore progress
*/
router.get('/progress', async (req, res) => {
router.get('/progress', requirePermission('backup.view'), async (req, res) => {
try {
const progress = restoreService.getProgress();
const logs = restoreService.restoreLog.slice(-50); // Last 50 log entries
@@ -195,7 +252,7 @@ router.get('/progress', async (req, res) => {
/**
* Get restore run details
*/
router.get('/run/:id', async (req, res) => {
router.get('/run/:id', requirePermission('backup.view'), async (req, res) => {
try {
const run = await db('restore_runs')
.where('id', req.params.id)
@@ -250,7 +307,7 @@ router.get('/run/:id', async (req, res) => {
/**
* Get restore run report
*/
router.get('/run/:id/report', async (req, res) => {
router.get('/run/:id/report', requirePermission('backup.view'), async (req, res) => {
try {
const run = await db('restore_runs')
.where('id', req.params.id)
@@ -289,7 +346,7 @@ router.get('/run/:id/report', async (req, res) => {
/**
* List available backups for restore
*/
router.get('/available-backups', async (req, res) => {
router.get('/available-backups', requirePermission('backup.view'), async (req, res) => {
try {
const backups = [];
@@ -349,10 +406,85 @@ router.get('/available-backups', async (req, res) => {
}
});
/**
* List backups for restore (POST version for frontend compatibility)
* Accepts source type in request body
*/
router.post('/list-backups', requirePermission('backup.view'), async (req, res) => {
try {
const { source } = req.body; // 'local', 's3', or undefined for all
const backups = [];
// Get backup configuration
const backupConfig = await getBackupConfig();
// Get database backups from backup_runs table
const backupRuns = await db('backup_runs')
.where('status', 'completed')
.whereNotNull('manifest_path')
.orderBy('completed_at', 'desc')
.limit(20);
for (const run of backupRuns) {
const isS3 = run.manifest_path.startsWith('s3://');
const backupType = isS3 ? 's3' : 'local';
// Filter by source if specified
if (source && source !== backupType) {
continue;
}
backups.push({
id: run.id,
type: backupType,
name: `Backup from ${new Date(run.completed_at).toLocaleString()}`,
path: run.manifest_path,
manifest_path: run.manifest_path,
manifestId: run.manifest_id,
manifestPath: run.manifest_path,
size: parseInt(run.total_size_bytes) || 0,
total_size: parseInt(run.total_size_bytes) || 0,
total_size_bytes: parseInt(run.total_size_bytes) || 0,
filesCount: run.files_backed_up || 0,
files_backed_up: run.files_backed_up || 0,
duration: run.duration_seconds,
duration_seconds: run.duration_seconds,
// Frontend expects snake_case date fields
created_at: run.completed_at,
completed_at: run.completed_at,
started_at: run.started_at,
// camelCase aliases
completedAt: run.completed_at,
startedAt: run.started_at,
// Backup metadata
status: run.status,
backup_type: run.backup_type,
backupType: run.backup_type,
backup_mode: run.backup_mode,
backupMode: run.backup_mode,
app_version: run.app_version,
appVersion: run.app_version
});
}
res.json({
success: true,
data: backups,
source: source || 'all'
});
} catch (error) {
logger.error('Failed to list backups for restore:', error);
res.status(500).json({
success: false,
error: 'Failed to list backups for restore'
});
}
});
/**
* Get restore settings
*/
router.get('/settings', async (req, res) => {
router.get('/settings', requirePermission('backup.view'), async (req, res) => {
try {
const settings = await getRestoreSettings();
res.json({
@@ -371,7 +503,7 @@ router.get('/settings', async (req, res) => {
/**
* Update restore settings
*/
router.put('/settings', [
router.put('/settings', requirePermission('backup.restore'), [
body('restore_allow_force').optional().isBoolean(),
body('restore_require_pre_backup').optional().isBoolean(),
body('restore_max_file_size_mb').optional().isInt({ min: 1 }),
+16 -15
View File
@@ -6,6 +6,7 @@ const { body, validationResult } = require('express-validator');
const { db, logActivity } = require('../database/db');
const { formatBoolean } = require('../utils/dbCompat');
const { adminAuth } = require('../middleware/auth');
const { requirePermission } = require('../middleware/permissions');
const { clearMaintenanceCache } = require('../middleware/maintenance');
const { clearSettingsCache } = require('../services/rateLimitService');
const {
@@ -92,7 +93,7 @@ const faviconUpload = multer({
});
// Get all settings
router.get('/', adminAuth, async (req, res) => {
router.get('/', adminAuth, requirePermission('settings.view'), async (req, res) => {
try {
const settings = await db('app_settings').select('*');
@@ -120,7 +121,7 @@ router.get('/', adminAuth, async (req, res) => {
});
// Get settings by type
router.get('/:type', adminAuth, async (req, res) => {
router.get('/:type', adminAuth, requirePermission('settings.view'), async (req, res) => {
try {
const { type } = req.params;
const settings = await db('app_settings')
@@ -151,7 +152,7 @@ router.get('/:type', adminAuth, async (req, res) => {
});
// Get password complexity settings for frontend
router.get('/password/complexity', adminAuth, async (req, res) => {
router.get('/password/complexity', adminAuth, requirePermission('settings.view'), async (req, res) => {
try {
const { getPasswordComplexitySettings, getPasswordConfigForComplexity } = require('../utils/passwordValidation');
@@ -172,7 +173,7 @@ router.get('/password/complexity', adminAuth, async (req, res) => {
});
// Update branding settings
router.put('/branding', adminAuth, async (req, res) => {
router.put('/branding', adminAuth, requirePermission('settings.edit'), async (req, res) => {
try {
const {
company_name,
@@ -313,7 +314,7 @@ router.put('/branding', adminAuth, async (req, res) => {
});
// Upload logo
router.post('/logo', adminAuth, upload.single('logo'), async (req, res) => {
router.post('/logo', adminAuth, requirePermission('settings.edit'), upload.single('logo'), async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ error: 'No logo file uploaded' });
@@ -375,7 +376,7 @@ router.post('/logo', adminAuth, upload.single('logo'), async (req, res) => {
});
// Upload watermark logo
router.post('/branding/watermark-logo', adminAuth, upload.single('watermarkLogo'), async (req, res) => {
router.post('/branding/watermark-logo', adminAuth, requirePermission('settings.edit'), upload.single('watermarkLogo'), async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ error: 'No file uploaded' });
@@ -437,7 +438,7 @@ router.post('/branding/watermark-logo', adminAuth, upload.single('watermarkLogo'
});
// Update theme settings
router.put('/theme', adminAuth, async (req, res) => {
router.put('/theme', adminAuth, requirePermission('settings.edit'), async (req, res) => {
try {
const themeSettings = req.body;
@@ -474,7 +475,7 @@ router.put('/theme', adminAuth, async (req, res) => {
});
// Update general settings
router.put('/general', adminAuth, async (req, res) => {
router.put('/general', adminAuth, requirePermission('settings.edit'), async (req, res) => {
try {
const settings = { ...req.body };
let uploadLimitTouched = false;
@@ -573,7 +574,7 @@ router.put('/general', adminAuth, async (req, res) => {
});
// Update security settings
router.put('/security', adminAuth, async (req, res) => {
router.put('/security', adminAuth, requirePermission('settings.edit'), async (req, res) => {
try {
const settings = req.body;
@@ -612,7 +613,7 @@ router.put('/security', adminAuth, async (req, res) => {
});
// Update analytics settings
router.put('/analytics', adminAuth, async (req, res) => {
router.put('/analytics', adminAuth, requirePermission('settings.edit'), async (req, res) => {
try {
const settings = req.body;
@@ -649,7 +650,7 @@ router.put('/analytics', adminAuth, async (req, res) => {
});
// Get storage info
router.get('/storage/info', adminAuth, async (req, res) => {
router.get('/storage/info', adminAuth, requirePermission('settings.view'), async (req, res) => {
try {
// Get total storage used
const totalStorage = await db('photos')
@@ -877,7 +878,7 @@ router.get('/storage/info', adminAuth, async (req, res) => {
});
// Upload favicon endpoint
router.post('/favicon', adminAuth, faviconUpload.single('favicon'), async (req, res) => {
router.post('/favicon', adminAuth, requirePermission('settings.edit'), faviconUpload.single('favicon'), async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ error: 'No favicon file provided' });
@@ -915,7 +916,7 @@ router.post('/favicon', adminAuth, faviconUpload.single('favicon'), async (req,
});
// Update rate limit settings
router.put('/security/rate-limit', adminAuth, [
router.put('/security/rate-limit', adminAuth, requirePermission('settings.edit'), [
body('rate_limit_enabled').isBoolean().withMessage('Enabled must be a boolean'),
body('rate_limit_window_minutes').isInt({ min: 1, max: 60 }).withMessage('Window must be between 1 and 60 minutes'),
body('rate_limit_max_requests').isInt({ min: 10, max: 10000 }).withMessage('Max requests must be between 10 and 10000'),
@@ -979,7 +980,7 @@ router.put('/security/rate-limit', adminAuth, [
});
// Get default public site template
router.get('/public-site/default', adminAuth, async (req, res) => {
router.get('/public-site/default', adminAuth, requirePermission('settings.view'), async (req, res) => {
try {
const defaults = await getDefaultPublicSitePayload();
@@ -1000,7 +1001,7 @@ router.get('/public-site/default', adminAuth, async (req, res) => {
});
// Reset public site template to defaults
router.post('/public-site/reset', adminAuth, async (req, res) => {
router.post('/public-site/reset', adminAuth, requirePermission('settings.edit'), async (req, res) => {
try {
const entries = [
{
+4 -3
View File
@@ -1,6 +1,7 @@
const express = require('express');
const { db, withRetry } = require('../database/db');
const { adminAuth } = require('../middleware/auth');
const { requirePermission } = require('../middleware/permissions');
const fs = require('fs').promises;
const path = require('path');
const os = require('os');
@@ -9,7 +10,7 @@ const logger = require('../utils/logger');
const router = express.Router();
// Get system version
router.get('/version', adminAuth, async (req, res) => {
router.get('/version', adminAuth, requirePermission('settings.view'), async (req, res) => {
try {
// Read backend version from package.json
let backendVersion = '1.0.0';
@@ -35,7 +36,7 @@ router.get('/version', adminAuth, async (req, res) => {
});
// Get comprehensive system status
router.get('/status', adminAuth, async (req, res) => {
router.get('/status', adminAuth, requirePermission('settings.view'), async (req, res) => {
try {
// Database size - check if PostgreSQL or SQLite
let dbSize = 0;
@@ -170,7 +171,7 @@ router.get('/status', adminAuth, async (req, res) => {
});
// Get database statistics
router.get('/database', adminAuth, async (req, res) => {
router.get('/database', adminAuth, requirePermission('settings.view'), async (req, res) => {
try {
// Get table info
const tables = [
+5 -4
View File
@@ -2,6 +2,7 @@ const express = require('express');
const router = express.Router();
const { db } = require('../database/db');
const { adminAuth } = require('../middleware/auth');
const { requirePermission } = require('../middleware/permissions');
const { generateThumbnail } = require('../services/imageProcessor');
const path = require('path');
const fs = require('fs').promises;
@@ -10,7 +11,7 @@ const logger = require('../utils/logger');
const getStoragePath = () => process.env.STORAGE_PATH || path.join(__dirname, '../../../storage');
// Get thumbnail settings
router.get('/settings', adminAuth, async (req, res) => {
router.get('/settings', adminAuth, requirePermission('photos.view'), async (req, res) => {
try {
const settings = await db('app_settings')
.whereIn('key', [
@@ -42,7 +43,7 @@ router.get('/settings', adminAuth, async (req, res) => {
});
// Update thumbnail settings
router.put('/settings', adminAuth, async (req, res) => {
router.put('/settings', adminAuth, requirePermission('photos.edit'), async (req, res) => {
try {
const { width, height, fit, quality, format } = req.body;
@@ -91,7 +92,7 @@ router.put('/settings', adminAuth, async (req, res) => {
});
// Regenerate all thumbnails with new settings
router.post('/regenerate', adminAuth, async (req, res) => {
router.post('/regenerate', adminAuth, requirePermission('photos.edit'), async (req, res) => {
try {
const { eventId } = req.body; // Optional: regenerate for specific event only
@@ -164,7 +165,7 @@ router.post('/regenerate', adminAuth, async (req, res) => {
});
// Get regeneration status
router.get('/regenerate/status', adminAuth, async (req, res) => {
router.get('/regenerate/status', adminAuth, requirePermission('photos.view'), async (req, res) => {
try {
// Count photos with and without thumbnails
const totalPhotos = await db('photos').count('id as count').first();
+194
View File
@@ -0,0 +1,194 @@
/**
* Admin Users Routes
* Handles user management, roles, and invitations
*/
const express = require('express');
const { body, param } = require('express-validator');
const { adminAuth } = require('../middleware/auth');
const { requirePermission, requireSuperAdmin, getUserPermissions } = require('../middleware/permissions');
const { handleAsync, validateRequest, successResponse } = require('../utils/routeHelpers');
const userManagementService = require('../services/userManagementService');
const router = express.Router();
/**
* Transform user object from snake_case (DB) to camelCase (API)
*/
function transformUser(user) {
return {
id: user.id,
username: user.username,
email: user.email,
isActive: user.is_active,
lastLogin: user.last_login,
lastLoginIp: user.last_login_ip,
createdAt: user.created_at,
updatedAt: user.updated_at,
roleId: user.role_id,
roleName: user.role_name,
roleDisplayName: user.role_display_name,
createdByUsername: user.created_by_username
};
}
/**
* Transform role object from snake_case (DB) to camelCase (API)
*/
function transformRole(role) {
return {
id: role.id,
name: role.name,
displayName: role.display_name,
description: role.description,
isSystem: role.is_system,
priority: role.priority
};
}
/**
* GET /me/permissions
* Get current user's permissions
*/
router.get('/me/permissions', adminAuth, handleAsync(async (req, res) => {
const permissions = await getUserPermissions(req.admin.id);
res.json(permissions);
}));
/**
* GET /
* List all admin users
* Requires: users.view permission
*/
router.get('/', adminAuth, requirePermission('users.view'), handleAsync(async (req, res) => {
const users = await userManagementService.getAllAdminUsers();
res.json({ users: users.map(transformUser) });
}));
/**
* GET /roles
* List all roles
* Requires: users.view permission
*/
router.get('/roles', adminAuth, requirePermission('users.view'), handleAsync(async (req, res) => {
const roles = await userManagementService.getAllRoles();
res.json({ roles: roles.map(transformRole) });
}));
/**
* GET /invitations
* List pending invitations
* Requires: users.view permission
*/
router.get('/invitations', adminAuth, requirePermission('users.view'), handleAsync(async (req, res) => {
const invitations = await userManagementService.getPendingInvitations();
res.json({ invitations });
}));
/**
* POST /invite
* Create invitation
* Requires: users.create permission
*/
router.post('/invite', [
adminAuth,
requirePermission('users.create'),
body('email').isEmail().normalizeEmail().withMessage('Valid email is required'),
body('role_id').isInt({ min: 1 }).withMessage('Role ID is required')
], handleAsync(async (req, res) => {
validateRequest(req);
const invitation = await userManagementService.createInvitation({
email: req.body.email,
roleId: req.body.role_id,
invitedById: req.admin.id
});
successResponse(res, { invitation }, 201);
}));
/**
* DELETE /invitations/:id
* Cancel invitation
* Requires: users.create permission
*/
router.delete('/invitations/:id', [
adminAuth,
requirePermission('users.create'),
param('id').isInt({ min: 1 }).withMessage('Valid invitation ID is required')
], handleAsync(async (req, res) => {
validateRequest(req);
await userManagementService.cancelInvitation(parseInt(req.params.id), req.admin.id);
successResponse(res, { message: 'Invitation cancelled' });
}));
/**
* GET /:id
* Get single user
* Requires: users.view permission
*/
router.get('/:id', [
adminAuth,
requirePermission('users.view'),
param('id').isInt({ min: 1 }).withMessage('Valid user ID is required')
], handleAsync(async (req, res) => {
validateRequest(req);
const user = await userManagementService.getAdminUserById(parseInt(req.params.id));
res.json({ user: transformUser(user) });
}));
/**
* PUT /:id
* Update user
* Requires: users.edit permission
*/
router.put('/:id', [
adminAuth,
requirePermission('users.edit'),
param('id').isInt({ min: 1 }).withMessage('Valid user ID is required'),
body('username').optional().trim().isLength({ min: 3, max: 50 }).withMessage('Username must be 3-50 characters'),
body('email').optional().isEmail().normalizeEmail().withMessage('Valid email is required'),
body('role_id').optional().isInt({ min: 1 }).withMessage('Valid role ID is required'),
body('is_active').optional().isBoolean().withMessage('is_active must be boolean')
], handleAsync(async (req, res) => {
validateRequest(req);
const user = await userManagementService.updateAdminUser(
parseInt(req.params.id),
req.body,
req.admin.id
);
successResponse(res, { user: transformUser(user), message: 'User updated successfully' });
}));
/**
* POST /:id/deactivate
* Deactivate user
* Requires: users.delete permission
*/
router.post('/:id/deactivate', [
adminAuth,
requirePermission('users.delete'),
param('id').isInt({ min: 1 }).withMessage('Valid user ID is required')
], handleAsync(async (req, res) => {
validateRequest(req);
await userManagementService.deactivateAdminUser(parseInt(req.params.id), req.admin.id);
successResponse(res, { message: 'User deactivated successfully' });
}));
/**
* POST /:id/reset-password
* Reset user password
* Requires: super_admin role
*/
router.post('/:id/reset-password', [
adminAuth,
requireSuperAdmin(),
param('id').isInt({ min: 1 }).withMessage('Valid user ID is required')
], handleAsync(async (req, res) => {
validateRequest(req);
const result = await userManagementService.resetAdminPassword(parseInt(req.params.id), req.admin.id);
successResponse(res, { message: 'Password reset email sent', ...result });
}));
module.exports = router;
+26 -13
View File
@@ -70,52 +70,65 @@ router.post('/admin/login', [
logger.warn('Suspicious login pattern detected', { username, ipAddress });
}
// Fetch admin with role information
const admin = await db('admin_users')
.where({ username })
.orWhere({ email: username })
.leftJoin('roles', 'roles.id', 'admin_users.role_id')
.where('admin_users.username', username)
.orWhere('admin_users.email', username)
.select(
'admin_users.*',
'roles.name as role_name',
'roles.display_name as role_display_name'
)
.first();
// Use generic error to prevent user enumeration
if (!admin || !await bcrypt.compare(password, admin.password_hash)) {
await trackFailedAttempt(username, ipAddress, userAgent);
return res.status(401).json({ error: getGenericAuthError() });
}
if (!admin.is_active) {
await trackFailedAttempt(username, ipAddress, userAgent);
return res.status(401).json({ error: getGenericAuthError() });
}
// Successful login
await trackSuccessfulLogin(username, ipAddress, userAgent);
// Update last login and login metadata
await db('admin_users').where('id', admin.id).update({
await db('admin_users').where('id', admin.id).update({
last_login: new Date(),
last_login_ip: ipAddress
});
// Generate token with additional claims
const token = jwt.sign({
// Generate token with additional claims including role
const token = jwt.sign({
id: admin.id,
username: admin.username,
type: 'admin',
role: admin.role_name, // Add role to JWT
ip: ipAddress,
loginTime: Date.now()
}, process.env.JWT_SECRET, {
}, process.env.JWT_SECRET, {
expiresIn: '24h',
issuer: 'picpeak-auth'
});
setAdminAuthCookie(res, token);
// Include role in response
res.json({
token,
user: {
id: admin.id,
username: admin.username,
email: admin.email,
mustChangePassword: admin.must_change_password || false
mustChangePassword: admin.must_change_password || false,
role: admin.role_name ? {
name: admin.role_name,
displayName: admin.role_display_name
} : null
}
});
} catch (error) {