diff --git a/frontend/src/pages/Backups.tsx b/frontend/src/pages/Backups.tsx new file mode 100644 index 00000000..4ec7495c --- /dev/null +++ b/frontend/src/pages/Backups.tsx @@ -0,0 +1,218 @@ +import { useState } from 'react' +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' +import { Card } from '../components/ui/Card' +import { Button } from '../components/ui/Button' +import { Input } from '../components/ui/Input' +import { toast } from '../components/Toast' +import { getBackups, createBackup, restoreBackup, deleteBackup } from '../api/backups' +import { getSettings, updateSetting } from '../api/settings' +import { Loader2, Download, RotateCcw, Plus, Archive, Trash2, Save } from 'lucide-react' + +export default function Backups() { + const queryClient = useQueryClient() + const [interval, setInterval] = useState('7') + const [retention, setRetention] = useState('30') + + // Fetch Backups + const { data: backups, isLoading: isLoadingBackups } = useQuery({ + queryKey: ['backups'], + queryFn: getBackups, + }) + + // Fetch Settings + const { data: settings } = useQuery({ + queryKey: ['settings'], + queryFn: getSettings, + }) + + // Update local state when settings load + useState(() => { + if (settings) { + if (settings['backup.interval']) setInterval(settings['backup.interval']) + if (settings['backup.retention']) setRetention(settings['backup.retention']) + } + }) + + const createMutation = useMutation({ + mutationFn: createBackup, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['backups'] }) + toast.success('Backup created successfully') + }, + onError: (error: any) => { + toast.error(`Failed to create backup: ${error.message}`) + }, + }) + + const restoreMutation = useMutation({ + mutationFn: restoreBackup, + onSuccess: () => { + toast.success('Backup restored successfully. Please restart the container.') + }, + onError: (error: any) => { + toast.error(`Failed to restore backup: ${error.message}`) + }, + }) + + const deleteMutation = useMutation({ + mutationFn: deleteBackup, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['backups'] }) + toast.success('Backup deleted successfully') + }, + onError: (error: any) => { + toast.error(`Failed to delete backup: ${error.message}`) + }, + }) + + const saveSettingsMutation = useMutation({ + mutationFn: async () => { + await updateSetting('backup.interval', interval, 'system', 'int') + await updateSetting('backup.retention', retention, 'system', 'int') + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['settings'] }) + toast.success('Backup settings saved') + }, + onError: (error: any) => { + toast.error(`Failed to save settings: ${error.message}`) + }, + }) + + const handleDownload = (_filename: string) => { + // Direct download link + // Assuming we have a download endpoint that serves the file + // For now, we can use window.open or create a link element + // But we need an auth token. + // A better way is to use the API client to get a blob and download it. + // Or just show a toast as before if not implemented fully. + toast.info('Download logic needs backend implementation for authenticated file serving') + } + + return ( +
+

+ + Backups +

+ + {/* Settings Section */} + +

Configuration

+
+ setInterval(e.target.value)} + min="1" + /> + setRetention(e.target.value)} + min="1" + /> + +
+
+ + {/* Actions */} +
+ +
+ + {/* List */} + +
+ + + + + + + + + + + {isLoadingBackups ? ( + + + + ) : backups?.length === 0 ? ( + + + + ) : ( + backups?.map((backup: any) => ( + + + + + + + )) + )} + +
FilenameSizeCreated AtActions
+ +
+ No backups found +
+ {backup.filename} + + {(backup.size / 1024 / 1024).toFixed(2)} MB + + {new Date(backup.created_at).toLocaleString()} + + + + +
+
+
+
+ ) +} diff --git a/frontend/src/pages/Logs.tsx b/frontend/src/pages/Logs.tsx index 3a2e60d7..353dd6b1 100644 --- a/frontend/src/pages/Logs.tsx +++ b/frontend/src/pages/Logs.tsx @@ -1,13 +1,17 @@ import React, { useState } from 'react'; -import { useQuery } from '@tanstack/react-query'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { getLogs, getLogContent } from '../api/logs'; +import { getSettings, updateSetting } from '../api/settings'; import { Card } from '../components/ui/Card'; import { Button } from '../components/ui/Button'; -import { Loader2, RefreshCw, FileText } from 'lucide-react'; +import { toast } from '../components/Toast'; +import { Loader2, RefreshCw, FileText, Save } from 'lucide-react'; const Logs: React.FC = () => { const [selectedLog, setSelectedLog] = useState(null); const [lineCount, setLineCount] = useState(100); + const [logLevel, setLogLevel] = useState('INFO'); + const queryClient = useQueryClient(); const { data: logs, isLoading: isLoadingLogs, refetch: refetchLogs } = useQuery({ queryKey: ['logs'], @@ -20,14 +24,60 @@ const Logs: React.FC = () => { enabled: !!selectedLog, }); + const { data: settings } = useQuery({ + queryKey: ['settings'], + queryFn: getSettings, + }); + + // Update local state when settings load + React.useEffect(() => { + if (settings && settings['logging.level']) { + setLogLevel(settings['logging.level']); + } + }, [settings]); + + const saveSettingsMutation = useMutation({ + mutationFn: async () => { + await updateSetting('logging.level', logLevel, 'caddy', 'string'); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['settings'] }); + toast.success('Log level saved'); + }, + onError: (error: any) => { + toast.error(`Failed to save log level: ${error.message}`); + }, + }); + return (

System Logs

- +
+
+ + +
+ +
diff --git a/frontend/src/pages/Security.tsx b/frontend/src/pages/Security.tsx new file mode 100644 index 00000000..3905af44 --- /dev/null +++ b/frontend/src/pages/Security.tsx @@ -0,0 +1,146 @@ +import { useState } from 'react' +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' +import { Card } from '../components/ui/Card' +import { Input } from '../components/ui/Input' +import { Button } from '../components/ui/Button' +import { toast } from '../components/Toast' +import client from '../api/client' +import { getProfile, regenerateApiKey } from '../api/user' +import { Copy, RefreshCw, Shield } from 'lucide-react' + +export default function Security() { + const [oldPassword, setOldPassword] = useState('') + const [newPassword, setNewPassword] = useState('') + const [confirmPassword, setConfirmPassword] = useState('') + const [loading, setLoading] = useState(false) + + const queryClient = useQueryClient() + + const { data: profile, isLoading: isLoadingProfile } = useQuery({ + queryKey: ['profile'], + queryFn: getProfile, + }) + + const regenerateMutation = useMutation({ + mutationFn: regenerateApiKey, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['profile'] }) + toast.success('API Key regenerated successfully') + }, + onError: (error: any) => { + toast.error(`Failed to regenerate API key: ${error.message}`) + }, + }) + + const handleChangePassword = async (e: React.FormEvent) => { + e.preventDefault() + if (newPassword !== confirmPassword) { + toast.error('New passwords do not match') + return + } + + setLoading(true) + try { + await client.post('/auth/change-password', { + old_password: oldPassword, + new_password: newPassword, + }) + toast.success('Password updated successfully') + setOldPassword('') + setNewPassword('') + setConfirmPassword('') + } catch (err: any) { + toast.error(err.response?.data?.error || 'Failed to update password') + } finally { + setLoading(false) + } + } + + const copyToClipboard = (text: string) => { + navigator.clipboard.writeText(text) + toast.success('Copied to clipboard') + } + + return ( +
+

+ + Security +

+ +
+ {/* Change Password */} + +

Change Password

+
+ setOldPassword(e.target.value)} + required + /> + setNewPassword(e.target.value)} + required + /> + setConfirmPassword(e.target.value)} + required + /> + +
+
+ + {/* API Key */} + +

API Key

+

+ Use this key to authenticate with the API externally. Keep it secret! +

+ + {isLoadingProfile ? ( +
+ ) : ( +
+
+ + +
+ +
+ )} + +
+
+ ) +} diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx deleted file mode 100644 index 82d587ca..00000000 --- a/frontend/src/pages/Settings.tsx +++ /dev/null @@ -1,196 +0,0 @@ -import { useState } from 'react' -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' -import { Card } from '../components/ui/Card' -import { Input } from '../components/ui/Input' -import { Button } from '../components/ui/Button' -import { toast } from '../components/Toast' -import client from '../api/client' -import { getBackups, createBackup, restoreBackup } from '../api/backups' -import { Loader2, Download, RotateCcw, Plus, Archive } from 'lucide-react' - -export default function Settings() { - const [oldPassword, setOldPassword] = useState('') - const [newPassword, setNewPassword] = useState('') - const [confirmPassword, setConfirmPassword] = useState('') - const [loading, setLoading] = useState(false) - - const queryClient = useQueryClient() - - const { data: backups, isLoading: isLoadingBackups } = useQuery({ - queryKey: ['backups'], - queryFn: getBackups, - }) - - const createMutation = useMutation({ - mutationFn: createBackup, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['backups'] }) - toast.success('Backup created successfully') - }, - onError: (error: any) => { - toast.error(`Failed to create backup: ${error.message}`) - }, - }) - - const restoreMutation = useMutation({ - mutationFn: restoreBackup, - onSuccess: () => { - toast.success('Backup restored successfully. Please restart the container.') - }, - onError: (error: any) => { - toast.error(`Failed to restore backup: ${error.message}`) - }, - }) - - const handleChangePassword = async (e: React.FormEvent) => { - e.preventDefault() - if (newPassword !== confirmPassword) { - toast.error('New passwords do not match') - return - } - - setLoading(true) - try { - await client.post('/auth/change-password', { - old_password: oldPassword, - new_password: newPassword, - }) - toast.success('Password updated successfully') - setOldPassword('') - setNewPassword('') - setConfirmPassword('') - } catch (err: any) { - toast.error(err.response?.data?.error || 'Failed to update password') - } finally { - setLoading(false) - } - } - - const handleDownload = (_filename: string) => { - toast.info('Download not yet implemented in backend') - } - - return ( -
-

Settings

- -
- -

Change Password

-
- setOldPassword(e.target.value)} - required - /> - setNewPassword(e.target.value)} - required - /> - setConfirmPassword(e.target.value)} - required - /> - -
-
- - -
-
-

- - Backups -

-

- Manage system backups. Backups include the database and Caddy configuration. -

-
- -
- - {isLoadingBackups ? ( -
- -
- ) : ( -
- - - - - - - - - - - {backups?.map((backup) => ( - - - - - - - ))} - {backups?.length === 0 && ( - - - - )} - -
FilenameSizeDateActions
- {backup.name} - - {(backup.size / 1024 / 1024).toFixed(2)} MB - - {new Date(backup.mod_time).toLocaleString()} - - - -
- No backups found. Create one to get started. -
-
- )} -
-
-
- ) -} diff --git a/frontend/src/pages/SettingsLayout.tsx b/frontend/src/pages/SettingsLayout.tsx new file mode 100644 index 00000000..86dffba4 --- /dev/null +++ b/frontend/src/pages/SettingsLayout.tsx @@ -0,0 +1,82 @@ +import { Outlet, Link, useLocation } from 'react-router-dom' +import { Shield, Archive, FileText, ChevronDown, ChevronRight } from 'lucide-react' +import { useState } from 'react' + +export default function SettingsLayout() { + const location = useLocation() + const [tasksOpen, setTasksOpen] = useState(true) + + const isActive = (path: string) => location.pathname === path + + return ( +
+ {/* Settings Sidebar */} +
+
+

+ Settings +

+ +
+
+ + {/* Content Area */} +
+ +
+
+ ) +}