Fix admin interface issues and 404 errors

- Create adminEvents.js router to handle /api/admin/events endpoints
- Mount events router in admin.js to fix 404 errors
- Fix admin layout CSS - changed from static to flex layout
- Update AdminSidebar positioning from static to relative
- Add missing PUT endpoints for general and security settings
- Fix frontend environment variables in docker-compose.local.yml
- Add build args to Dockerfile.dev for environment variables
- Update CORS to accept requests from all dev servers
- Remove unused imports from SettingsPage

This fixes:
- Events page 404 error
- Admin layout misalignment (sidebar and content on different rows)
- Settings page not loading
- CORS issues between frontend and backend

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-07-07 08:40:26 +02:00
parent 024c8eac2d
commit f38a8ef598
10 changed files with 417 additions and 15 deletions
+22 -3
View File
@@ -20,10 +20,29 @@ const PORT = process.env.PORT || 3000;
// Security middleware
app.use(helmet());
app.use(cors({
origin: process.env.FRONTEND_URL || 'http://localhost:3005',
// CORS configuration
const corsOptions = {
origin: function (origin, callback) {
const allowedOrigins = [
process.env.FRONTEND_URL || 'http://localhost:3005',
process.env.ADMIN_URL || 'http://localhost:3005',
'http://localhost:3002', // Vite dev server
'http://localhost:3001', // For API testing
'http://localhost:3000' // Direct backend access
];
// Allow requests with no origin (like mobile apps or curl)
if (!origin || allowedOrigins.indexOf(origin) !== -1) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true
}));
};
app.use(cors(corsOptions));
// Rate limiting
const limiter = rateLimit({
+2
View File
@@ -6,11 +6,13 @@ const dashboardRoutes = require('./adminDashboard');
const archiveRoutes = require('./adminArchives');
const emailRoutes = require('./adminEmail');
const settingsRoutes = require('./adminSettings');
const eventsRoutes = require('./adminEvents');
// Mount sub-routers
router.use('/dashboard', dashboardRoutes);
router.use('/archives', archiveRoutes);
router.use('/email', emailRoutes);
router.use('/settings', settingsRoutes);
router.use('/events', eventsRoutes);
module.exports = router;
+295
View File
@@ -0,0 +1,295 @@
const express = require('express');
const { body, query, validationResult } = require('express-validator');
const { db } = require('../database/db');
const { adminAuth } = require('../middleware/auth');
const router = express.Router();
// Get all events with pagination and filters
router.get('/', adminAuth, async (req, res) => {
try {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 20;
const offset = (page - 1) * limit;
const search = req.query.search || '';
const status = req.query.status || 'all';
const sortBy = req.query.sortBy || 'created_at';
const sortOrder = req.query.sortOrder || 'desc';
// Build query
let query = db('events');
// Apply search filter
if (search) {
query = query.where((builder) => {
builder.where('event_name', 'like', `%${search}%`)
.orWhere('admin_email', 'like', `%${search}%`)
.orWhere('slug', 'like', `%${search}%`);
});
}
// Apply status filter
if (status === 'active') {
query = query.where('is_active', true).where('is_archived', false);
} else if (status === 'archived') {
query = query.where('is_archived', true);
} else if (status === 'inactive') {
query = query.where('is_active', false).where('is_archived', false);
} else if (status === 'expiring') {
const sevenDaysFromNow = new Date();
sevenDaysFromNow.setDate(sevenDaysFromNow.getDate() + 7);
query = query
.where('is_active', true)
.where('is_archived', false)
.where('expires_at', '<=', sevenDaysFromNow.toISOString())
.where('expires_at', '>', new Date().toISOString());
}
// Get total count for pagination
const countQuery = query.clone();
const [{ count }] = await countQuery.count('* as count');
// Apply sorting and pagination
const events = await query
.orderBy(sortBy, sortOrder)
.limit(limit)
.offset(offset);
// Get photo counts for each event
const eventIds = events.map(e => e.id);
const photoCounts = await db('photos')
.whereIn('event_id', eventIds)
.groupBy('event_id')
.select('event_id')
.count('* as count');
// Map photo counts to events
const photoCountMap = photoCounts.reduce((acc, { event_id, count }) => {
acc[event_id] = parseInt(count);
return acc;
}, {});
// Add photo counts to events
const eventsWithCounts = events.map(event => ({
...event,
photo_count: photoCountMap[event.id] || 0
}));
res.json({
events: eventsWithCounts,
pagination: {
page,
limit,
total: parseInt(count),
totalPages: Math.ceil(count / limit)
}
});
} catch (error) {
console.error('Error fetching events:', error);
res.status(500).json({ error: 'Failed to fetch events' });
}
});
// Get single event details
router.get('/:id', adminAuth, async (req, res) => {
try {
const { id } = req.params;
const event = await db('events')
.where('id', id)
.first();
if (!event) {
return res.status(404).json({ error: 'Event not found' });
}
// Get photo count
const [{ count: photoCount }] = await db('photos')
.where('event_id', id)
.count('* as count');
// Get total size
const [{ totalSize }] = await db('photos')
.where('event_id', id)
.sum('size_bytes as totalSize');
// Get recent photos
const recentPhotos = await db('photos')
.where('event_id', id)
.orderBy('uploaded_at', 'desc')
.limit(10)
.select('filename', 'type', 'size_bytes', 'uploaded_at');
res.json({
...event,
photo_count: parseInt(photoCount) || 0,
total_size: parseInt(totalSize) || 0,
recent_photos: recentPhotos
});
} catch (error) {
console.error('Error fetching event:', error);
res.status(500).json({ error: 'Failed to fetch event details' });
}
});
// Update event
router.put('/:id', adminAuth, [
body('event_name').optional().trim().notEmpty(),
body('admin_email').optional().isEmail(),
body('is_active').optional().isBoolean(),
body('expires_at').optional().isISO8601(),
body('welcome_message').optional().trim(),
body('color_theme').optional().trim()
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { id } = req.params;
const updates = req.body;
// Check if event exists
const event = await db('events').where('id', id).first();
if (!event) {
return res.status(404).json({ error: 'Event not found' });
}
// Update event
await db('events')
.where('id', id)
.update({
...updates,
updated_at: new Date()
});
// Log activity
await db.logActivity({
type: 'event_updated',
actorType: 'admin',
actorId: req.user.id,
actorName: req.user.username,
eventId: id,
eventName: event.event_name,
metadata: { changes: Object.keys(updates) }
});
res.json({ message: 'Event updated successfully' });
} catch (error) {
console.error('Error updating event:', error);
res.status(500).json({ error: 'Failed to update event' });
}
});
// Delete event
router.delete('/:id', adminAuth, async (req, res) => {
try {
const { id } = req.params;
// Check if event exists
const event = await db('events').where('id', id).first();
if (!event) {
return res.status(404).json({ error: 'Event not found' });
}
// Delete associated photos
await db('photos').where('event_id', id).del();
// Delete event
await db('events').where('id', id).del();
// Log activity
await db.logActivity({
type: 'event_deleted',
actorType: 'admin',
actorId: req.user.id,
actorName: req.user.username,
metadata: { event_name: event.event_name }
});
res.json({ message: 'Event deleted successfully' });
} catch (error) {
console.error('Error deleting event:', error);
res.status(500).json({ error: 'Failed to delete event' });
}
});
// Toggle event status
router.post('/:id/toggle-status', adminAuth, async (req, res) => {
try {
const { id } = req.params;
const event = await db('events').where('id', id).first();
if (!event) {
return res.status(404).json({ error: 'Event not found' });
}
const newStatus = !event.is_active;
await db('events')
.where('id', id)
.update({
is_active: newStatus,
updated_at: new Date()
});
// Log activity
await db.logActivity({
type: newStatus ? 'event_activated' : 'event_deactivated',
actorType: 'admin',
actorId: req.user.id,
actorName: req.user.username,
eventId: id,
eventName: event.event_name
});
res.json({
message: `Event ${newStatus ? 'activated' : 'deactivated'} successfully`,
is_active: newStatus
});
} catch (error) {
console.error('Error toggling event status:', error);
res.status(500).json({ error: 'Failed to toggle event status' });
}
});
// Archive event
router.post('/:id/archive', adminAuth, async (req, res) => {
try {
const { id } = req.params;
const event = await db('events').where('id', id).first();
if (!event) {
return res.status(404).json({ error: 'Event not found' });
}
if (event.is_archived) {
return res.status(400).json({ error: 'Event is already archived' });
}
await db('events')
.where('id', id)
.update({
is_archived: true,
is_active: false,
archived_at: new Date(),
updated_at: new Date()
});
// Log activity
await db.logActivity({
type: 'event_archived',
actorType: 'admin',
actorId: req.user.id,
actorName: req.user.username,
eventId: id,
eventName: event.event_name
});
res.json({ message: 'Event archived successfully' });
} catch (error) {
console.error('Error archiving event:', error);
res.status(500).json({ error: 'Failed to archive event' });
}
});
module.exports = router;
+74
View File
@@ -227,6 +227,80 @@ router.put('/theme', adminAuth, async (req, res) => {
}
});
// Update general settings
router.put('/general', adminAuth, async (req, res) => {
try {
const settings = req.body;
// Update or insert each setting
for (const [key, value] of Object.entries(settings)) {
await db('app_settings')
.insert({
setting_key: key,
setting_value: JSON.stringify(value),
setting_type: 'general',
updated_at: new Date()
})
.onConflict('setting_key')
.merge({
setting_value: JSON.stringify(value),
updated_at: new Date()
});
}
// Log activity
await db('activity_logs').insert({
activity_type: 'general_settings_updated',
actor_type: 'admin',
actor_id: req.user.id,
actor_name: req.user.username,
metadata: JSON.stringify({ settings_count: Object.keys(settings).length })
});
res.json({ message: 'General settings updated successfully' });
} catch (error) {
console.error('General settings update error:', error);
res.status(500).json({ error: 'Failed to update general settings' });
}
});
// Update security settings
router.put('/security', adminAuth, async (req, res) => {
try {
const settings = req.body;
// Update or insert each setting
for (const [key, value] of Object.entries(settings)) {
await db('app_settings')
.insert({
setting_key: key,
setting_value: JSON.stringify(value),
setting_type: 'security',
updated_at: new Date()
})
.onConflict('setting_key')
.merge({
setting_value: JSON.stringify(value),
updated_at: new Date()
});
}
// Log activity
await db('activity_logs').insert({
activity_type: 'security_settings_updated',
actor_type: 'admin',
actor_id: req.user.id,
actor_name: req.user.username,
metadata: JSON.stringify({ settings_count: Object.keys(settings).length })
});
res.json({ message: 'Security settings updated successfully' });
} catch (error) {
console.error('Security settings update error:', error);
res.status(500).json({ error: 'Failed to update security settings' });
}
});
// Get storage info
router.get('/storage/info', adminAuth, async (req, res) => {
try {
+7
View File
@@ -42,6 +42,10 @@ services:
build:
context: ./frontend
dockerfile: Dockerfile.dev
args:
- VITE_API_URL=http://localhost:3001
- VITE_UMAMI_URL=
- VITE_UMAMI_WEBSITE_ID=
ports:
- "3005:80"
environment:
@@ -65,6 +69,9 @@ services:
- "3002:5173"
environment:
- NODE_ENV=development
- VITE_API_URL=http://localhost:3001
- VITE_UMAMI_URL=
- VITE_UMAMI_WEBSITE_ID=
volumes:
- ./frontend:/app
- /app/node_modules
+1 -1
View File
@@ -1,5 +1,5 @@
# Backend API URL
VITE_API_URL=http://localhost:3001
VITE_API_URL=http://localhost:3000
# Umami Analytics Configuration
# Get these values from your Umami installation
+10
View File
@@ -3,6 +3,16 @@ FROM node:18-alpine AS builder
WORKDIR /app
# Accept build arguments
ARG VITE_API_URL
ARG VITE_UMAMI_URL
ARG VITE_UMAMI_WEBSITE_ID
# Set environment variables for build
ENV VITE_API_URL=$VITE_API_URL
ENV VITE_UMAMI_URL=$VITE_UMAMI_URL
ENV VITE_UMAMI_WEBSITE_ID=$VITE_UMAMI_WEBSITE_ID
# Copy package files
COPY package*.json ./
@@ -25,7 +25,7 @@ export const AdminLayout: React.FC = () => {
}
return (
<div className="min-h-screen bg-neutral-50">
<div className="min-h-screen bg-neutral-50 flex">
{/* Mobile sidebar backdrop */}
{sidebarOpen && (
<div
@@ -38,12 +38,12 @@ export const AdminLayout: React.FC = () => {
<AdminSidebar isOpen={sidebarOpen} onClose={() => setSidebarOpen(false)} />
{/* Main content */}
<div className="lg:pl-64">
<div className="flex-1 flex flex-col min-w-0">
{/* Header */}
<AdminHeader onMenuClick={() => setSidebarOpen(true)} />
{/* Page content */}
<main id="main-content" className="px-4 sm:px-6 lg:px-8 py-8">
<main id="main-content" className="flex-1 px-4 sm:px-6 lg:px-8 py-8">
<Outlet />
</main>
</div>
@@ -40,7 +40,7 @@ export const AdminSidebar: React.FC<AdminSidebarProps> = ({ isOpen, onClose }) =
return (
<div
className={`fixed inset-y-0 left-0 z-50 w-64 bg-white border-r border-neutral-200 transform transition-transform duration-200 ease-in-out lg:translate-x-0 lg:static ${
className={`fixed inset-y-0 left-0 z-50 w-64 bg-white border-r border-neutral-200 transform transition-transform duration-200 ease-in-out lg:relative lg:translate-x-0 ${
isOpen ? 'translate-x-0' : '-translate-x-full'
}`}
>
@@ -87,7 +87,6 @@ export const AdminSidebar: React.FC<AdminSidebarProps> = ({ isOpen, onClose }) =
{/* Storage Info */}
<StorageInfo />
</div>
</div>
</div>
);
+2 -6
View File
@@ -1,20 +1,16 @@
import React, { useState } from 'react';
import {
Save,
Shield,
HardDrive,
Bell,
Database,
Globe,
Key,
AlertCircle,
CheckCircle
AlertCircle
} from 'lucide-react';
import { toast } from 'react-toastify';
import { Button, Card, Input, Loading } from '../../components/common';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { settingsService, type BrandingSettings, type ThemeSettings, type StorageInfo } from '../../services/settings.service';
import { settingsService } from '../../services/settings.service';
export const SettingsPage: React.FC = () => {
const [activeTab, setActiveTab] = useState<'general' | 'storage' | 'security'>('general');