335 lines
10 KiB
JavaScript
335 lines
10 KiB
JavaScript
"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([]);
|
|
const [cronStatus, setCronStatus] = useState(null);
|
|
const [cronLoading, setCronLoading] = useState(false);
|
|
const [cronActionLoading, setCronActionLoading] = useState(null);
|
|
|
|
// Redirect if not admin
|
|
useEffect(() => {
|
|
if (status === "loading") return;
|
|
if (!session || session.user.role !== "admin") {
|
|
router.push("/");
|
|
return;
|
|
}
|
|
fetchSettings();
|
|
fetchUsers();
|
|
fetchCronStatus();
|
|
}, [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 fetchCronStatus = async () => {
|
|
setCronLoading(true);
|
|
try {
|
|
const response = await fetch("/api/admin/cron");
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
setCronStatus(data);
|
|
}
|
|
} catch (error) {
|
|
console.error("Error fetching cron status:", error);
|
|
} finally {
|
|
setCronLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleCronAction = async (action) => {
|
|
setCronActionLoading(action);
|
|
try {
|
|
const response = await fetch("/api/admin/cron", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ action }),
|
|
});
|
|
const data = await response.json();
|
|
if (data.success) {
|
|
alert(data.message);
|
|
fetchCronStatus();
|
|
} else {
|
|
alert("Error: " + data.message);
|
|
}
|
|
} catch (error) {
|
|
console.error("Error performing cron action:", error);
|
|
alert("Error performing action");
|
|
} finally {
|
|
setCronActionLoading(null);
|
|
}
|
|
};
|
|
|
|
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">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<h3 className="text-lg font-medium text-gray-900">
|
|
Cron Jobs Status
|
|
</h3>
|
|
<button
|
|
onClick={fetchCronStatus}
|
|
disabled={cronLoading}
|
|
className="text-sm text-blue-600 hover:text-blue-800"
|
|
>
|
|
{cronLoading ? "Refreshing..." : "↻ Refresh"}
|
|
</button>
|
|
</div>
|
|
|
|
{cronLoading && !cronStatus ? (
|
|
<p className="text-sm text-gray-500">Loading cron status...</p>
|
|
) : cronStatus ? (
|
|
<div className="space-y-4">
|
|
{/* Status indicators */}
|
|
<div className="flex flex-wrap gap-3">
|
|
<div className={`inline-flex items-center px-3 py-1 rounded-full text-sm font-medium ${
|
|
cronStatus.available
|
|
? "bg-green-100 text-green-800"
|
|
: "bg-yellow-100 text-yellow-800"
|
|
}`}>
|
|
{cronStatus.available ? "✓ Cron Available" : "⚠ Cron Unavailable"}
|
|
</div>
|
|
{cronStatus.available && (
|
|
<div className={`inline-flex items-center px-3 py-1 rounded-full text-sm font-medium ${
|
|
cronStatus.running
|
|
? "bg-green-100 text-green-800"
|
|
: "bg-red-100 text-red-800"
|
|
}`}>
|
|
{cronStatus.running ? "✓ Daemon Running" : "✗ Daemon Not Running"}
|
|
</div>
|
|
)}
|
|
{cronStatus.available && (
|
|
<div className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-blue-800">
|
|
{cronStatus.jobCount || 0} Job(s) Scheduled
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Scheduled jobs */}
|
|
{cronStatus.jobs && cronStatus.jobs.length > 0 && (
|
|
<div>
|
|
<p className="text-sm font-medium text-gray-700 mb-1">Scheduled Jobs:</p>
|
|
<div className="bg-gray-50 rounded p-2 font-mono text-xs">
|
|
{cronStatus.jobs.map((job, idx) => (
|
|
<div key={idx} className="py-0.5">{job}</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Last backup info */}
|
|
{cronStatus.lastBackup && (
|
|
<div className="text-sm">
|
|
<span className="font-medium text-gray-700">Last Backup: </span>
|
|
{cronStatus.lastBackup.exists ? (
|
|
<span className="text-green-600">
|
|
{cronStatus.lastBackup.filename} ({new Date(cronStatus.lastBackup.date).toLocaleString()})
|
|
<span className="text-gray-500 ml-2">
|
|
({cronStatus.lastBackup.count} total backups)
|
|
</span>
|
|
</span>
|
|
) : (
|
|
<span className="text-gray-500">{cronStatus.lastBackup.message || "No backups"}</span>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Message for non-Linux environments */}
|
|
{cronStatus.message && (
|
|
<p className="text-sm text-yellow-600">{cronStatus.message}</p>
|
|
)}
|
|
|
|
{/* Action buttons */}
|
|
<div className="flex flex-wrap gap-2 pt-2">
|
|
{cronStatus.available && (
|
|
<button
|
|
onClick={() => handleCronAction("restart")}
|
|
disabled={cronActionLoading === "restart"}
|
|
className="px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded hover:bg-blue-700 disabled:opacity-50"
|
|
>
|
|
{cronActionLoading === "restart" ? "Restarting..." : "🔄 Restart Cron Jobs"}
|
|
</button>
|
|
)}
|
|
<button
|
|
onClick={() => handleCronAction("run-backup")}
|
|
disabled={cronActionLoading === "run-backup"}
|
|
className="px-4 py-2 bg-green-600 text-white text-sm font-medium rounded hover:bg-green-700 disabled:opacity-50"
|
|
>
|
|
{cronActionLoading === "run-backup" ? "Running..." : "💾 Run Backup Now"}
|
|
</button>
|
|
<button
|
|
onClick={() => handleCronAction("run-reminders")}
|
|
disabled={cronActionLoading === "run-reminders"}
|
|
className="px-4 py-2 bg-purple-600 text-white text-sm font-medium rounded hover:bg-purple-700 disabled:opacity-50"
|
|
>
|
|
{cronActionLoading === "run-reminders" ? "Running..." : "📧 Send Reminders Now"}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<p className="text-sm text-red-500">Failed to load cron status</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* System Information */}
|
|
<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>
|
|
);
|
|
} |