feat: add settings table and backup notification functionality

This commit is contained in:
2025-12-02 11:30:13 +01:00
parent e6fab5ba31
commit eb41814c24
6 changed files with 298 additions and 1 deletions

4
.gitignore vendored
View File

@@ -47,4 +47,6 @@ next-env.d.ts
/kosz /kosz
# uploads # uploads
/public/uploads /public/uploads
/backups

View File

@@ -19,6 +19,27 @@ fs.copyFileSync(dbPath, backupPath);
console.log(`✅ Backup created: ${backupPath}`); console.log(`✅ Backup created: ${backupPath}`);
// Send notification if configured
try {
const { createNotification, NOTIFICATION_TYPES } = await import("./src/lib/notifications.js");
const db = (await import("./src/lib/db.js")).default;
const setting = db.prepare("SELECT value FROM settings WHERE key = 'backup_notification_user_id'").get();
if (setting && setting.value) {
const userId = setting.value;
await createNotification({
userId,
type: NOTIFICATION_TYPES.SYSTEM_ANNOUNCEMENT,
title: "Database Backup Completed",
message: `Daily database backup completed successfully. Backup file: ${backupPath}`,
priority: "normal"
});
console.log(`📢 Notification sent to user ${userId}`);
}
} catch (error) {
console.error("Failed to send backup notification:", error);
}
// Cleanup: keep only last 30 backups // Cleanup: keep only last 30 backups
const files = fs.readdirSync(backupDir) const files = fs.readdirSync(backupDir)
.filter(f => f.startsWith('backup-')) .filter(f => f.startsWith('backup-'))

View File

@@ -0,0 +1,25 @@
import db from "./src/lib/db.js";
console.log("Adding settings table...");
try {
db.exec(`
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
description TEXT,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_by TEXT,
FOREIGN KEY (updated_by) REFERENCES users(id)
);
`);
db.exec(`
INSERT OR IGNORE INTO settings (key, value, description) VALUES
('backup_notification_user_id', '', 'User ID to receive backup completion notifications');
`);
console.log("✅ Settings table created successfully");
} catch (error) {
console.error("Error creating settings table:", error);
}

View File

@@ -0,0 +1,183 @@
"use client";
import { useState, useEffect } from "react";
import { useSession } from "next-auth/react";
import { useRouter } from "next/navigation";
import Link from "next/link";
export default function AdminSettingsPage() {
const { data: session, status } = useSession();
const router = useRouter();
const [settings, setSettings] = useState([]);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [users, setUsers] = useState([]);
// Redirect if not admin
useEffect(() => {
if (status === "loading") return;
if (!session || session.user.role !== "admin") {
router.push("/");
return;
}
fetchSettings();
fetchUsers();
}, [session, status, router]);
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("Error fetching settings:", error);
} finally {
setLoading(false);
}
};
const fetchUsers = async () => {
try {
const response = await fetch("/api/admin/users");
if (response.ok) {
const data = await response.json();
setUsers(data);
}
} catch (error) {
console.error("Error fetching users:", error);
}
};
const updateSetting = async (key, value) => {
setSaving(true);
try {
const response = await fetch("/api/admin/settings", {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ key, value }),
});
if (response.ok) {
// Update local state
setSettings(prev =>
prev.map(setting =>
setting.key === key ? { ...setting, value } : setting
)
);
alert("Setting updated successfully!");
} else {
alert("Failed to update setting");
}
} catch (error) {
console.error("Error updating setting:", error);
alert("Error updating setting");
} finally {
setSaving(false);
}
};
const handleBackupUserChange = (userId) => {
updateSetting("backup_notification_user_id", userId);
};
if (status === "loading" || loading) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-lg">Loading...</div>
</div>
);
}
if (!session || session.user.role !== "admin") {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<h1 className="text-2xl font-bold text-gray-800 mb-4">
Access Denied
</h1>
<p className="text-gray-600 mb-6">
You need admin privileges to access this page.
</p>
<Link
href="/"
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
>
Go Home
</Link>
</div>
</div>
);
}
const backupUserSetting = settings.find(s => s.key === "backup_notification_user_id");
return (
<div className="min-h-screen bg-gray-50 py-8">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="bg-white shadow rounded-lg">
<div className="px-6 py-4 border-b border-gray-200">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold text-gray-900">
Admin Settings
</h1>
<Link
href="/admin"
className="text-blue-600 hover:text-blue-800"
>
Back to Admin
</Link>
</div>
</div>
<div className="p-6 space-y-6">
{/* Backup Notifications Setting */}
<div className="border rounded-lg p-4">
<h3 className="text-lg font-medium text-gray-900 mb-2">
Backup Notifications
</h3>
<p className="text-sm text-gray-600 mb-4">
Select which user should receive notifications when daily database backups are completed.
</p>
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700">
Notification Recipient
</label>
<select
value={backupUserSetting?.value || ""}
onChange={(e) => handleBackupUserChange(e.target.value)}
disabled={saving}
className="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md"
>
<option value="">No notifications</option>
{users.map((user) => (
<option key={user.id} value={user.id}>
{user.name} ({user.username}) - {user.role}
</option>
))}
</select>
{saving && (
<p className="text-sm text-blue-600">Saving...</p>
)}
</div>
</div>
{/* Future settings can be added here */}
<div className="border rounded-lg p-4">
<h3 className="text-lg font-medium text-gray-900 mb-2">
System Information
</h3>
<p className="text-sm text-gray-600">
Daily database backups run automatically at 2 AM and keep the last 30 backups.
Backups are stored in the <code className="bg-gray-100 px-1 rounded">./backups/</code> directory.
</p>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,52 @@
import { NextResponse } from "next/server";
import { withAdminAuth } from "@/lib/middleware/auth";
import db from "@/lib/db";
// GET: Get all settings
async function getSettingsHandler() {
try {
const settings = db.prepare("SELECT * FROM settings ORDER BY key").all();
return NextResponse.json(settings);
} catch (error) {
console.error("Error fetching settings:", error);
return NextResponse.json(
{ error: "Failed to fetch settings" },
{ status: 500 }
);
}
}
// PUT: Update a setting
async function updateSettingHandler(request) {
try {
const { key, value } = await request.json();
if (!key || value === undefined) {
return NextResponse.json(
{ error: "Key and value are required" },
{ status: 400 }
);
}
const updatedBy = request.user.id;
const stmt = db.prepare(`
INSERT OR REPLACE INTO settings (key, value, updated_at, updated_by)
VALUES (?, ?, CURRENT_TIMESTAMP, ?)
`);
stmt.run(key, value, updatedBy);
return NextResponse.json({ success: true });
} catch (error) {
console.error("Error updating setting:", error);
return NextResponse.json(
{ error: "Failed to update setting" },
{ status: 500 }
);
}
}
// Protected routes - require admin authentication
export const GET = withAdminAuth(getSettingsHandler);
export const PUT = withAdminAuth(updateSettingHandler);

View File

@@ -500,5 +500,19 @@ export default function initializeDatabase() {
CREATE INDEX IF NOT EXISTS idx_notifications_user_read ON notifications(user_id, is_read); CREATE INDEX IF NOT EXISTS idx_notifications_user_read ON notifications(user_id, is_read);
CREATE INDEX IF NOT EXISTS idx_notifications_created ON notifications(created_at); CREATE INDEX IF NOT EXISTS idx_notifications_created ON notifications(created_at);
CREATE INDEX IF NOT EXISTS idx_notifications_type ON notifications(type); CREATE INDEX IF NOT EXISTS idx_notifications_type ON notifications(type);
-- Settings table for application configuration
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
description TEXT,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_by TEXT,
FOREIGN KEY (updated_by) REFERENCES users(id)
);
-- Insert default settings
INSERT OR IGNORE INTO settings (key, value, description) VALUES
('backup_notification_user_id', '', 'User ID to receive backup completion notifications');
`); `);
} }