diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 73d527c..3a4dc18 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -17,7 +17,8 @@ import { EmailConfigPage, ArchivesPage, AnalyticsPage, - BrandingPage + BrandingPage, + SettingsPage } from './pages/admin'; import { AdminLayout } from './components/admin'; import { PageErrorBoundary, ErrorBoundary, OfflineIndicator, SkipLink } from './components/common'; @@ -76,6 +77,7 @@ function App() { } /> } /> } /> + } /> } /> diff --git a/frontend/src/components/admin/AdminSidebar.tsx b/frontend/src/components/admin/AdminSidebar.tsx index 96d47b9..f1bf836 100644 --- a/frontend/src/components/admin/AdminSidebar.tsx +++ b/frontend/src/components/admin/AdminSidebar.tsx @@ -11,6 +11,8 @@ import { X, Palette } from 'lucide-react'; +import { useQuery } from '@tanstack/react-query'; +import { settingsService } from '../../services/settings.service'; interface AdminSidebarProps { isOpen: boolean; @@ -84,22 +86,51 @@ export const AdminSidebar: React.FC = ({ isOpen, onClose }) = {/* Storage Info */} -
-
-
- Storage Used - 2.4 GB -
-
-
-
-

24% of 10 GB

-
+
); +}; + +const StorageInfo: React.FC = () => { + const { data: storageInfo } = useQuery({ + queryKey: ['storage-info'], + queryFn: () => settingsService.getStorageInfo(), + refetchInterval: 60000 // Refresh every minute + }); + + if (!storageInfo) { + return ( +
+
+
+
+
+ ); + } + + const usagePercent = Math.round((storageInfo.total_used / storageInfo.storage_limit) * 100); + + return ( +
+
+
+ Storage Used + + {settingsService.formatBytes(storageInfo.total_used)} + +
+
+
+
+

+ {usagePercent}% of {settingsService.formatBytes(storageInfo.storage_limit)} +

+
+
+ ); }; \ No newline at end of file diff --git a/frontend/src/pages/admin/AnalyticsPage.tsx b/frontend/src/pages/admin/AnalyticsPage.tsx index 8a8bd61..a882590 100644 --- a/frontend/src/pages/admin/AnalyticsPage.tsx +++ b/frontend/src/pages/admin/AnalyticsPage.tsx @@ -5,14 +5,13 @@ import { Users, Eye, Download, - Globe, Smartphone, Monitor, Activity, RefreshCw, Tablet } from 'lucide-react'; -import { format, subDays, parseISO } from 'date-fns'; +import { format, parseISO } from 'date-fns'; import { Button, Card, Loading } from '../../components/common'; import { useQuery } from '@tanstack/react-query'; diff --git a/frontend/src/pages/admin/EmailConfigPage.tsx b/frontend/src/pages/admin/EmailConfigPage.tsx index 9993eaa..0aebd73 100644 --- a/frontend/src/pages/admin/EmailConfigPage.tsx +++ b/frontend/src/pages/admin/EmailConfigPage.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState } from 'react'; import { Mail, Save, @@ -10,13 +10,12 @@ import { CheckCircle, Eye, EyeOff, - RefreshCw } from 'lucide-react'; import { toast } from 'react-toastify'; import { Button, Input, Card, Loading } from '../../components/common'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import { emailService, EmailConfig, EmailTemplate } from '../../services/email.service'; +import { emailService, type EmailConfig, type EmailTemplate } from '../../services/email.service'; const defaultTemplateKeys = [ { @@ -102,12 +101,9 @@ export const EmailConfigPage: React.FC = () => { }); // Fetch SMTP config - const { data: fetchedConfig, isLoading: configLoading } = useQuery({ + const { isLoading: configLoading } = useQuery({ queryKey: ['email-config'], queryFn: () => emailService.getConfig(), - onSuccess: (data) => { - setSmtpConfig(data); - } }); // Fetch email templates @@ -121,11 +117,27 @@ export const EmailConfigPage: React.FC = () => { queryKey: ['email-template', selectedTemplateKey], queryFn: () => emailService.getTemplate(selectedTemplateKey), enabled: !!selectedTemplateKey && activeTab === 'templates', - onSuccess: (data) => { - setEditedTemplate(data); - } }); + // Update local state when data is fetched + React.useEffect(() => { + const fetchConfig = async () => { + try { + const config = await emailService.getConfig(); + setSmtpConfig(config); + } catch (error) { + // Config might not exist yet + } + }; + fetchConfig(); + }, []); + + React.useEffect(() => { + if (selectedTemplate) { + setEditedTemplate(selectedTemplate); + } + }, [selectedTemplate]); + // Mutations const saveConfigMutation = useMutation({ mutationFn: (config: EmailConfig) => emailService.updateConfig(config), diff --git a/frontend/src/pages/admin/SettingsPage.tsx b/frontend/src/pages/admin/SettingsPage.tsx new file mode 100644 index 0000000..e7d67ec --- /dev/null +++ b/frontend/src/pages/admin/SettingsPage.tsx @@ -0,0 +1,531 @@ +import React, { useState } from 'react'; +import { + Save, + Shield, + HardDrive, + Bell, + Database, + Globe, + Key, + AlertCircle, + CheckCircle +} 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'; + +export const SettingsPage: React.FC = () => { + const [activeTab, setActiveTab] = useState<'general' | 'storage' | 'security'>('general'); + const queryClient = useQueryClient(); + + // Fetch settings + const { data: settings, isLoading } = useQuery({ + queryKey: ['admin-settings'], + queryFn: () => settingsService.getAllSettings(), + }); + + // Fetch storage info + const { data: storageInfo } = useQuery({ + queryKey: ['admin-storage-info'], + queryFn: () => settingsService.getStorageInfo(), + enabled: activeTab === 'storage' + }); + + // General settings state + const [generalSettings, setGeneralSettings] = useState({ + site_url: '', + default_expiration_days: 30, + max_file_size_mb: 50, + allowed_file_types: 'jpg,jpeg,png,gif,webp', + enable_watermark: false, + enable_analytics: true, + enable_registration: false, + maintenance_mode: false + }); + + // Security settings state + const [securitySettings, setSecuritySettings] = useState({ + require_password: true, + password_min_length: 8, + enable_2fa: false, + session_timeout_minutes: 60, + max_login_attempts: 5, + enable_recaptcha: false, + recaptcha_site_key: '', + recaptcha_secret_key: '' + }); + + React.useEffect(() => { + if (settings) { + // Extract general settings + setGeneralSettings({ + site_url: settings.general_site_url || '', + default_expiration_days: settings.general_default_expiration_days || 30, + max_file_size_mb: settings.general_max_file_size_mb || 50, + allowed_file_types: settings.general_allowed_file_types || 'jpg,jpeg,png,gif,webp', + enable_watermark: settings.general_enable_watermark || false, + enable_analytics: settings.general_enable_analytics || true, + enable_registration: settings.general_enable_registration || false, + maintenance_mode: settings.general_maintenance_mode || false + }); + + // Extract security settings + setSecuritySettings({ + require_password: settings.security_require_password || true, + password_min_length: settings.security_password_min_length || 8, + enable_2fa: settings.security_enable_2fa || false, + session_timeout_minutes: settings.security_session_timeout_minutes || 60, + max_login_attempts: settings.security_max_login_attempts || 5, + enable_recaptcha: settings.security_enable_recaptcha || false, + recaptcha_site_key: settings.security_recaptcha_site_key || '', + recaptcha_secret_key: settings.security_recaptcha_secret_key || '' + }); + } + }, [settings]); + + // 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); + }, + onSuccess: () => { + toast.success('General settings saved successfully'); + queryClient.invalidateQueries({ queryKey: ['admin-settings'] }); + }, + onError: () => { + toast.error('Failed to save general settings'); + } + }); + + 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); + }, + onSuccess: () => { + toast.success('Security settings saved successfully'); + queryClient.invalidateQueries({ queryKey: ['admin-settings'] }); + }, + onError: () => { + toast.error('Failed to save security settings'); + } + }); + + if (isLoading) { + return ( +
+ +
+ ); + } + + return ( +
+
+

System Settings

+

Configure system-wide settings and preferences

+
+ + {/* Tab Navigation */} +
+ +
+ + {/* General Settings Tab */} + {activeTab === 'general' && ( +
+ +

Site Configuration

+ +
+
+ + setGeneralSettings(prev => ({ ...prev, site_url: e.target.value }))} + placeholder="https://yourdomain.com" + leftIcon={} + /> +

+ Used for generating gallery links in emails +

+
+ +
+
+ + setGeneralSettings(prev => ({ ...prev, default_expiration_days: parseInt(e.target.value) || 30 }))} + min="1" + max="365" + /> +
+
+ + setGeneralSettings(prev => ({ ...prev, max_file_size_mb: parseInt(e.target.value) || 50 }))} + min="1" + max="500" + /> +
+
+ +
+ + setGeneralSettings(prev => ({ ...prev, allowed_file_types: e.target.value }))} + placeholder="jpg,jpeg,png,gif" + /> +

+ Comma-separated list of file extensions +

+
+
+
+ + +

Feature Toggles

+ +
+ + + + + + + +
+ +
+ +
+
+
+ )} + + {/* Storage Tab */} + {activeTab === 'storage' && storageInfo && ( +
+ +

Storage Overview

+ +
+
+

Total Used

+

+ {settingsService.formatBytes(storageInfo.total_used)} +

+
+
+

Archive Storage

+

+ {settingsService.formatBytes(storageInfo.archive_storage)} +

+
+
+

Storage Limit

+

+ {settingsService.formatBytes(storageInfo.storage_limit)} +

+
+
+ +
+
+ Storage Usage + + {Math.round((storageInfo.total_used / storageInfo.storage_limit) * 100)}% + +
+
+
+
+
+ +
+

Storage by Event

+
+ {storageInfo.storage_by_event.slice(0, 10).map((event) => ( +
+ {event.event_name} + + {settingsService.formatBytes(event.size)} + +
+ ))} +
+
+ + + +
+ +
+

Storage Management

+

+ Consider archiving or deleting old events to free up storage space. + Archived events are compressed and use less storage than active galleries. +

+
+
+
+
+ )} + + {/* Security Tab */} + {activeTab === 'security' && ( +
+ +

Password Settings

+ +
+ + +
+ + setSecuritySettings(prev => ({ ...prev, password_min_length: parseInt(e.target.value) || 8 }))} + min="4" + max="32" + /> +
+
+
+ + +

Session & Authentication

+ +
+
+
+ + setSecuritySettings(prev => ({ ...prev, session_timeout_minutes: parseInt(e.target.value) || 60 }))} + min="5" + max="1440" + /> +
+
+ + setSecuritySettings(prev => ({ ...prev, max_login_attempts: parseInt(e.target.value) || 5 }))} + min="3" + max="10" + /> +
+
+ + +
+
+ + +

reCAPTCHA Settings

+ +
+ + + {securitySettings.enable_recaptcha && ( + <> +
+ + setSecuritySettings(prev => ({ ...prev, recaptcha_site_key: e.target.value }))} + placeholder="Your reCAPTCHA site key" + leftIcon={} + /> +
+
+ + setSecuritySettings(prev => ({ ...prev, recaptcha_secret_key: e.target.value }))} + placeholder="Your reCAPTCHA secret key" + leftIcon={} + /> +
+ + )} + +
+
+ +
+

Get your reCAPTCHA keys from Google reCAPTCHA Admin

+
+
+
+
+ +
+ +
+
+
+ )} +
+ ); +}; \ No newline at end of file diff --git a/frontend/src/pages/admin/index.ts b/frontend/src/pages/admin/index.ts index c336e0d..17b9a3f 100644 --- a/frontend/src/pages/admin/index.ts +++ b/frontend/src/pages/admin/index.ts @@ -6,4 +6,5 @@ export { EventDetailsPage } from './EventDetailsPage'; export { EmailConfigPage } from './EmailConfigPage'; export { ArchivesPage } from './ArchivesPage'; export { AnalyticsPage } from './AnalyticsPage'; -export { BrandingPage } from './BrandingPage'; \ No newline at end of file +export { BrandingPage } from './BrandingPage'; +export { SettingsPage } from './SettingsPage'; \ No newline at end of file diff --git a/frontend/src/services/settings.service.ts b/frontend/src/services/settings.service.ts index 67c1d0d..02f071f 100644 --- a/frontend/src/services/settings.service.ts +++ b/frontend/src/services/settings.service.ts @@ -93,5 +93,14 @@ export const settingsService = { // Format theme settings from raw data formatThemeSettings(rawSettings: Record): ThemeSettings { return rawSettings.theme_config || {}; + }, + + // Format bytes to human readable + formatBytes(bytes: number): string { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + 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]; } }; \ No newline at end of file