Fix multiple production issues and add password change functionality
- Fixed frontend API URL configuration to use correct port 3002 - Fixed create event functionality by adding proper endpoint and fixing JSON parsing - Fixed email settings save functionality by importing logActivity correctly - Fixed admin settings save functionality by using api client instead of direct fetch - Implemented password change functionality with modal and backend endpoint - Added updated_at column to admin_users table - Fixed all mock data issues - now using real backend data throughout 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Binary file not shown.
+4
-1
@@ -14,6 +14,7 @@ const authRoutes = require('./src/routes/auth');
|
||||
const eventRoutes = require('./src/routes/events');
|
||||
const galleryRoutes = require('./src/routes/gallery');
|
||||
const adminRoutes = require('./src/routes/admin');
|
||||
const adminAuthRoutes = require('./src/routes/adminAuth');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
@@ -27,7 +28,8 @@ const corsOptions = {
|
||||
const allowedOrigins = [
|
||||
process.env.FRONTEND_URL || 'http://localhost:3005',
|
||||
process.env.ADMIN_URL || 'http://localhost:3005',
|
||||
'http://localhost:3002', // Vite dev server
|
||||
'http://localhost:5173', // Vite dev server
|
||||
'http://localhost:3002', // Backend server
|
||||
'http://localhost:3001', // For API testing
|
||||
'http://localhost:3000' // Direct backend access
|
||||
];
|
||||
@@ -75,6 +77,7 @@ app.use('/api/auth', authRoutes);
|
||||
app.use('/api/events', eventRoutes);
|
||||
app.use('/api/gallery', galleryRoutes);
|
||||
app.use('/api/admin', adminRoutes);
|
||||
app.use('/api/admin/auth', adminAuthRoutes);
|
||||
|
||||
// Error handling middleware
|
||||
app.use((err, req, res, next) => {
|
||||
|
||||
@@ -92,8 +92,17 @@ async function initializeDatabase() {
|
||||
table.string('password_hash').notNullable();
|
||||
table.boolean('is_active').defaultTo(true);
|
||||
table.datetime('created_at').defaultTo(db.fn.now());
|
||||
table.datetime('updated_at').defaultTo(db.fn.now());
|
||||
table.datetime('last_login');
|
||||
});
|
||||
} else {
|
||||
// Check if updated_at column exists
|
||||
const hasUpdatedAt = await db.schema.hasColumn('admin_users', 'updated_at');
|
||||
if (!hasUpdatedAt) {
|
||||
await db.schema.table('admin_users', (table) => {
|
||||
table.datetime('updated_at').defaultTo(db.fn.now());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Email configuration table
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
const express = require('express');
|
||||
const bcrypt = require('bcrypt');
|
||||
const { body, validationResult } = require('express-validator');
|
||||
const { db } = require('../database/db');
|
||||
const { adminAuth } = require('../middleware/auth');
|
||||
const router = express.Router();
|
||||
|
||||
// Change password
|
||||
router.post('/change-password', [
|
||||
adminAuth,
|
||||
body('currentPassword').notEmpty().withMessage('Current password is required'),
|
||||
body('newPassword').isLength({ min: 6 }).withMessage('New password must be at least 6 characters')
|
||||
], async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({ errors: errors.array() });
|
||||
}
|
||||
|
||||
const { currentPassword, newPassword } = req.body;
|
||||
const userId = req.user.id;
|
||||
|
||||
// Get user from database
|
||||
const user = await db('admin_users')
|
||||
.where('id', userId)
|
||||
.first();
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({ error: 'User not found' });
|
||||
}
|
||||
|
||||
// Verify current password
|
||||
const validPassword = await bcrypt.compare(currentPassword, user.password_hash);
|
||||
if (!validPassword) {
|
||||
return res.status(400).json({ error: 'Current password is incorrect' });
|
||||
}
|
||||
|
||||
// Hash new password
|
||||
const newPasswordHash = await bcrypt.hash(newPassword, 10);
|
||||
|
||||
// Update password
|
||||
await db('admin_users')
|
||||
.where('id', userId)
|
||||
.update({
|
||||
password_hash: newPasswordHash,
|
||||
updated_at: new Date()
|
||||
});
|
||||
|
||||
res.json({ message: 'Password changed successfully' });
|
||||
} catch (error) {
|
||||
console.error('Password change error:', error);
|
||||
res.status(500).json({ error: 'Failed to change password' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,7 +1,7 @@
|
||||
const express = require('express');
|
||||
const nodemailer = require('nodemailer');
|
||||
const { body, validationResult } = require('express-validator');
|
||||
const { db } = require('../database/db');
|
||||
const { db, logActivity } = require('../database/db');
|
||||
const { adminAuth } = require('../middleware/auth');
|
||||
const router = express.Router();
|
||||
|
||||
@@ -83,13 +83,11 @@ router.post('/config', [
|
||||
}
|
||||
|
||||
// Log activity
|
||||
await db('activity_logs').insert({
|
||||
activity_type: 'email_config_updated',
|
||||
actor_type: 'admin',
|
||||
actor_id: req.user.id,
|
||||
actor_name: req.user.username,
|
||||
metadata: JSON.stringify({ smtp_host, from_email })
|
||||
});
|
||||
await logActivity('email_config_updated',
|
||||
{ smtp_host, from_email },
|
||||
null,
|
||||
{ type: 'admin', id: req.user.id, name: req.user.username }
|
||||
);
|
||||
|
||||
res.json({ message: 'Email configuration updated successfully' });
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,8 +1,120 @@
|
||||
const express = require('express');
|
||||
const { body, query, validationResult } = require('express-validator');
|
||||
const { db } = require('../database/db');
|
||||
const { db, logActivity } = require('../database/db');
|
||||
const { adminAuth } = require('../middleware/auth');
|
||||
const router = express.Router();
|
||||
const bcrypt = require('bcrypt');
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
|
||||
// Create new event
|
||||
router.post('/', adminAuth, [
|
||||
body('event_type').isIn(['wedding', 'birthday', 'corporate', 'other']),
|
||||
body('event_name').notEmpty().trim(),
|
||||
body('event_date').isDate(),
|
||||
body('host_email').isEmail().normalizeEmail(),
|
||||
body('admin_email').isEmail().normalizeEmail(),
|
||||
body('password').isLength({ min: 6 }),
|
||||
body('expiration_days').isInt({ min: 1, max: 365 }).optional(),
|
||||
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 {
|
||||
event_type,
|
||||
event_name,
|
||||
event_date,
|
||||
host_email,
|
||||
admin_email,
|
||||
password,
|
||||
welcome_message = '',
|
||||
color_theme = 'default',
|
||||
expiration_days = 30
|
||||
} = req.body;
|
||||
|
||||
// Generate unique slug
|
||||
const baseSlug = `${event_type}-${event_name.toLowerCase().replace(/[^a-z0-9]/g, '-')}-${event_date}`;
|
||||
let slug = baseSlug;
|
||||
let counter = 1;
|
||||
|
||||
while (await db('events').where({ slug }).first()) {
|
||||
slug = `${baseSlug}-${counter}`;
|
||||
counter++;
|
||||
}
|
||||
|
||||
// Generate share link
|
||||
const shareToken = crypto.randomBytes(16).toString('hex');
|
||||
const shareLink = `${process.env.FRONTEND_URL}/gallery/${slug}/${shareToken}`;
|
||||
|
||||
// Hash password
|
||||
const password_hash = await bcrypt.hash(password, 10);
|
||||
|
||||
// Calculate expiration date
|
||||
const expires_at = new Date();
|
||||
expires_at.setDate(expires_at.getDate() + expiration_days);
|
||||
|
||||
// Create folder structure
|
||||
const eventPath = path.join(__dirname, '../../../storage/events/active', slug);
|
||||
await fs.mkdir(path.join(eventPath, 'collages'), { recursive: true });
|
||||
await fs.mkdir(path.join(eventPath, 'individual'), { recursive: true });
|
||||
|
||||
// Insert into database
|
||||
const [eventId] = await db('events').insert({
|
||||
slug,
|
||||
event_type,
|
||||
event_name,
|
||||
event_date,
|
||||
host_email,
|
||||
admin_email,
|
||||
password_hash,
|
||||
welcome_message,
|
||||
color_theme,
|
||||
share_link: shareLink,
|
||||
expires_at,
|
||||
created_at: new Date(),
|
||||
updated_at: new Date()
|
||||
});
|
||||
|
||||
// Log activity
|
||||
await logActivity('event_created',
|
||||
{ event_type, expires_at },
|
||||
eventId,
|
||||
{ type: 'admin', id: req.user.id, name: req.user.username }
|
||||
);
|
||||
|
||||
// Queue creation email
|
||||
await db('email_queue').insert({
|
||||
event_id: eventId,
|
||||
recipient_email: host_email,
|
||||
email_type: 'creation',
|
||||
email_data: JSON.stringify({
|
||||
event_name,
|
||||
share_link: shareLink,
|
||||
password,
|
||||
expires_at: expires_at.toISOString()
|
||||
}),
|
||||
created_at: new Date()
|
||||
});
|
||||
|
||||
res.json({
|
||||
id: eventId,
|
||||
slug,
|
||||
event_name,
|
||||
event_type,
|
||||
share_link: shareLink,
|
||||
expires_at
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error creating event:', error);
|
||||
res.status(500).json({ error: 'Failed to create event' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get all events with pagination and filters
|
||||
router.get('/', adminAuth, async (req, res) => {
|
||||
@@ -164,15 +276,11 @@ router.put('/:id', adminAuth, [
|
||||
});
|
||||
|
||||
// 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) }
|
||||
});
|
||||
await logActivity('event_updated',
|
||||
{ changes: Object.keys(updates), eventName: event.event_name },
|
||||
id,
|
||||
{ type: 'admin', id: req.user.id, name: req.user.username }
|
||||
);
|
||||
|
||||
res.json({ message: 'Event updated successfully' });
|
||||
} catch (error) {
|
||||
@@ -199,13 +307,11 @@ router.delete('/:id', adminAuth, async (req, res) => {
|
||||
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 }
|
||||
});
|
||||
await logActivity('event_deleted',
|
||||
{ event_name: event.event_name },
|
||||
null,
|
||||
{ type: 'admin', id: req.user.id, name: req.user.username }
|
||||
);
|
||||
|
||||
res.json({ message: 'Event deleted successfully' });
|
||||
} catch (error) {
|
||||
@@ -233,14 +339,11 @@ router.post('/:id/toggle-status', adminAuth, async (req, res) => {
|
||||
});
|
||||
|
||||
// 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
|
||||
});
|
||||
await logActivity(newStatus ? 'event_activated' : 'event_deactivated',
|
||||
{ eventName: event.event_name },
|
||||
id,
|
||||
{ type: 'admin', id: req.user.id, name: req.user.username }
|
||||
);
|
||||
|
||||
res.json({
|
||||
message: `Event ${newStatus ? 'activated' : 'deactivated'} successfully`,
|
||||
@@ -276,14 +379,11 @@ router.post('/:id/archive', adminAuth, async (req, res) => {
|
||||
});
|
||||
|
||||
// Log activity
|
||||
await db.logActivity({
|
||||
type: 'event_archived',
|
||||
actorType: 'admin',
|
||||
actorId: req.user.id,
|
||||
actorName: req.user.username,
|
||||
eventId: id,
|
||||
eventName: event.event_name
|
||||
});
|
||||
await logActivity('event_archived',
|
||||
{ eventName: event.event_name },
|
||||
id,
|
||||
{ type: 'admin', id: req.user.id, name: req.user.username }
|
||||
);
|
||||
|
||||
res.json({ message: 'Event archived successfully' });
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Menu, User, LogOut, Settings, Bell } from 'lucide-react';
|
||||
import { Menu, User, LogOut, Settings, Bell, Lock } from 'lucide-react';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
import { useAdminAuth } from '../../contexts';
|
||||
import { useOnClickOutside } from '../../hooks/useOnClickOutside';
|
||||
import { PasswordChangeModal } from './PasswordChangeModal';
|
||||
|
||||
interface AdminHeaderProps {
|
||||
onMenuClick: () => void;
|
||||
@@ -15,6 +16,7 @@ export const AdminHeader: React.FC<AdminHeaderProps> = ({ onMenuClick }) => {
|
||||
const { user, logout } = useAdminAuth();
|
||||
const [showUserMenu, setShowUserMenu] = useState(false);
|
||||
const [showNotifications, setShowNotifications] = useState(false);
|
||||
const [showPasswordModal, setShowPasswordModal] = useState(false);
|
||||
|
||||
const userMenuRef = useRef<HTMLDivElement>(null);
|
||||
const notificationRef = useRef<HTMLDivElement>(null);
|
||||
@@ -136,6 +138,16 @@ export const AdminHeader: React.FC<AdminHeaderProps> = ({ onMenuClick }) => {
|
||||
<Settings className="w-4 h-4" />
|
||||
Settings
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowUserMenu(false);
|
||||
setShowPasswordModal(true);
|
||||
}}
|
||||
className="w-full px-4 py-2 text-left text-sm text-neutral-700 hover:bg-neutral-50 flex items-center gap-3"
|
||||
>
|
||||
<Lock className="w-4 h-4" />
|
||||
Change Password
|
||||
</button>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="w-full px-4 py-2 text-left text-sm text-neutral-700 hover:bg-neutral-50 flex items-center gap-3"
|
||||
@@ -149,6 +161,12 @@ export const AdminHeader: React.FC<AdminHeaderProps> = ({ onMenuClick }) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Password Change Modal */}
|
||||
<PasswordChangeModal
|
||||
isOpen={showPasswordModal}
|
||||
onClose={() => setShowPasswordModal(false)}
|
||||
/>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,234 @@
|
||||
import React, { useState } from 'react';
|
||||
import { X, Lock, Eye, EyeOff, AlertCircle } from 'lucide-react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
|
||||
import { Button, Input, Card } from '../common';
|
||||
import { adminService } from '../../services/admin.service';
|
||||
|
||||
interface PasswordChangeModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const PasswordChangeModal: React.FC<PasswordChangeModalProps> = ({ isOpen, onClose }) => {
|
||||
const [formData, setFormData] = useState({
|
||||
currentPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: ''
|
||||
});
|
||||
const [showPasswords, setShowPasswords] = useState({
|
||||
current: false,
|
||||
new: false,
|
||||
confirm: false
|
||||
});
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
|
||||
const changePasswordMutation = useMutation({
|
||||
mutationFn: adminService.changePassword,
|
||||
onSuccess: () => {
|
||||
toast.success('Password changed successfully');
|
||||
onClose();
|
||||
// Reset form
|
||||
setFormData({
|
||||
currentPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: ''
|
||||
});
|
||||
setErrors({});
|
||||
},
|
||||
onError: (error: any) => {
|
||||
if (error.response?.data?.error) {
|
||||
toast.error(error.response.data.error);
|
||||
} else {
|
||||
toast.error('Failed to change password');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const validateForm = (): boolean => {
|
||||
const newErrors: Record<string, string> = {};
|
||||
|
||||
if (!formData.currentPassword) {
|
||||
newErrors.currentPassword = 'Current password is required';
|
||||
}
|
||||
|
||||
if (!formData.newPassword) {
|
||||
newErrors.newPassword = 'New password is required';
|
||||
} else if (formData.newPassword.length < 6) {
|
||||
newErrors.newPassword = 'Password must be at least 6 characters';
|
||||
}
|
||||
|
||||
if (!formData.confirmPassword) {
|
||||
newErrors.confirmPassword = 'Please confirm your new password';
|
||||
} else if (formData.newPassword !== formData.confirmPassword) {
|
||||
newErrors.confirmPassword = 'Passwords do not match';
|
||||
}
|
||||
|
||||
if (formData.currentPassword === formData.newPassword) {
|
||||
newErrors.newPassword = 'New password must be different from current password';
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
changePasswordMutation.mutate({
|
||||
currentPassword: formData.currentPassword,
|
||||
newPassword: formData.newPassword
|
||||
});
|
||||
};
|
||||
|
||||
const handleInputChange = (field: keyof typeof formData) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setFormData(prev => ({ ...prev, [field]: e.target.value }));
|
||||
// Clear error when user types
|
||||
if (errors[field]) {
|
||||
setErrors(prev => ({ ...prev, [field]: '' }));
|
||||
}
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
||||
<Card className="w-full max-w-md">
|
||||
<div className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-xl font-semibold text-neutral-900">Change Password</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-1 hover:bg-neutral-100 rounded-lg transition-colors"
|
||||
>
|
||||
<X className="w-5 h-5 text-neutral-500" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{/* Current Password */}
|
||||
<div>
|
||||
<label htmlFor="currentPassword" className="block text-sm font-medium text-neutral-700 mb-1">
|
||||
Current Password
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="currentPassword"
|
||||
type={showPasswords.current ? 'text' : 'password'}
|
||||
value={formData.currentPassword}
|
||||
onChange={handleInputChange('currentPassword')}
|
||||
error={errors.currentPassword}
|
||||
placeholder="Enter current password"
|
||||
leftIcon={<Lock className="w-5 h-5 text-neutral-400" />}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPasswords(prev => ({ ...prev, current: !prev.current }))}
|
||||
className="absolute right-3 top-2 p-1 hover:bg-neutral-100 rounded"
|
||||
>
|
||||
{showPasswords.current ?
|
||||
<EyeOff className="w-4 h-4 text-neutral-500" /> :
|
||||
<Eye className="w-4 h-4 text-neutral-500" />
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* New Password */}
|
||||
<div>
|
||||
<label htmlFor="newPassword" className="block text-sm font-medium text-neutral-700 mb-1">
|
||||
New Password
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="newPassword"
|
||||
type={showPasswords.new ? 'text' : 'password'}
|
||||
value={formData.newPassword}
|
||||
onChange={handleInputChange('newPassword')}
|
||||
error={errors.newPassword}
|
||||
placeholder="Enter new password"
|
||||
leftIcon={<Lock className="w-5 h-5 text-neutral-400" />}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPasswords(prev => ({ ...prev, new: !prev.new }))}
|
||||
className="absolute right-3 top-2 p-1 hover:bg-neutral-100 rounded"
|
||||
>
|
||||
{showPasswords.new ?
|
||||
<EyeOff className="w-4 h-4 text-neutral-500" /> :
|
||||
<Eye className="w-4 h-4 text-neutral-500" />
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Confirm Password */}
|
||||
<div>
|
||||
<label htmlFor="confirmPassword" className="block text-sm font-medium text-neutral-700 mb-1">
|
||||
Confirm New Password
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="confirmPassword"
|
||||
type={showPasswords.confirm ? 'text' : 'password'}
|
||||
value={formData.confirmPassword}
|
||||
onChange={handleInputChange('confirmPassword')}
|
||||
error={errors.confirmPassword}
|
||||
placeholder="Confirm new password"
|
||||
leftIcon={<Lock className="w-5 h-5 text-neutral-400" />}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPasswords(prev => ({ ...prev, confirm: !prev.confirm }))}
|
||||
className="absolute right-3 top-2 p-1 hover:bg-neutral-100 rounded"
|
||||
>
|
||||
{showPasswords.confirm ?
|
||||
<EyeOff className="w-4 h-4 text-neutral-500" /> :
|
||||
<Eye className="w-4 h-4 text-neutral-500" />
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Password Requirements */}
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
|
||||
<div className="flex items-start gap-2">
|
||||
<AlertCircle className="w-5 h-5 text-blue-600 flex-shrink-0 mt-0.5" />
|
||||
<div className="text-sm text-blue-800">
|
||||
<p className="font-medium">Password Requirements:</p>
|
||||
<ul className="list-disc list-inside mt-1 space-y-1">
|
||||
<li>At least 6 characters long</li>
|
||||
<li>Must be different from current password</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex justify-end gap-3 pt-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={onClose}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
isLoading={changePasswordMutation.isPending}
|
||||
>
|
||||
Change Password
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
export { AdminLayout } from './AdminLayout';
|
||||
export { AdminSidebar } from './AdminSidebar';
|
||||
export { AdminHeader } from './AdminHeader';
|
||||
export { ThemeCustomizer } from './ThemeCustomizer';
|
||||
export { ThemeCustomizer } from './ThemeCustomizer';
|
||||
export { PasswordChangeModal } from './PasswordChangeModal';
|
||||
@@ -7,7 +7,7 @@ export const GALLERY_TOKEN_KEY = 'gallery_token';
|
||||
|
||||
// Create axios instance
|
||||
export const api = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3001',
|
||||
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3002',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
|
||||
@@ -32,7 +32,6 @@ const EVENT_TYPES = [
|
||||
{ value: 'wedding', label: 'Wedding', emoji: '💒' },
|
||||
{ value: 'birthday', label: 'Birthday', emoji: '🎂' },
|
||||
{ value: 'corporate', label: 'Corporate', emoji: '🏢' },
|
||||
{ value: 'party', label: 'Party', emoji: '🎉' },
|
||||
{ value: 'other', label: 'Other', emoji: '📸' },
|
||||
];
|
||||
|
||||
@@ -126,8 +125,6 @@ export const CreateEventPage: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const expiresAt = addDays(new Date(), formData.expires_in_days);
|
||||
|
||||
createMutation.mutate({
|
||||
event_type: formData.event_type,
|
||||
event_name: formData.event_name,
|
||||
@@ -135,9 +132,9 @@ export const CreateEventPage: React.FC = () => {
|
||||
host_email: formData.host_email,
|
||||
admin_email: formData.admin_email,
|
||||
password: formData.password,
|
||||
welcome_message: formData.welcome_message || undefined,
|
||||
welcome_message: formData.welcome_message || '',
|
||||
color_theme: formData.color_theme || undefined,
|
||||
expires_at: expiresAt.toISOString(),
|
||||
expiration_days: formData.expires_in_days,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -78,8 +78,7 @@ export const EventDetailsPage: React.FC = () => {
|
||||
// Extend expiration mutation
|
||||
const extendMutation = useMutation({
|
||||
mutationFn: (days: number) => {
|
||||
const newDate = addDays(parseISO(event!.expires_at), days);
|
||||
return eventsService.extendExpiration(parseInt(id!), newDate.toISOString());
|
||||
return eventsService.extendExpiration(parseInt(id!), days);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['admin-event', id] });
|
||||
|
||||
@@ -84,18 +84,12 @@ export const SettingsPage: React.FC = () => {
|
||||
// Save mutations
|
||||
const saveGeneralMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
// Save each setting
|
||||
const promises = Object.entries(generalSettings).map(([key, value]) =>
|
||||
fetch('/api/admin/settings/general', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${localStorage.getItem('adminToken')}`
|
||||
},
|
||||
body: JSON.stringify({ [`general_${key}`]: value })
|
||||
})
|
||||
);
|
||||
await Promise.all(promises);
|
||||
// Convert to the format expected by the API
|
||||
const settingsData: Record<string, any> = {};
|
||||
Object.entries(generalSettings).forEach(([key, value]) => {
|
||||
settingsData[`general_${key}`] = value;
|
||||
});
|
||||
return settingsService.updateSettings(settingsData);
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success('General settings saved successfully');
|
||||
@@ -108,18 +102,12 @@ export const SettingsPage: React.FC = () => {
|
||||
|
||||
const saveSecurityMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
// Save each setting
|
||||
const promises = Object.entries(securitySettings).map(([key, value]) =>
|
||||
fetch('/api/admin/settings/security', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${localStorage.getItem('adminToken')}`
|
||||
},
|
||||
body: JSON.stringify({ [`security_${key}`]: value })
|
||||
})
|
||||
);
|
||||
await Promise.all(promises);
|
||||
// Convert to the format expected by the API
|
||||
const settingsData: Record<string, any> = {};
|
||||
Object.entries(securitySettings).forEach(([key, value]) => {
|
||||
settingsData[`security_${key}`] = value;
|
||||
});
|
||||
return settingsService.updateSettings(settingsData);
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success('Security settings saved successfully');
|
||||
|
||||
@@ -91,5 +91,10 @@ export const adminService = {
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
},
|
||||
|
||||
// Change password
|
||||
async changePassword(data: { currentPassword: string; newPassword: string }): Promise<void> {
|
||||
await api.post('/api/admin/auth/change-password', data);
|
||||
}
|
||||
};
|
||||
@@ -10,10 +10,15 @@ interface CreateEventData {
|
||||
password: string;
|
||||
welcome_message?: string;
|
||||
color_theme?: string;
|
||||
expires_at: string;
|
||||
expiration_days: number;
|
||||
}
|
||||
|
||||
interface UpdateEventData {
|
||||
event_name?: string;
|
||||
event_date?: string;
|
||||
host_email?: string;
|
||||
admin_email?: string;
|
||||
password?: string;
|
||||
welcome_message?: string;
|
||||
color_theme?: string;
|
||||
expires_at?: string;
|
||||
@@ -61,13 +66,13 @@ export const eventsService = {
|
||||
|
||||
// Update event (admin)
|
||||
async updateEvent(id: number, data: UpdateEventData): Promise<Event> {
|
||||
const response = await api.patch<Event>(`/api/admin/events/${id}`, data);
|
||||
const response = await api.put<Event>(`/api/events/${id}`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Delete/deactivate event (admin)
|
||||
async deleteEvent(id: number): Promise<void> {
|
||||
await api.delete(`/api/admin/events/${id}`);
|
||||
await api.delete(`/api/events/${id}`);
|
||||
},
|
||||
|
||||
// Force archive event (admin)
|
||||
@@ -76,9 +81,9 @@ export const eventsService = {
|
||||
},
|
||||
|
||||
// Extend event expiration (admin)
|
||||
async extendExpiration(id: number, newExpiryDate: string): Promise<Event> {
|
||||
const response = await api.patch<Event>(`/api/admin/events/${id}`, {
|
||||
expires_at: newExpiryDate,
|
||||
async extendExpiration(id: number, days: number): Promise<Event> {
|
||||
const response = await api.post<Event>(`/api/events/${id}/extend`, {
|
||||
days,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user