WebOrbiton
v1.0.1

PriviMetrics 2

320 lines · 10.4 KB
  1. <?php
  2. ​
  3. declare(strict_types=1);
  4. require_once __DIR__ . '/functions.php';
  5. require_once __DIR__ . '/font-options.php';
  6. ​
  7. if (!envIsInstalled()) {
  8. header('Location: install.php');
  9. exit;
  10. }
  11. ​
  12. startSecureSession();
  13. pmSecurityHeaders();
  14. ​
  15. if (!empty($_SESSION['admin_logged_in']) && $_SESSION['admin_logged_in'] === true) {
  16. header('Location: dashboard.php');
  17. exit;
  18. }
  19. ​
  20. $error = '';
  21. $installed = isset($_GET['installed']);
  22. $timedOut = isset($_GET['timeout']);
  23. ​
  24. if ($_SERVER['REQUEST_METHOD'] === 'POST') {
  25. if (!verifyCSRFToken($_POST['csrf_token'] ?? null)) {
  26. $error = 'Invalid security token. Please try again.';
  27. } elseif (empty($_POST['antibot_code']) || !isset($_SESSION['antibot_answer']) || strtoupper(trim($_POST['antibot_code'])) !== $_SESSION['antibot_answer']) {
  28. $error = 'Invalid verification code. Please try again.';
  29. } elseif (!checkRateLimit('login_' . getClientIP(), 5, 900)) {
  30. $error = 'Too many login attempts. Please try again shortly.';
  31. } else {
  32. $username = trim($_POST['username'] ?? '');
  33. $password = $_POST['password'] ?? '';
  34. ​
  35. $validUsername = env('ADMIN_USERNAME', '');
  36. $validHash = env('ADMIN_PASSWORD_HASH', '');
  37. ​
  38. if ($username === $validUsername && $validHash && verifyPassword($password, $validHash)) {
  39. session_regenerate_id(true);
  40. $_SESSION['admin_logged_in'] = true;
  41. $_SESSION['admin_username'] = $username;
  42. $_SESSION['login_time'] = time();
  43. $_SESSION['created'] = time();
  44. header('Location: dashboard.php');
  45. exit;
  46. }
  47. $error = 'Invalid username or password.';
  48. }
  49. }
  50. ​
  51. $csrfToken = generateCSRFToken();
  52. $theme = env('DEFAULT_THEME', 'dark');
  53. $siteName = env('SITE_NAME', 'PriviMetrics');
  54. ?>
  55. <!DOCTYPE html>
  56. <html lang="en" data-theme="<?= sanitize($theme) ?>">
  57. ​
  58. <head>
  59. <meta charset="UTF-8">
  60. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  61. <title>Sign In - <?= sanitize($siteName) ?></title>
  62. <?= fontFaceStyleTag() ?>
  63. <script>
  64. (function() {
  65. var t = localStorage.getItem('pm_theme') || '<?= sanitize($theme) ?>';
  66. document.documentElement.setAttribute('data-theme', t);
  67. })();
  68. </script>
  69. <style>
  70. :root {
  71. --font-body: <?= fontFamilyCss('body') ?>;
  72. --font-heading: <?= fontFamilyCss('heading') ?>;
  73. --font-value: <?= fontFamilyCss('value') ?>;
  74. }
  75. ​
  76. :root[data-theme="dark"] {
  77. --bg-primary: #0a0a0a;
  78. --bg-secondary: #141414;
  79. --border-color: #262626;
  80. --text-primary: #eaeaea;
  81. --text-secondary: #9a9a9a;
  82. --accent: #f1484e;
  83. --accent-hover: #d53b40;
  84. --error-bg: #2a1414;
  85. --error-border: #5c2323;
  86. --error-text: #fca5a5;
  87. --info-bg: #0f2418;
  88. --info-border: #1e4530;
  89. --info-text: #86efac;
  90. }
  91. ​
  92. :root[data-theme="light"] {
  93. --bg-primary: #f7f7f8;
  94. --bg-secondary: #ffffff;
  95. --border-color: #e5e5e7;
  96. --text-primary: #16171a;
  97. --text-secondary: #6b6d73;
  98. --accent: #f1484e;
  99. --accent-hover: #d53b40;
  100. --error-bg: #fee2e2;
  101. --error-border: #fecaca;
  102. --error-text: #991b1b;
  103. --info-bg: #ecfdf5;
  104. --info-border: #a7f3d0;
  105. --info-text: #065f46;
  106. }
  107. ​
  108. * {
  109. margin: 0;
  110. padding: 0;
  111. box-sizing: border-box;
  112. }
  113. ​
  114. body {
  115. font-family: var(--font-body);
  116. background: radial-gradient(circle at 80% 0%, rgba(241, 72, 78, 0.08), transparent 40%), var(--bg-primary);
  117. color: var(--text-primary);
  118. min-height: 100vh;
  119. display: flex;
  120. align-items: center;
  121. justify-content: center;
  122. padding: 20px;
  123. }
  124. ​
  125. .login-container {
  126. background: linear-gradient(180deg, color-mix(in srgb, var(--bg-secondary) 94%, #fff) 0%, var(--bg-secondary) 100%);
  127. border: 1px solid var(--border-color);
  128. border-radius: 16px;
  129. box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04), 0 1px 2px rgba(0, 0, 0, 0.18), 0 12px 28px -14px rgba(0, 0, 0, 0.35);
  130. padding: 36px;
  131. max-width: 400px;
  132. width: 100%;
  133. }
  134. ​
  135. .header {
  136. display: flex;
  137. align-items: center;
  138. justify-content: space-between;
  139. margin-bottom: 24px;
  140. }
  141. ​
  142. .logo {
  143. width: 44px;
  144. height: 44px;
  145. border-radius: 10px;
  146. display: flex;
  147. align-items: center;
  148. justify-content: center;
  149. font-weight: 700;
  150. font-size: 18px;
  151. color: white;
  152. background: var(--accent);
  153. }
  154. ​
  155. .theme-toggle {
  156. width: 38px;
  157. height: 38px;
  158. border: 1px solid var(--border-color);
  159. border-radius: 8px;
  160. background: var(--bg-primary);
  161. color: var(--text-primary);
  162. display: flex;
  163. align-items: center;
  164. justify-content: center;
  165. text-decoration: none;
  166. cursor: pointer;
  167. }
  168. ​
  169. h1 {
  170. font-family: var(--font-heading);
  171. font-size: 22px;
  172. margin-bottom: 6px;
  173. }
  174. ​
  175. p.sub {
  176. color: var(--text-secondary);
  177. margin-bottom: 26px;
  178. font-size: 14px;
  179. }
  180. ​
  181. .form-group {
  182. margin-bottom: 18px;
  183. }
  184. ​
  185. .form-group img {
  186. display: block;
  187. margin-bottom: 10px;
  188. border-radius: 8px;
  189. border: 1px solid var(--border-color);
  190. max-width: 100%;
  191. height: auto;
  192. }
  193. ​
  194. label {
  195. display: block;
  196. margin-bottom: 7px;
  197. font-size: 13px;
  198. font-weight: 500;
  199. }
  200. ​
  201. input {
  202. width: 100%;
  203. padding: 12px 14px;
  204. background: var(--bg-primary);
  205. border: 1px solid var(--border-color);
  206. border-radius: 10px;
  207. color: var(--text-primary);
  208. font-size: 14px;
  209. box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.25);
  210. transition: border-color 0.2s ease, box-shadow 0.2s ease;
  211. }
  212. ​
  213. input:focus {
  214. outline: none;
  215. border-color: var(--accent);
  216. box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
  217. }
  218. ​
  219. button {
  220. width: 100%;
  221. padding: 12px 16px;
  222. background: linear-gradient(155deg, var(--accent-hover), var(--accent));
  223. color: white;
  224. border: 1px solid var(--accent);
  225. border-radius: 10px;
  226. font-size: 14px;
  227. font-weight: 600;
  228. cursor: pointer;
  229. box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.14), 0 1px 2px rgba(0, 0, 0, 0.2);
  230. transition: background 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
  231. }
  232. ​
  233. button:hover {
  234. background: linear-gradient(155deg, var(--accent-hover), var(--accent-hover));
  235. box-shadow: 0 4px 14px color-mix(in srgb, var(--accent) 40%, transparent), inset 0 1px 0 rgba(255, 255, 255, 0.14);
  236. transform: translateY(-1px);
  237. }
  238. ​
  239. button:active {
  240. transform: translateY(0) scale(0.98);
  241. }
  242. ​
  243. .error,
  244. .info {
  245. padding: 12px 16px;
  246. border-radius: 8px;
  247. font-size: 13px;
  248. margin-bottom: 20px;
  249. }
  250. ​
  251. .error {
  252. background: var(--error-bg);
  253. border: 1px solid var(--error-border);
  254. color: var(--error-text);
  255. }
  256. ​
  257. .info {
  258. background: var(--info-bg);
  259. border: 1px solid var(--info-border);
  260. color: var(--info-text);
  261. }
  262. </style>
  263. </head>
  264. ​
  265. <body>
  266. <div class="login-container">
  267. <div class="header">
  268. <div class="logo">
  269. <img src="logo.webp" alt="Logo" width="24" height="24" loading="lazy">
  270. </div>
  271. <a href="javascript:void(0)" class="theme-toggle" onclick="toggleTheme()">
  272. <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2">
  273. <circle cx="12" cy="12" r="5" />
  274. <path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42" />
  275. </svg>
  276. </a>
  277. </div>
  278. <h1>Welcome back</h1>
  279. <p class="sub">Sign in to your analytics dashboard</p>
  280. ​
  281. <?php if ($installed): ?>
  282. <div class="info">Installation completed successfully. Sign in with your credentials.</div>
  283. <?php endif; ?>
  284. <?php if ($timedOut): ?>
  285. <div class="info">Your session has expired. Please sign in again.</div>
  286. <?php endif; ?>
  287. <?php if ($error): ?>
  288. <div class="error"><?= sanitize($error) ?></div>
  289. <?php endif; ?>
  290. ​
  291. <form method="post">
  292. <input type="hidden" name="csrf_token" value="<?= sanitize($csrfToken) ?>">
  293. <div class="form-group">
  294. <label for="username">Username</label>
  295. <input type="text" id="username" name="username" required autofocus autocomplete="username">
  296. </div>
  297. <div class="form-group">
  298. <label for="password">Password</label>
  299. <input type="password" id="password" name="password" required autocomplete="current-password">
  300. </div>
  301. <div class="form-group">
  302. <label for="antibot_code">Verification Code</label>
  303. <img src="antibot.php" alt="Verification">
  304. <input type="text" id="antibot_code" name="antibot_code" required autocomplete="off" placeholder="Enter code from image">
  305. </div>
  306. <button type="submit">Sign In</button>
  307. </form>
  308. </div>
  309. <script>
  310. function toggleTheme() {
  311. const html = document.documentElement;
  312. const current = html.getAttribute('data-theme');
  313. const next = current === 'dark' ? 'light' : 'dark';
  314. html.setAttribute('data-theme', next);
  315. localStorage.setItem('pm_theme', next);
  316. }
  317. </script>
  318. </body>
  319. ​
  320. </html>