WebOrbiton
v3.0.0.0

FlatlyPage

623 lines · 34.3 KB
  1. <?php
  2. require_once __DIR__ . "/../config.php";
  3. require_once __DIR__ . "/sidebar.php";
  4. require_once __DIR__ . "/login_tracking.php";
  5. ​
  6. require_login();
  7. ​
  8. $message = '';
  9. $message_type = '';
  10. $loginTracker = new LoginTracker();
  11. ​
  12. $loginTracker->updateActivity();
  13. ​
  14. $adminFile = DATA_DIR . '/admin.json';
  15. $adminData = json_decode(file_get_contents($adminFile), true);
  16. ​
  17. if ($_SERVER['REQUEST_METHOD'] === 'POST') {
  18. $token = $_POST['csrf_token'] ?? '';
  19. if (!verify_csrf_token($token)) {
  20. $message = 'Invalid request. Please try again.';
  21. $message_type = 'error';
  22. } else {
  23. $action = $_POST['action'] ?? '';
  24. switch ($action) {
  25. case 'logout_session':
  26. $loginId = $_POST['login_id'] ?? '';
  27. if ($loginTracker->logoutSession($loginId)) {
  28. $message = 'Session logged out successfully.';
  29. $message_type = 'success';
  30. } else {
  31. $message = 'Failed to logout session.';
  32. $message_type = 'error';
  33. }
  34. break;
  35. case 'ban_ip':
  36. $ipToBan = $_POST['ip_address'] ?? '';
  37. if ($loginTracker->banIP($ipToBan)) {
  38. $message = 'IP address banned and all sessions terminated.';
  39. $message_type = 'success';
  40. } else {
  41. $message = 'Failed to ban IP address.';
  42. $message_type = 'error';
  43. }
  44. break;
  45. case 'unban_ip':
  46. $ipToUnban = $_POST['ip_address'] ?? '';
  47. if ($loginTracker->unbanIP($ipToUnban)) {
  48. $message = 'IP address unbanned successfully.';
  49. $message_type = 'success';
  50. } else {
  51. $message = 'Failed to unban IP address.';
  52. $message_type = 'error';
  53. }
  54. break;
  55. case 'clean_old_sessions':
  56. if ($loginTracker->cleanOldSessions(30)) {
  57. $message = 'Old sessions cleaned successfully.';
  58. $message_type = 'success';
  59. } else {
  60. $message = 'Failed to clean old sessions.';
  61. $message_type = 'error';
  62. }
  63. break;
  64. case 'update_username':
  65. $newUsername = trim($_POST['new_username'] ?? '');
  66. $currentPassword = $_POST['current_password'] ?? '';
  67. if (empty($newUsername)) {
  68. $message = 'Username cannot be empty.';
  69. $message_type = 'error';
  70. } elseif (!password_verify($currentPassword, $adminData['password'])) {
  71. $message = 'Current password is incorrect.';
  72. $message_type = 'error';
  73. } else {
  74. $adminData['username'] = $newUsername;
  75. $adminData['updated_at'] = date('Y-m-d H:i:s');
  76. if (file_put_contents($adminFile, json_encode($adminData, JSON_PRETTY_PRINT))) {
  77. $_SESSION['admin_username'] = $newUsername;
  78. $message = 'Username updated successfully!';
  79. $message_type = 'success';
  80. } else {
  81. $message = 'Failed to update username.';
  82. $message_type = 'error';
  83. }
  84. }
  85. break;
  86. case 'change_password':
  87. $currentPassword = $_POST['current_password'] ?? '';
  88. $newPassword = $_POST['new_password'] ?? '';
  89. $confirmPassword = $_POST['confirm_password'] ?? '';
  90. if (empty($currentPassword) || empty($newPassword)) {
  91. $message = 'Please fill in all password fields.';
  92. $message_type = 'error';
  93. } elseif (!password_verify($currentPassword, $adminData['password'])) {
  94. $message = 'Current password is incorrect.';
  95. $message_type = 'error';
  96. } elseif (strlen($newPassword) < 8) {
  97. $message = 'New password must be at least 8 characters.';
  98. $message_type = 'error';
  99. } elseif ($newPassword !== $confirmPassword) {
  100. $message = 'New passwords do not match.';
  101. $message_type = 'error';
  102. } else {
  103. $adminData['password'] = password_hash($newPassword, PASSWORD_DEFAULT);
  104. $adminData['updated_at'] = date('Y-m-d H:i:s');
  105. if (file_put_contents($adminFile, json_encode($adminData, JSON_PRETTY_PRINT))) {
  106. $message = 'Password changed successfully!';
  107. $message_type = 'success';
  108. } else {
  109. $message = 'Failed to change password.';
  110. $message_type = 'error';
  111. }
  112. }
  113. break;
  114. ​
  115. case 'update_email':
  116. $newEmail = strtolower(trim($_POST['new_email'] ?? ''));
  117. $currentPassword = $_POST['current_password'] ?? '';
  118. if ($newEmail !== '' && !filter_var($newEmail, FILTER_VALIDATE_EMAIL)) {
  119. $message = 'Please enter a valid email address.';
  120. $message_type = 'error';
  121. } elseif (!password_verify($currentPassword, $adminData['password'])) {
  122. $message = 'Current password is incorrect.';
  123. $message_type = 'error';
  124. } else {
  125. $adminData['email'] = $newEmail;
  126. $adminData['updated_at'] = date('Y-m-d H:i:s');
  127. if (file_put_contents($adminFile, json_encode($adminData, JSON_PRETTY_PRINT))) {
  128. $message = 'Email updated successfully!';
  129. $message_type = 'success';
  130. } else {
  131. $message = 'Failed to update email.';
  132. $message_type = 'error';
  133. }
  134. }
  135. break;
  136. case 'logout':
  137. if (isset($_SESSION['login_token'])) {
  138. $activeLogins = $loginTracker->getActiveLogins();
  139. foreach ($activeLogins as $login) {
  140. if ($login['is_current']) {
  141. $loginTracker->logoutSession($login['id']);
  142. break;
  143. }
  144. }
  145. }
  146. $_SESSION = array();
  147. if (ini_get("session.use_cookies")) {
  148. $params = session_get_cookie_params();
  149. setcookie(session_name(), '', time() - 42000,
  150. $params["path"], $params["domain"],
  151. $params["secure"], $params["httponly"]
  152. );
  153. }
  154. session_destroy();
  155. header('Location: ' . BASE_URL . '/admin');
  156. exit;
  157. break;
  158. }
  159. }
  160. }
  161. ​
  162. $csrf_token = generate_csrf_token();
  163. $site_settings = get_site_settings();
  164. $activeLogins = $loginTracker->getActiveLogins();
  165. $bannedIPs = $loginTracker->getBannedIPs();
  166. ?>
  167. <!DOCTYPE html>
  168. <html lang="en">
  169. <head>
  170. <meta charset="UTF-8">
  171. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  172. <title>Account Settings - <?= e($site_settings['site_name'] ?? SITE_NAME) ?></title>
  173. <link rel="icon" href="admin.ico?v=<?= filemtime(__DIR__ . '/../css/admin.css') ?>" type="image/x-icon">
  174. <?= admin_font_head() ?>
  175. <link rel="stylesheet" href="/css/admin.css?v=<?= filemtime(__DIR__ . '/../css/admin.css') ?>">
  176. <script src="/assets/js/admin-theme.js?v=5"></script>
  177. <style>
  178. .session-card {
  179. background: var(--bg);
  180. border: 1px solid var(--border);
  181. border-radius: 12px;
  182. padding: 16px;
  183. margin-bottom: 12px;
  184. }
  185. .session-card.current {
  186. border-color: var(--primary);
  187. background: var(--primary-light, rgba(59, 130, 246, 0.05));
  188. }
  189. .session-info {
  190. display: flex;
  191. align-items: start;
  192. gap: 12px;
  193. }
  194. .session-icon {
  195. width: 40px;
  196. height: 40px;
  197. border-radius: 8px;
  198. background: var(--primary);
  199. display: flex;
  200. align-items: center;
  201. justify-content: center;
  202. flex-shrink: 0;
  203. }
  204. .session-details {
  205. flex: 1;
  206. }
  207. .session-actions {
  208. display: flex;
  209. gap: 8px;
  210. margin-top: 12px;
  211. }
  212. .badge {
  213. display: inline-block;
  214. padding: 4px 8px;
  215. border-radius: 6px;
  216. font-size: 0.75rem;
  217. font-weight: 500;
  218. }
  219. .badge-current {
  220. background: var(--primary);
  221. }
  222. .badge-info {
  223. background: var(--bg);
  224. border: 1px solid var(--border);
  225. color: var(--text-muted);
  226. }
  227. .ip-item {
  228. display: flex;
  229. align-items: center;
  230. justify-content: space-between;
  231. padding: 12px;
  232. background: var(--bg);
  233. border: 1px solid var(--border);
  234. border-radius: 8px;
  235. margin-bottom: 8px;
  236. }
  237. </style>
  238. </head>
  239. <body>
  240. <div class="app">
  241. <?php admin_sidebar('account'); ?>
  242. <main class="main">
  243. <header class="main-header">
  244. <button class="mobile-nav-toggle" onclick="document.querySelector('.sidebar').classList.add('open')">
  245. <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
  246. <line x1="3" y1="6" x2="21" y2="6"/>
  247. <line x1="3" y1="12" x2="21" y2="12"/>
  248. <line x1="3" y1="18" x2="21" y2="18"/>
  249. </svg>
  250. </button>
  251. ​
  252. <div class="main-header-inner">
  253. <h1>Account Settings</h1>
  254. <div class="header-actions">
  255. <button type="button" onclick="toggleTheme()" class="btn btn-secondary btn-sm" id="theme-toggle-btn">
  256. <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" width="16" height="16">
  257. <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
  258. </svg>
  259. <span>Theme</span>
  260. </button>
  261. <a href="/admin/dashboard.php" class="btn btn-secondary btn-sm">
  262. <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" width="16" height="16"><path d="M19 12H5"/><polyline points="12 19 5 12 12 5"/></svg>
  263. Back to Dashboard
  264. </a>
  265. </div>
  266. </div>
  267. </header>
  268. <div class="main-content">
  269. <?php if ($message): ?>
  270. <div class="message <?= $message_type ?>">
  271. <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" width="20" height="20">
  272. <?php if ($message_type === 'success'): ?>
  273. <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/>
  274. <?php else: ?>
  275. <circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/>
  276. <?php endif; ?>
  277. </svg>
  278. <?= e($message) ?>
  279. </div>
  280. <?php endif; ?>
  281. ​
  282. <div style="max-width: 1200px;">
  283. <div class="card" style="margin-bottom: 24px;">
  284. <div class="card-header">
  285. <h3 class="card-title">Account Information</h3>
  286. </div>
  287. <div class="card-body">
  288. <div style="display: flex; align-items: center; gap: 20px; padding: 20px; background: var(--bg); border-radius: 12px; border: 1px solid var(--border);">
  289. <div class="user-avatar" style="width: 64px; height: 64px; font-size: 24px;">
  290. <?= strtoupper(substr($adminData['username'], 0, 1)) ?>
  291. </div>
  292. <div>
  293. <div style="font-weight: 600; font-size: 1.125rem; margin-bottom: 4px;">
  294. <?= e($adminData['username']) ?>
  295. </div>
  296. <div style="color: var(--text-muted); font-size: 0.875rem;">
  297. Administrator
  298. </div>
  299. <?php if (isset($adminData['created_at'])): ?>
  300. <div style="color: var(--text-muted); font-size: 0.8125rem; margin-top: 8px;">
  301. Account created: <?= date('F j, Y', strtotime($adminData['created_at'])) ?>
  302. </div>
  303. <?php endif; ?>
  304. </div>
  305. </div>
  306. </div>
  307. </div>
  308. ​
  309. <div class="card" style="margin-bottom: 24px;">
  310. <div class="card-header" style="display: flex; justify-content: space-between; align-items: center;">
  311. <h3 class="card-title">Active Sessions (<?= count($activeLogins) ?>)</h3>
  312. <form method="POST" action="" style="margin: 0;">
  313. <input type="hidden" name="csrf_token" value="<?= e($csrf_token) ?>">
  314. <input type="hidden" name="action" value="clean_old_sessions">
  315. <button type="submit" class="btn btn-secondary btn-sm">
  316. <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" width="16" height="16">
  317. <polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/>
  318. </svg>
  319. Clean Old Sessions
  320. </button>
  321. </form>
  322. </div>
  323. <div class="card-body">
  324. <?php if (empty($activeLogins)): ?>
  325. <p style="color: var(--text-muted); text-align: center; padding: 20px;">No active sessions</p>
  326. <?php else: ?>
  327. <?php foreach ($activeLogins as $login): ?>
  328. <div class="session-card <?= $login['is_current'] ? 'current' : '' ?>">
  329. <div class="session-info">
  330. <div class="session-icon">
  331. <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" width="20" height="20">
  332. <?php if (stripos($login['platform'], 'windows') !== false): ?>
  333. <rect x="2" y="3" width="20" height="14" rx="2" ry="2"/>
  334. <line x1="8" y1="21" x2="16" y2="21"/>
  335. <line x1="12" y1="17" x2="12" y2="21"/>
  336. <?php elseif (stripos($login['platform'], 'ios') !== false || stripos($login['platform'], 'android') !== false): ?>
  337. <rect x="5" y="2" width="14" height="20" rx="2" ry="2"/>
  338. <line x1="12" y1="18" x2="12.01" y2="18"/>
  339. <?php else: ?>
  340. <rect x="2" y="7" width="20" height="15" rx="2" ry="2"/>
  341. <polyline points="17 2 12 7 7 2"/>
  342. <?php endif; ?>
  343. </svg>
  344. </div>
  345. <div class="session-details">
  346. <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
  347. <strong><?= e($login['platform']) ?> • <?= e($login['browser']) ?></strong>
  348. <?php if ($login['is_current']): ?>
  349. <span class="badge badge-current">Current Session</span>
  350. <?php endif; ?>
  351. </div>
  352. <div style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 4px;">
  353. <strong>IP:</strong> <?= e($login['ip']) ?>
  354. </div>
  355. <div style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 4px;">
  356. <strong>Language:</strong> <?= e($login['language']) ?>
  357. </div>
  358. <div style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 4px;">
  359. <strong>Login:</strong> <?= date('M j, Y g:i A', strtotime($login['login_time'])) ?>
  360. </div>
  361. <div style="font-size: 0.875rem; color: var(--text-muted);">
  362. <strong>Last Activity:</strong> <?= date('M j, Y g:i A', strtotime($login['last_activity'])) ?>
  363. </div>
  364. <div class="session-actions">
  365. <?php if (!$login['is_current']): ?>
  366. <form method="POST" action="" style="margin: 0; display: inline-block;">
  367. <input type="hidden" name="csrf_token" value="<?= e($csrf_token) ?>">
  368. <input type="hidden" name="action" value="logout_session">
  369. <input type="hidden" name="login_id" value="<?= e($login['id']) ?>">
  370. <button type="submit" class="btn btn-secondary btn-sm" onclick="return confirm('Logout this session?')">
  371. <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" width="14" height="14">
  372. <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/>
  373. </svg>
  374. Logout
  375. </button>
  376. </form>
  377. <?php endif; ?>
  378. <form method="POST" action="" style="margin: 0; display: inline-block;">
  379. <input type="hidden" name="csrf_token" value="<?= e($csrf_token) ?>">
  380. <input type="hidden" name="action" value="ban_ip">
  381. <input type="hidden" name="ip_address" value="<?= e($login['ip']) ?>">
  382. <button type="submit" class="btn btn-secondary btn-sm" onclick="return confirm('Ban this IP address? All sessions from this IP will be terminated.')">
  383. <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" width="14" height="14">
  384. <circle cx="12" cy="12" r="10"/><line x1="4.93" y1="4.93" x2="19.07" y2="19.07"/>
  385. </svg>
  386. Ban IP
  387. </button>
  388. </form>
  389. </div>
  390. </div>
  391. </div>
  392. </div>
  393. <?php endforeach; ?>
  394. <?php endif; ?>
  395. </div>
  396. </div>
  397. ​
  398. <?php if (!empty($bannedIPs)): ?>
  399. <div class="card" style="margin-bottom: 24px;">
  400. <div class="card-header">
  401. <h3 class="card-title">Banned IP Addresses (<?= count($bannedIPs) ?>)</h3>
  402. </div>
  403. <div class="card-body">
  404. <?php foreach ($bannedIPs as $ip): ?>
  405. <div class="ip-item">
  406. <div>
  407. <div style="font-weight: 500; font-family: 'Courier New', monospace;"><?= e($ip) ?></div>
  408. <div style="font-size: 0.875rem; color: var(--text-muted); margin-top: 4px;">
  409. All access denied from this IP
  410. </div>
  411. </div>
  412. <form method="POST" action="" style="margin: 0;">
  413. <input type="hidden" name="csrf_token" value="<?= e($csrf_token) ?>">
  414. <input type="hidden" name="action" value="unban_ip">
  415. <input type="hidden" name="ip_address" value="<?= e($ip) ?>">
  416. <button type="submit" class="btn btn-secondary btn-sm" onclick="return confirm('Unban this IP address?')">
  417. <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" width="14" height="14">
  418. <polyline points="20 6 9 17 4 12"/>
  419. </svg>
  420. Unban
  421. </button>
  422. </form>
  423. </div>
  424. <?php endforeach; ?>
  425. </div>
  426. </div>
  427. <?php endif; ?>
  428. ​
  429. <div class="card" style="margin-bottom: 24px;">
  430. <div class="card-header">
  431. <h3 class="card-title">Change Username</h3>
  432. </div>
  433. <div class="card-body">
  434. <form method="POST" action="">
  435. <input type="hidden" name="csrf_token" value="<?= e($csrf_token) ?>">
  436. <input type="hidden" name="action" value="update_username">
  437. <div class="form-group">
  438. <label class="form-label">New Username</label>
  439. <input type="text"
  440. name="new_username"
  441. class="form-input"
  442. value="<?= e($adminData['username']) ?>"
  443. required
  444. autocomplete="username">
  445. </div>
  446. <div class="form-group">
  447. <label class="form-label">Current Password (to confirm)</label>
  448. <input type="password"
  449. name="current_password"
  450. class="form-input"
  451. required
  452. autocomplete="current-password"
  453. placeholder="Enter your current password">
  454. </div>
  455. <div style="display: flex; justify-content: flex-end; margin-top: 20px;">
  456. <button type="submit" class="btn btn-primary">
  457. <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" width="16" height="16"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>
  458. Update Username
  459. </button>
  460. </div>
  461. </form>
  462. </div>
  463. </div>
  464. ​
  465. <div class="card" style="margin-bottom: 24px;">
  466. <div class="card-header">
  467. <h3 class="card-title">Change Email</h3>
  468. </div>
  469. <div class="card-body">
  470. <form method="POST" action="">
  471. <input type="hidden" name="csrf_token" value="<?= e($csrf_token) ?>">
  472. <input type="hidden" name="action" value="update_email">
  473. <div class="form-group">
  474. <label class="form-label">Email Address</label>
  475. <input type="email"
  476. name="new_email"
  477. class="form-input"
  478. value="<?= e($adminData['email'] ?? '') ?>"
  479. autocomplete="email"
  480. placeholder="you@example.com">
  481. <p class="form-hint">Used for login security alerts</p>
  482. </div>
  483. <div class="form-group">
  484. <label class="form-label">Current Password (to confirm)</label>
  485. <input type="password"
  486. name="current_password"
  487. class="form-input"
  488. required
  489. autocomplete="current-password"
  490. placeholder="Enter your current password">
  491. </div>
  492. <div style="display: flex; justify-content: flex-end; margin-top: 20px;">
  493. <button type="submit" class="btn btn-primary">
  494. <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" width="16" height="16"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>
  495. Update Email
  496. </button>
  497. </div>
  498. </form>
  499. </div>
  500. </div>
  501. ​
  502. <div class="card" style="margin-bottom: 24px;">
  503. <div class="card-header">
  504. <h3 class="card-title">Change Password</h3>
  505. </div>
  506. <div class="card-body">
  507. <form method="POST" action="">
  508. <input type="hidden" name="csrf_token" value="<?= e($csrf_token) ?>">
  509. <input type="hidden" name="action" value="change_password">
  510. <div class="form-group">
  511. <label class="form-label">Current Password</label>
  512. <input type="password"
  513. name="current_password"
  514. class="form-input"
  515. required
  516. autocomplete="current-password"
  517. placeholder="Enter your current password">
  518. </div>
  519. <div class="form-group">
  520. <label class="form-label">New Password</label>
  521. <input type="password"
  522. name="new_password"
  523. class="form-input"
  524. required
  525. minlength="8"
  526. autocomplete="new-password"
  527. placeholder="Minimum 8 characters">
  528. <p class="form-hint">Must be at least 8 characters long</p>
  529. </div>
  530. <div class="form-group">
  531. <label class="form-label">Confirm New Password</label>
  532. <input type="password"
  533. name="confirm_password"
  534. class="form-input"
  535. required
  536. minlength="8"
  537. autocomplete="new-password"
  538. placeholder="Re-enter new password">
  539. </div>
  540. <div style="display: flex; justify-content: flex-end; margin-top: 20px;">
  541. <button type="submit" class="btn btn-primary">
  542. <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" width="16" height="16"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>
  543. Change Password
  544. </button>
  545. </div>
  546. </form>
  547. </div>
  548. </div>
  549. ​
  550. <div class="card">
  551. <div class="card-header">
  552. <h3 class="card-title">Session Management</h3>
  553. </div>
  554. <div class="card-body">
  555. <div style="display: flex; align-items: center; justify-content: space-between; padding: 20px; background: var(--bg); border-radius: 12px; border: 1px solid var(--border);">
  556. <div>
  557. <div style="font-weight: 600; margin-bottom: 4px;">Sign Out</div>
  558. <div style="color: var(--text-muted); font-size: 0.875rem;">
  559. End your current session and return to login
  560. </div>
  561. </div>
  562. <form method="POST" action="" style="margin: 0;">
  563. <input type="hidden" name="csrf_token" value="<?= e($csrf_token) ?>">
  564. <input type="hidden" name="action" value="logout">
  565. <button type="submit" class="btn btn-secondary">
  566. <svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" width="16" height="16"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
  567. Sign Out
  568. </button>
  569. </form>
  570. </div>
  571. </div>
  572. </div>
  573. </div>
  574. </div>
  575. </main>
  576. </div>
  577. ​
  578. <script>
  579. document.addEventListener('DOMContentLoaded', function() {
  580. const messages = document.querySelectorAll('.message');
  581. messages.forEach(message => {
  582. setTimeout(() => {
  583. message.style.opacity = '0';
  584. setTimeout(() => {
  585. message.remove();
  586. }, 300);
  587. }, 5000);
  588. });
  589. });
  590. </script>
  591. </body>
  592. </html>