Source: frontend/src/pages/admin/AdminDashboard.jsx

  1. /**
  2. * @file AdminDashboard.jsx - Administrator-Verwaltungsoberfläche
  3. * @author Miray
  4. * @description Diese Komponente stellt die Hauptverwaltungsoberfläche für Administratoren dar.
  5. * Sie ermöglicht die Verwaltung von Benutzern und das Monitoring von Datenbank-Aktivitäten.
  6. *
  7. * @requires react
  8. * @requires sweetalert2
  9. * @requires ../../production-config
  10. */
  11. import React, { useEffect, useState } from "react";
  12. import Swal from "sweetalert2";
  13. import prodconfig from "../../production-config";
  14. /**
  15. * @component AdminDashboard
  16. * @description Hauptkomponente für die Administrator-Verwaltungsoberfläche
  17. *
  18. * @example
  19. * return (
  20. * <AdminDashboard />
  21. * )
  22. */
  23. /**
  24. * @typedef {Object} User
  25. * @property {number} user_id - Eindeutige Benutzer-ID
  26. * @property {string} user_name - Benutzername
  27. * @property {string} email - E-Mail-Adresse des Benutzers
  28. * @property {boolean} is_verified - Verifizierungsstatus
  29. * @property {Date} registered_at - Registrierungsdatum
  30. */
  31. /**
  32. * @typedef {Object} DBSession
  33. * @property {string} datname - Name der Datenbank
  34. * @property {string} usename - Datenbankbenutzer
  35. * @property {string} state - Aktueller Status
  36. * @property {string} query - Aktuelle Abfrage
  37. * @property {string} query_start - Startzeitpunkt der Abfrage
  38. */
  39. /**
  40. * @typedef {Object} DBStats
  41. * @property {string} datname - Name der Datenbank
  42. * @property {number} active_connections - Anzahl aktiver Verbindungen
  43. * @property {number} committed_transactions - Anzahl erfolgreich abgeschlossener Transaktionen
  44. * @property {number} rolledback_transactions - Anzahl zurückgerollter Transaktionen
  45. * @property {number} blocks_read - Anzahl gelesener Blöcke
  46. * @property {number} blocks_hit - Cache-Treffer
  47. */
  48. /**
  49. * State-Hooks der Komponente
  50. * @type {Object}
  51. * @property {User[]} users - Liste aller Benutzer
  52. * @property {User|null} editingUser - Aktuell bearbeiteter Benutzer
  53. * @property {string} newEmail - Neue E-Mail-Adresse bei Bearbeitung
  54. * @property {string} newPassword - Neues Passwort bei Bearbeitung
  55. * @property {DBSession[]} dbSessions - Aktive Datenbank-Sitzungen
  56. * @property {DBStats[]} dbStats - Datenbankstatistiken
  57. * @property {boolean} isMonitoringLoading - Ladezustand des Monitorings
  58. * @property {Set<number>} adminUserIds - Set der Admin-Benutzer-IDs
  59. */
  60. const AdminDashboard = () => {
  61. const [users, setUsers] = useState([]);
  62. const [editingUser, setEditingUser] = useState(null);
  63. const [newEmail, setNewEmail] = useState("");
  64. const [newPassword, setNewPassword] = useState("");
  65. const [dbSessions, setDbSessions] = useState([]);
  66. const [dbStats, setDbStats] = useState([]);
  67. const [isMonitoringLoading, setIsMonitoringLoading] = useState(false);
  68. const [adminUserIds, setAdminUserIds] = useState(new Set());
  69. useEffect(() => {
  70. fetchAdminUserIds();
  71. }, []);
  72. /**
  73. * @function fetchAdminUserIds
  74. * @async
  75. * @description Ruft die IDs aller Admin-Benutzer vom Server ab
  76. * @throws {Error} Wenn die Anfrage fehlschlägt
  77. */
  78. const fetchAdminUserIds = async () => {
  79. try {
  80. const response = await fetch(
  81. `${prodconfig.backendUrl}/api/admin/admin-roles`,
  82. { credentials: "include" }
  83. );
  84. const data = await response.json();
  85. setAdminUserIds(new Set(data.adminUserIds));
  86. } catch (error) {
  87. console.error("Fehler beim Abrufen der Admin-Benutzer:", error);
  88. }
  89. };
  90. useEffect(() => {
  91. fetch(`${prodconfig.backendUrl}/api/admin/users`, { credentials: "include" })
  92. .then((response) => response.json())
  93. .then((data) => setUsers(data))
  94. .catch((error) => console.error("Fehler beim Abrufen der Benutzer:", error));
  95. fetchDbSessions();
  96. fetchDbStats();
  97. }, []);
  98. /**
  99. * @function fetchDbSessions
  100. * @async
  101. * @description Ruft aktuelle Datenbank-Sitzungsinformationen ab
  102. * @throws {Error} Wenn die Abfrage fehlschlägt
  103. */
  104. const fetchDbSessions = async () => {
  105. setIsMonitoringLoading(true);
  106. try {
  107. const response = await fetch(`${prodconfig.backendUrl}/monitor/db-sessions`, { credentials: "include" });
  108. const data = await response.json();
  109. setDbSessions(data);
  110. } catch (error) {
  111. console.error("Fehler beim Abrufen der DB-Sitzungen:", error);
  112. } finally {
  113. setIsMonitoringLoading(false);
  114. }
  115. };
  116. /**
  117. * @function fetchDbStats
  118. * @async
  119. * @description Ruft Datenbankstatistiken ab
  120. * @throws {Error} Wenn die Abfrage fehlschlägt
  121. */
  122. const fetchDbStats = async () => {
  123. setIsMonitoringLoading(true);
  124. try {
  125. const response = await fetch(`${prodconfig.backendUrl}/monitor/db-stats`, { credentials: "include" });
  126. const data = await response.json();
  127. setDbStats(data);
  128. } catch (error) {
  129. console.error("Fehler beim Abrufen der DB-Statistiken:", error);
  130. } finally {
  131. setIsMonitoringLoading(false);
  132. }
  133. };
  134. /**
  135. * @function deleteUser
  136. * @async
  137. * @param {number} id - ID des zu löschenden Benutzers
  138. * @description Löscht einen Benutzer nach Bestätigung
  139. * @throws {Error} Wenn das Löschen fehlschlägt
  140. */
  141. const deleteUser = async (id) => {
  142. const confirmed = await Swal.fire({
  143. title: "Benutzer löschen?",
  144. text: "Dieser Vorgang kann nicht rückgängig gemacht werden!",
  145. icon: "warning",
  146. showCancelButton: true,
  147. confirmButtonText: "Ja, löschen!",
  148. cancelButtonText: "Abbrechen",
  149. });
  150. if (confirmed.isConfirmed) {
  151. try {
  152. await fetch(`${prodconfig.backendUrl}/api/admin/users/${id}`, { method: "DELETE", credentials: "include" });
  153. setUsers(users.filter((user) => user.user_id !== id));
  154. Swal.fire("Gelöscht!", "Der Benutzer wurde erfolgreich gelöscht.", "success");
  155. } catch (error) {
  156. Swal.fire("Fehler", "Benutzer konnte nicht gelöscht werden.", "error");
  157. console.error("Fehler beim Löschen des Benutzers:", error);
  158. }
  159. }
  160. };
  161. /**
  162. * @function assignAdmin
  163. * @async
  164. * @param {number} id - ID des Benutzers, der Admin-Rechte erhalten soll
  165. * @description Weist einem Benutzer Admin-Rechte zu
  166. * @throws {Error} Wenn die Rechtezuweisung fehlschlägt
  167. */
  168. const assignAdmin = async (id) => {
  169. const user = users.find(u => u.user_id === id);
  170. if (user?.roles?.some(role => role.role_name === 'admin')) {
  171. Swal.fire("Info", "Dieser Benutzer ist bereits Admin.", "info");
  172. return;
  173. }
  174. const confirmed = await Swal.fire({
  175. title: "Sind Sie sicher?",
  176. text: "Dieser Benutzer wird Admin-Rechte erhalten.",
  177. icon: "warning",
  178. showCancelButton: true,
  179. confirmButtonText: "Ja, befördern!",
  180. cancelButtonText: "Abbrechen",
  181. });
  182. if (!confirmed.isConfirmed) return;
  183. try {
  184. const response = await fetch(`${prodconfig.backendUrl}/api/admin/users/${id}/assign-admin`, {
  185. method: "POST",
  186. credentials: "include",
  187. });
  188. if (!response.ok) {
  189. const errorData = await response.json();
  190. throw new Error(errorData.message || "Fehler beim Zuweisen der Admin-Rechte.");
  191. }
  192. await Swal.fire("Erfolg", "Admin-Rechte erfolgreich zugewiesen.", "success");
  193. // Seite neu laden
  194. window.location.reload();
  195. } catch (error) {
  196. Swal.fire("Fehler", error.message, "error");
  197. console.error("Fehler beim Zuweisen der Admin-Rechte:", error);
  198. }
  199. };
  200. /**
  201. * @function updateUser
  202. * @async
  203. * @description Aktualisiert die Benutzerdaten des ausgewählten Benutzers
  204. * @throws {Error} Wenn die Aktualisierung fehlschlägt
  205. */
  206. const updateUser = async () => {
  207. try {
  208. const response = await fetch(`${prodconfig.backendUrl}/api/admin/users/${editingUser.user_id}`, {
  209. method: "PUT",
  210. headers: { "Content-Type": "application/json" },
  211. credentials: "include",
  212. body: JSON.stringify({ email: newEmail, password: newPassword }),
  213. });
  214. if (!response.ok) {
  215. const errorData = await response.json();
  216. throw new Error(errorData.message || "Fehler beim Aktualisieren.");
  217. }
  218. setUsers((prevUsers) =>
  219. prevUsers.map((user) =>
  220. user.user_id === editingUser.user_id ? { ...user, email: newEmail } : user
  221. )
  222. );
  223. Swal.fire("Erfolg", "Benutzer erfolgreich aktualisiert.", "success");
  224. setEditingUser(null);
  225. setNewEmail("");
  226. setNewPassword("");
  227. } catch (error) {
  228. console.error("Fehler beim Aktualisieren des Benutzers:", error);
  229. }
  230. };
  231. return (
  232. <div className="dashboard">
  233. <div className="shadow-sm bg-white border-y border-slate-200">
  234. <div className="flex items-center justify-between py-2 px-3">
  235. <h1 className="text-lg font-semibold text-black">Admin-Dashboard</h1>
  236. <button
  237. onClick={() => { window.location.href = "/dashboard"; }}
  238. className="text-white bg-red-500 hover:bg-red-600 px-4 py-2 rounded-md"
  239. >
  240. Zurück zum Dashboard
  241. </button>
  242. </div>
  243. </div>
  244. <div className="mt-16 px-4">
  245. <h2 className="text-xl font-semibold mb-4">Benutzerverwaltung</h2>
  246. <table className="w-full text-black border-collapse border border-slate-300">
  247. <thead>
  248. <tr>
  249. <th className="border border-slate-300 px-4 py-2">ID</th>
  250. <th className="border border-slate-300 px-4 py-2">Benutzername</th>
  251. <th className="border border-slate-300 px-4 py-2">E-Mail</th>
  252. <th className="border border-slate-300 px-4 py-2">Verifiziert</th>
  253. <th className="border border-slate-300 px-4 py-2">Registriert am</th>
  254. <th className="border border-slate-300 px-4 py-2">Admin</th>
  255. <th className="border border-slate-300 px-4 py-2">Aktionen</th>
  256. </tr>
  257. </thead>
  258. <tbody>
  259. {users.map((user) => (
  260. <tr key={user.user_id}>
  261. <td className="border border-slate-300 px-4 py-2">{user.user_id}</td>
  262. <td className="border border-slate-300 px-4 py-2">{user.user_name}</td>
  263. <td className="border border-slate-300 px-4 py-2">{user.email}</td>
  264. <td className="border border-slate-300 px-4 py-2">
  265. {user.is_verified ? "Ja" : "Nein"}
  266. </td>
  267. <td className="border border-slate-300 px-4 py-2">
  268. {user.registered_at
  269. ? new Date(user.registered_at).toLocaleDateString("de-DE")
  270. : "Kein Datum"}
  271. </td>
  272. <td className="border border-slate-300 px-4 py-2">
  273. {adminUserIds.has(user.user_id) ? "Ja" : "Nein"}
  274. </td>
  275. <td className="border border-slate-300 px-4 py-2 flex gap-2">
  276. <button
  277. className={`px-3 py-1 rounded ${adminUserIds.has(user.user_id)
  278. ? "bg-gray-200 text-gray-600 cursor-not-allowed border border-gray-300"
  279. : "bg-[#669f62] hover:bg-[#68b461] text-white"
  280. }`}
  281. onClick={() => !adminUserIds.has(user.user_id) && assignAdmin(user.user_id)}
  282. disabled={adminUserIds.has(user.user_id)}
  283. title={
  284. adminUserIds.has(user.user_id)
  285. ? "Benutzer ist bereits Admin"
  286. : "Zum Admin befördern"
  287. }
  288. >
  289. {adminUserIds.has(user.user_id) ? "Bereits Admin" : "Admin-Rechte zuweisen"}
  290. </button>
  291. <button
  292. className="bg-red-500 text-white px-3 py-1 rounded hover:bg-red-600"
  293. onClick={() => deleteUser(user.user_id)}
  294. >
  295. Löschen
  296. </button>
  297. <button
  298. className="bg-blue-500 text-white px-3 py-1 rounded hover:bg-blue-600"
  299. onClick={() => {
  300. setEditingUser(user);
  301. setNewEmail(user.email);
  302. setNewPassword("");
  303. }}
  304. >
  305. Bearbeiten
  306. </button>
  307. </td>
  308. </tr>
  309. ))}
  310. </tbody>
  311. </table>
  312. </div>
  313. {editingUser && (
  314. <div className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50">
  315. <div className="bg-white p-6 rounded-lg shadow-lg max-w-md w-full">
  316. <h2 className="text-xl font-bold mb-4">Benutzer bearbeiten</h2>
  317. <form
  318. onSubmit={(e) => {
  319. e.preventDefault();
  320. updateUser();
  321. }}
  322. >
  323. <div className="mb-4">
  324. <label className="block text-sm font-medium text-gray-700">
  325. Benutzername:
  326. </label>
  327. <input
  328. type="text"
  329. value={editingUser.user_name}
  330. disabled
  331. className="w-full mt-1 p-2 border border-gray-300 rounded-md"
  332. />
  333. </div>
  334. <div className="mb-4">
  335. <label className="block text-sm font-medium text-gray-700">
  336. E-Mail:
  337. </label>
  338. <input
  339. type="email"
  340. value={newEmail}
  341. onChange={(e) => setNewEmail(e.target.value)}
  342. className="w-full mt-1 p-2 border border-gray-300 rounded-md"
  343. />
  344. </div>
  345. <div className="mb-4">
  346. <label className="block text-sm font-medium text-gray-700">
  347. Neues Passwort:
  348. </label>
  349. <input
  350. type="password"
  351. value={newPassword}
  352. onChange={(e) => setNewPassword(e.target.value)}
  353. className="w-full mt-1 p-2 border border-gray-300 rounded-md"
  354. />
  355. </div>
  356. <div className="flex justify-end gap-2">
  357. <button
  358. type="submit"
  359. className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
  360. >
  361. Speichern
  362. </button>
  363. <button
  364. type="button"
  365. onClick={() => {
  366. setEditingUser(null);
  367. setNewEmail("");
  368. setNewPassword("");
  369. }}
  370. className="bg-gray-300 px-4 py-2 rounded hover:bg-gray-400"
  371. >
  372. Abbrechen
  373. </button>
  374. </div>
  375. </form>
  376. </div>
  377. </div>
  378. )}
  379. <div className="monitoring-section mt-8">
  380. <h2 className="text-xl font-semibold mb-4">System Monitoring</h2>
  381. {isMonitoringLoading ? (
  382. <p>Lade Monitoring-Daten...</p>
  383. ) : (
  384. <>
  385. <div className="mb-6">
  386. <h3 className="text-lg font-medium">Aktive DB-Sitzungen</h3>
  387. <table className="w-full text-black border-collapse border border-slate-300">
  388. <thead>
  389. <tr>
  390. <th className="border border-slate-300 px-4 py-2">Datenbank</th>
  391. <th className="border border-slate-300 px-4 py-2">Benutzer</th>
  392. <th className="border border-slate-300 px-4 py-2">Status</th>
  393. <th className="border border-slate-300 px-4 py-2">Abfrage</th>
  394. <th className="border border-slate-300 px-4 py-2">Abfragebeginn</th>
  395. </tr>
  396. </thead>
  397. <tbody>
  398. {dbSessions.map((session, index) => (
  399. <tr key={index}>
  400. <td className="border border-slate-300 px-4 py-2">{session.datname}</td>
  401. <td className="border border-slate-300 px-4 py-2">{session.usename}</td>
  402. <td className="border border-slate-300 px-4 py-2">{session.state}</td>
  403. <td className="border border-slate-300 px-4 py-2">{session.query}</td>
  404. <td className="border border-slate-300 px-4 py-2">{session.query_start}</td>
  405. </tr>
  406. ))}
  407. </tbody>
  408. </table>
  409. </div>
  410. <div>
  411. <h3 className="text-lg font-medium">Datenbankstatistiken</h3>
  412. <table className="w-full text-black border-collapse border border-slate-300">
  413. <thead>
  414. <tr>
  415. <th className="border border-slate-300 px-4 py-2">Datenbank</th>
  416. <th className="border border-slate-300 px-4 py-2">Aktive Verbindungen</th>
  417. <th className="border border-slate-300 px-4 py-2">Transaktionen (Commit)</th>
  418. <th className="border border-slate-300 px-4 py-2">Transaktionen (Rollback)</th>
  419. <th className="border border-slate-300 px-4 py-2">Gelesene Blöcke</th>
  420. <th className="border border-slate-300 px-4 py-2">Treffer in Blöcken</th>
  421. </tr>
  422. </thead>
  423. <tbody>
  424. {dbStats.map((stat, index) => (
  425. <tr key={index}>
  426. <td className="border border-slate-300 px-4 py-2">{stat.datname}</td>
  427. <td className="border border-slate-300 px-4 py-2">{stat.active_connections}</td>
  428. <td className="border border-slate-300 px-4 py-2">{stat.committed_transactions}</td>
  429. <td className="border border-slate-300 px-4 py-2">{stat.rolledback_transactions}</td>
  430. <td className="border border-slate-300 px-4 py-2">{stat.blocks_read}</td>
  431. <td className="border border-slate-300 px-4 py-2">{stat.blocks_hit}</td>
  432. </tr>
  433. ))}
  434. </tbody>
  435. </table>
  436. </div>
  437. </>
  438. )}
  439. </div>
  440. </div>
  441. );
  442. };
  443. export default AdminDashboard;