diff --git a/backend/data/photo_sharing.db b/backend/data/photo_sharing.db new file mode 100644 index 0000000..c765d91 Binary files /dev/null and b/backend/data/photo_sharing.db differ diff --git a/backend/data/photos.db b/backend/data/photos.db new file mode 100644 index 0000000..e69de29 diff --git a/backend/server.js b/backend/server.js index fc7dba4..b071d3e 100644 --- a/backend/server.js +++ b/backend/server.js @@ -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) => { diff --git a/backend/src/database/db.js b/backend/src/database/db.js index 15e8544..f55e870 100644 --- a/backend/src/database/db.js +++ b/backend/src/database/db.js @@ -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 diff --git a/backend/src/routes/adminAuth.js b/backend/src/routes/adminAuth.js new file mode 100644 index 0000000..a48dbeb --- /dev/null +++ b/backend/src/routes/adminAuth.js @@ -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; \ No newline at end of file diff --git a/backend/src/routes/adminEmail.js b/backend/src/routes/adminEmail.js index 897c231..111a8bc 100644 --- a/backend/src/routes/adminEmail.js +++ b/backend/src/routes/adminEmail.js @@ -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) { diff --git a/backend/src/routes/adminEvents.js b/backend/src/routes/adminEvents.js index 896409a..e4b26ac 100644 --- a/backend/src/routes/adminEvents.js +++ b/backend/src/routes/adminEvents.js @@ -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) { diff --git a/frontend/src/components/admin/AdminHeader.tsx b/frontend/src/components/admin/AdminHeader.tsx index 2b5203e..33aeab3 100644 --- a/frontend/src/components/admin/AdminHeader.tsx +++ b/frontend/src/components/admin/AdminHeader.tsx @@ -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 = ({ onMenuClick }) => { const { user, logout } = useAdminAuth(); const [showUserMenu, setShowUserMenu] = useState(false); const [showNotifications, setShowNotifications] = useState(false); + const [showPasswordModal, setShowPasswordModal] = useState(false); const userMenuRef = useRef(null); const notificationRef = useRef(null); @@ -136,6 +138,16 @@ export const AdminHeader: React.FC = ({ onMenuClick }) => { Settings + + + +
+ {/* Current Password */} +
+ +
+ } + /> + +
+
+ + {/* New Password */} +
+ +
+ } + /> + +
+
+ + {/* Confirm Password */} +
+ +
+ } + /> + +
+
+ + {/* Password Requirements */} +
+
+ +
+

Password Requirements:

+
    +
  • At least 6 characters long
  • +
  • Must be different from current password
  • +
+
+
+
+ + {/* Action Buttons */} +
+ + +
+
+ + + + ); +}; \ No newline at end of file diff --git a/frontend/src/components/admin/index.ts b/frontend/src/components/admin/index.ts index 51833ae..c469868 100644 --- a/frontend/src/components/admin/index.ts +++ b/frontend/src/components/admin/index.ts @@ -1,4 +1,5 @@ export { AdminLayout } from './AdminLayout'; export { AdminSidebar } from './AdminSidebar'; export { AdminHeader } from './AdminHeader'; -export { ThemeCustomizer } from './ThemeCustomizer'; \ No newline at end of file +export { ThemeCustomizer } from './ThemeCustomizer'; +export { PasswordChangeModal } from './PasswordChangeModal'; \ No newline at end of file diff --git a/frontend/src/config/api.ts b/frontend/src/config/api.ts index 4acd2b1..388f5ca 100644 --- a/frontend/src/config/api.ts +++ b/frontend/src/config/api.ts @@ -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', }, diff --git a/frontend/src/pages/admin/CreateEventPage.tsx b/frontend/src/pages/admin/CreateEventPage.tsx index 85a54a0..aad89de 100644 --- a/frontend/src/pages/admin/CreateEventPage.tsx +++ b/frontend/src/pages/admin/CreateEventPage.tsx @@ -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, }); }; diff --git a/frontend/src/pages/admin/EventDetailsPage.tsx b/frontend/src/pages/admin/EventDetailsPage.tsx index 3d9810a..9121bcd 100644 --- a/frontend/src/pages/admin/EventDetailsPage.tsx +++ b/frontend/src/pages/admin/EventDetailsPage.tsx @@ -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] }); diff --git a/frontend/src/pages/admin/SettingsPage.tsx b/frontend/src/pages/admin/SettingsPage.tsx index 2f4c5d7..167d4a6 100644 --- a/frontend/src/pages/admin/SettingsPage.tsx +++ b/frontend/src/pages/admin/SettingsPage.tsx @@ -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 = {}; + 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 = {}; + Object.entries(securitySettings).forEach(([key, value]) => { + settingsData[`security_${key}`] = value; + }); + return settingsService.updateSettings(settingsData); }, onSuccess: () => { toast.success('Security settings saved successfully'); diff --git a/frontend/src/services/admin.service.ts b/frontend/src/services/admin.service.ts index fb368a0..b8b2935 100644 --- a/frontend/src/services/admin.service.ts +++ b/frontend/src/services/admin.service.ts @@ -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 { + await api.post('/api/admin/auth/change-password', data); } }; \ No newline at end of file diff --git a/frontend/src/services/events.service.ts b/frontend/src/services/events.service.ts index 1059970..833aa7f 100644 --- a/frontend/src/services/events.service.ts +++ b/frontend/src/services/events.service.ts @@ -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 { - const response = await api.patch(`/api/admin/events/${id}`, data); + const response = await api.put(`/api/events/${id}`, data); return response.data; }, // Delete/deactivate event (admin) async deleteEvent(id: number): Promise { - 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 { - const response = await api.patch(`/api/admin/events/${id}`, { - expires_at: newExpiryDate, + async extendExpiration(id: number, days: number): Promise { + const response = await api.post(`/api/events/${id}/extend`, { + days, }); return response.data; },