feat: add settings table and backup notification functionality
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -48,3 +48,5 @@ next-env.d.ts
|
|||||||
|
|
||||||
# uploads
|
# uploads
|
||||||
/public/uploads
|
/public/uploads
|
||||||
|
|
||||||
|
/backups
|
||||||
@@ -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-'))
|
||||||
|
|||||||
25
migrate-add-settings-table.mjs
Normal file
25
migrate-add-settings-table.mjs
Normal 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);
|
||||||
|
}
|
||||||
183
src/app/admin/settings/page.js
Normal file
183
src/app/admin/settings/page.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
52
src/app/api/admin/settings/route.js
Normal file
52
src/app/api/admin/settings/route.js
Normal 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);
|
||||||
@@ -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');
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user