- migrate from SQLite to PostgreSQL with Drizzle ORM - implement comprehensive AdminLayout with expandable sidebar navigation - create professional dashboard with real-time charts and metrics - add advanced monitoring, reporting, and export functionality - fix menu alignment and remove non-existent pages - eliminate duplicate headers and improve UI consistency - add Tailwind CSS v3 for professional styling - expand database schema from 6 to 15 tables - implement role-based access control and API key management - create comprehensive settings, monitoring, and system info pages
421 lines
15 KiB
TypeScript
421 lines
15 KiB
TypeScript
import { useState, useEffect } from 'react'
|
|
import type { NextPage } from 'next'
|
|
import Head from 'next/head'
|
|
import AdminLayout from '../../../components/AdminLayout'
|
|
import {
|
|
Cog6ToothIcon,
|
|
GlobeAltIcon,
|
|
ShieldCheckIcon,
|
|
ClockIcon,
|
|
DocumentTextIcon,
|
|
EnvelopeIcon,
|
|
BellIcon
|
|
} from '@heroicons/react/24/outline'
|
|
|
|
interface SystemSetting {
|
|
key: string
|
|
value: string
|
|
type: 'string' | 'number' | 'boolean' | 'json'
|
|
description: string
|
|
category: string
|
|
}
|
|
|
|
const GeneralSettings: NextPage = () => {
|
|
const [settings, setSettings] = useState<SystemSetting[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
const [saving, setSaving] = useState(false)
|
|
|
|
useEffect(() => {
|
|
fetchSettings()
|
|
}, [])
|
|
|
|
const fetchSettings = async () => {
|
|
try {
|
|
const response = await fetch('/api/admin/settings')
|
|
if (response.ok) {
|
|
const data = await response.json()
|
|
setSettings(data)
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to fetch settings:', error)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const updateSetting = async (key: string, value: string) => {
|
|
setSaving(true)
|
|
try {
|
|
const response = await fetch('/api/admin/settings', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ key, value })
|
|
})
|
|
|
|
if (response.ok) {
|
|
setSettings(prev => prev.map(s => s.key === key ? { ...s, value } : s))
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to update setting:', error)
|
|
} finally {
|
|
setSaving(false)
|
|
}
|
|
}
|
|
|
|
const getSetting = (key: string) => settings.find(s => s.key === key)
|
|
|
|
const SettingField = ({ setting }: { setting: SystemSetting }) => {
|
|
const [value, setValue] = useState(setting.value)
|
|
|
|
const handleSave = () => {
|
|
if (value !== setting.value) {
|
|
updateSetting(setting.key, value)
|
|
}
|
|
}
|
|
|
|
if (setting.type === 'boolean') {
|
|
return (
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex-1">
|
|
<label className="text-sm font-medium text-gray-700">{setting.description}</label>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 ${
|
|
value === 'true' ? 'bg-primary-600' : 'bg-gray-200'
|
|
}`}
|
|
onClick={() => {
|
|
const newValue = value === 'true' ? 'false' : 'true'
|
|
setValue(newValue)
|
|
updateSetting(setting.key, newValue)
|
|
}}
|
|
>
|
|
<span
|
|
className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
|
|
value === 'true' ? 'translate-x-5' : 'translate-x-0'
|
|
}`}
|
|
/>
|
|
</button>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
{setting.description}
|
|
</label>
|
|
<div className="flex space-x-2">
|
|
<input
|
|
type={setting.type === 'number' ? 'number' : 'text'}
|
|
value={value}
|
|
onChange={(e) => setValue(e.target.value)}
|
|
className="input flex-1"
|
|
/>
|
|
{value !== setting.value && (
|
|
<button
|
|
onClick={handleSave}
|
|
disabled={saving}
|
|
className="btn-primary px-3 py-2"
|
|
>
|
|
Uložiť
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (loading) {
|
|
return (
|
|
<AdminLayout title="Všeobecné nastavenia">
|
|
<Head>
|
|
<title>Všeobecné nastavenia - Hliadka.sk Admin</title>
|
|
</Head>
|
|
<div className="flex items-center justify-center h-64">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600"></div>
|
|
</div>
|
|
</AdminLayout>
|
|
)
|
|
}
|
|
|
|
const generalSettings = settings.filter(s => s.category === 'general')
|
|
const apiSettings = settings.filter(s => s.category === 'api')
|
|
const emailSettings = settings.filter(s => s.category === 'email')
|
|
const moderationSettings = settings.filter(s => s.category === 'moderation')
|
|
|
|
return (
|
|
<AdminLayout title="Všeobecné nastavenia">
|
|
<Head>
|
|
<title>Všeobecné nastavenia - Hliadka.sk Admin</title>
|
|
</Head>
|
|
|
|
<div className="space-y-8">
|
|
{/* System Information */}
|
|
<div className="card p-6">
|
|
<div className="flex items-center mb-6">
|
|
<GlobeAltIcon className="h-6 w-6 text-primary-600 mr-3" />
|
|
<h2 className="text-xl font-semibold text-gray-900">Systémové informácie</h2>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
<div className="stat-card">
|
|
<div className="text-sm font-medium text-gray-500">Verzia aplikácie</div>
|
|
<div className="text-lg font-semibold text-gray-900">v1.0.0</div>
|
|
</div>
|
|
<div className="stat-card">
|
|
<div className="text-sm font-medium text-gray-500">Verzia databázy</div>
|
|
<div className="text-lg font-semibold text-gray-900">PostgreSQL 14.2</div>
|
|
</div>
|
|
<div className="stat-card">
|
|
<div className="text-sm font-medium text-gray-500">Posledná aktualizácia</div>
|
|
<div className="text-lg font-semibold text-gray-900">6.9.2025</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* General Settings */}
|
|
<div className="card p-6">
|
|
<div className="flex items-center mb-6">
|
|
<Cog6ToothIcon className="h-6 w-6 text-primary-600 mr-3" />
|
|
<h2 className="text-xl font-semibold text-gray-900">Všeobecné nastavenia</h2>
|
|
</div>
|
|
|
|
<div className="space-y-6">
|
|
<SettingField setting={{
|
|
key: 'site_name',
|
|
value: getSetting('site_name')?.value || 'Hliadka.sk',
|
|
type: 'string',
|
|
description: 'Názov aplikácie',
|
|
category: 'general'
|
|
}} />
|
|
|
|
<SettingField setting={{
|
|
key: 'site_description',
|
|
value: getSetting('site_description')?.value || 'Platforma na monitorovanie problematických webových zdrojov',
|
|
type: 'string',
|
|
description: 'Popis aplikácie',
|
|
category: 'general'
|
|
}} />
|
|
|
|
<SettingField setting={{
|
|
key: 'admin_email',
|
|
value: getSetting('admin_email')?.value || 'admin@hliadka.sk',
|
|
type: 'string',
|
|
description: 'Administrátorský email',
|
|
category: 'general'
|
|
}} />
|
|
|
|
<SettingField setting={{
|
|
key: 'maintenance_mode',
|
|
value: getSetting('maintenance_mode')?.value || 'false',
|
|
type: 'boolean',
|
|
description: 'Režim údržby',
|
|
category: 'general'
|
|
}} />
|
|
|
|
<SettingField setting={{
|
|
key: 'registration_enabled',
|
|
value: getSetting('registration_enabled')?.value || 'false',
|
|
type: 'boolean',
|
|
description: 'Povoliť registráciu nových moderátorov',
|
|
category: 'general'
|
|
}} />
|
|
</div>
|
|
</div>
|
|
|
|
{/* API Settings */}
|
|
<div className="card p-6">
|
|
<div className="flex items-center mb-6">
|
|
<ShieldCheckIcon className="h-6 w-6 text-primary-600 mr-3" />
|
|
<h2 className="text-xl font-semibold text-gray-900">API nastavenia</h2>
|
|
</div>
|
|
|
|
<div className="space-y-6">
|
|
<SettingField setting={{
|
|
key: 'api_rate_limit_default',
|
|
value: getSetting('api_rate_limit_default')?.value || '1000',
|
|
type: 'number',
|
|
description: 'Predvolený rate limit pre API kľúče (požiadavky/hodinu)',
|
|
category: 'api'
|
|
}} />
|
|
|
|
<SettingField setting={{
|
|
key: 'api_cache_duration',
|
|
value: getSetting('api_cache_duration')?.value || '300',
|
|
type: 'number',
|
|
description: 'Doba cachovania API odpovedí (sekundy)',
|
|
category: 'api'
|
|
}} />
|
|
|
|
<SettingField setting={{
|
|
key: 'api_require_auth',
|
|
value: getSetting('api_require_auth')?.value || 'true',
|
|
type: 'boolean',
|
|
description: 'Vyžadovať autentifikáciu pre všetky API endpointy',
|
|
category: 'api'
|
|
}} />
|
|
|
|
<SettingField setting={{
|
|
key: 'api_cors_enabled',
|
|
value: getSetting('api_cors_enabled')?.value || 'true',
|
|
type: 'boolean',
|
|
description: 'Povoliť CORS pre API',
|
|
category: 'api'
|
|
}} />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Email Settings */}
|
|
<div className="card p-6">
|
|
<div className="flex items-center mb-6">
|
|
<EnvelopeIcon className="h-6 w-6 text-primary-600 mr-3" />
|
|
<h2 className="text-xl font-semibold text-gray-900">Email nastavenia</h2>
|
|
</div>
|
|
|
|
<div className="space-y-6">
|
|
<SettingField setting={{
|
|
key: 'smtp_host',
|
|
value: getSetting('smtp_host')?.value || 'localhost',
|
|
type: 'string',
|
|
description: 'SMTP server',
|
|
category: 'email'
|
|
}} />
|
|
|
|
<SettingField setting={{
|
|
key: 'smtp_port',
|
|
value: getSetting('smtp_port')?.value || '587',
|
|
type: 'number',
|
|
description: 'SMTP port',
|
|
category: 'email'
|
|
}} />
|
|
|
|
<SettingField setting={{
|
|
key: 'smtp_from_email',
|
|
value: getSetting('smtp_from_email')?.value || 'noreply@hliadka.sk',
|
|
type: 'string',
|
|
description: 'Odosielateľský email',
|
|
category: 'email'
|
|
}} />
|
|
|
|
<SettingField setting={{
|
|
key: 'email_notifications_enabled',
|
|
value: getSetting('email_notifications_enabled')?.value || 'true',
|
|
type: 'boolean',
|
|
description: 'Povoliť email notifikácie',
|
|
category: 'email'
|
|
}} />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Moderation Settings */}
|
|
<div className="card p-6">
|
|
<div className="flex items-center mb-6">
|
|
<DocumentTextIcon className="h-6 w-6 text-primary-600 mr-3" />
|
|
<h2 className="text-xl font-semibold text-gray-900">Nastavenia moderovania</h2>
|
|
</div>
|
|
|
|
<div className="space-y-6">
|
|
<SettingField setting={{
|
|
key: 'auto_assign_reports',
|
|
value: getSetting('auto_assign_reports')?.value || 'true',
|
|
type: 'boolean',
|
|
description: 'Automaticky priradiť hlásenia moderátorom',
|
|
category: 'moderation'
|
|
}} />
|
|
|
|
<SettingField setting={{
|
|
key: 'require_evidence_for_reports',
|
|
value: getSetting('require_evidence_for_reports')?.value || 'false',
|
|
type: 'boolean',
|
|
description: 'Vyžadovať dôkazy pre hlásenia',
|
|
category: 'moderation'
|
|
}} />
|
|
|
|
<SettingField setting={{
|
|
key: 'max_reports_per_ip',
|
|
value: getSetting('max_reports_per_ip')?.value || '10',
|
|
type: 'number',
|
|
description: 'Maximálny počet hlásení z jednej IP denne',
|
|
category: 'moderation'
|
|
}} />
|
|
|
|
<SettingField setting={{
|
|
key: 'high_risk_threshold',
|
|
value: getSetting('high_risk_threshold')?.value || '4',
|
|
type: 'number',
|
|
description: 'Prah pre označenie ako vysoké riziko (1-5)',
|
|
category: 'moderation'
|
|
}} />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Notification Settings */}
|
|
<div className="card p-6">
|
|
<div className="flex items-center mb-6">
|
|
<BellIcon className="h-6 w-6 text-primary-600 mr-3" />
|
|
<h2 className="text-xl font-semibold text-gray-900">Nastavenia notifikácií</h2>
|
|
</div>
|
|
|
|
<div className="space-y-6">
|
|
<SettingField setting={{
|
|
key: 'notify_new_reports',
|
|
value: getSetting('notify_new_reports')?.value || 'true',
|
|
type: 'boolean',
|
|
description: 'Notifikovať o nových hláseniach',
|
|
category: 'notifications'
|
|
}} />
|
|
|
|
<SettingField setting={{
|
|
key: 'notify_high_risk_sources',
|
|
value: getSetting('notify_high_risk_sources')?.value || 'true',
|
|
type: 'boolean',
|
|
description: 'Notifikovať o zdrojoch s vysokým rizikom',
|
|
category: 'notifications'
|
|
}} />
|
|
|
|
<SettingField setting={{
|
|
key: 'notification_frequency',
|
|
value: getSetting('notification_frequency')?.value || 'realtime',
|
|
type: 'string',
|
|
description: 'Frekvencia notifikácií (realtime, hourly, daily)',
|
|
category: 'notifications'
|
|
}} />
|
|
</div>
|
|
</div>
|
|
|
|
{/* System Maintenance */}
|
|
<div className="card p-6">
|
|
<div className="flex items-center mb-6">
|
|
<ClockIcon className="h-6 w-6 text-primary-600 mr-3" />
|
|
<h2 className="text-xl font-semibold text-gray-900">Údržba systému</h2>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<button className="btn-secondary p-4 text-left">
|
|
<div className="text-sm font-medium text-gray-900">Vyčistiť cache</div>
|
|
<div className="text-sm text-gray-500">Vymazať všetky cached dáta</div>
|
|
</button>
|
|
|
|
<button className="btn-secondary p-4 text-left">
|
|
<div className="text-sm font-medium text-gray-900">Optimalizovať databázu</div>
|
|
<div className="text-sm text-gray-500">Spustiť optimalizáciu databázy</div>
|
|
</button>
|
|
|
|
<button className="btn-secondary p-4 text-left">
|
|
<div className="text-sm font-medium text-gray-900">Vyčistiť logy</div>
|
|
<div className="text-sm text-gray-500">Vymazať staré log súbory</div>
|
|
</button>
|
|
|
|
<button className="btn-secondary p-4 text-left">
|
|
<div className="text-sm font-medium text-gray-900">Záloha databázy</div>
|
|
<div className="text-sm text-gray-500">Vytvoriť manuálnu zálohu</div>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</AdminLayout>
|
|
)
|
|
}
|
|
|
|
export default GeneralSettings |