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:
+22
-3
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user