v1.0.1
PriviMetrics 2
- <?php
-
- declare(strict_types=1);
- require_once __DIR__ . '/functions.php';
- require_once __DIR__ . '/font-options.php';
-
- if (!envIsInstalled()) {
- header('Location: install.php');
- exit;
- }
-
- startSecureSession();
- pmSecurityHeaders();
-
- if (!empty($_SESSION['admin_logged_in']) && $_SESSION['admin_logged_in'] === true) {
- header('Location: dashboard.php');
- exit;
- }
-
- $error = '';
- $installed = isset($_GET['installed']);
- $timedOut = isset($_GET['timeout']);
-
- if ($_SERVER['REQUEST_METHOD'] === 'POST') {
- if (!verifyCSRFToken($_POST['csrf_token'] ?? null)) {
- $error = 'Invalid security token. Please try again.';
- } elseif (empty($_POST['antibot_code']) || !isset($_SESSION['antibot_answer']) || strtoupper(trim($_POST['antibot_code'])) !== $_SESSION['antibot_answer']) {
- $error = 'Invalid verification code. Please try again.';
- } elseif (!checkRateLimit('login_' . getClientIP(), 5, 900)) {
- $error = 'Too many login attempts. Please try again shortly.';
- } else {
- $username = trim($_POST['username'] ?? '');
- $password = $_POST['password'] ?? '';
-
- $validUsername = env('ADMIN_USERNAME', '');
- $validHash = env('ADMIN_PASSWORD_HASH', '');
-
- if ($username === $validUsername && $validHash && verifyPassword($password, $validHash)) {
- session_regenerate_id(true);
- $_SESSION['admin_logged_in'] = true;
- $_SESSION['admin_username'] = $username;
- $_SESSION['login_time'] = time();
- $_SESSION['created'] = time();
- header('Location: dashboard.php');
- exit;
- }
- $error = 'Invalid username or password.';
- }
- }
-
- $csrfToken = generateCSRFToken();
- $theme = env('DEFAULT_THEME', 'dark');
- $siteName = env('SITE_NAME', 'PriviMetrics');
- ?>
- <!DOCTYPE html>
- <html lang="en" data-theme="<?= sanitize($theme) ?>">
-
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>Sign In - <?= sanitize($siteName) ?></title>
- <?= fontFaceStyleTag() ?>
- <script>
- (function() {
- var t = localStorage.getItem('pm_theme') || '<?= sanitize($theme) ?>';
- document.documentElement.setAttribute('data-theme', t);
- })();
- </script>
- <style>
- :root {
- --font-body: <?= fontFamilyCss('body') ?>;
- --font-heading: <?= fontFamilyCss('heading') ?>;
- --font-value: <?= fontFamilyCss('value') ?>;
- }
-
- :root[data-theme="dark"] {
- --bg-primary: #0a0a0a;
- --bg-secondary: #141414;
- --border-color: #262626;
- --text-primary: #eaeaea;
- --text-secondary: #9a9a9a;
- --accent: #f1484e;
- --accent-hover: #d53b40;
- --error-bg: #2a1414;
- --error-border: #5c2323;
- --error-text: #fca5a5;
- --info-bg: #0f2418;
- --info-border: #1e4530;
- --info-text: #86efac;
- }
-
- :root[data-theme="light"] {
- --bg-primary: #f7f7f8;
- --bg-secondary: #ffffff;
- --border-color: #e5e5e7;
- --text-primary: #16171a;
- --text-secondary: #6b6d73;
- --accent: #f1484e;
- --accent-hover: #d53b40;
- --error-bg: #fee2e2;
- --error-border: #fecaca;
- --error-text: #991b1b;
- --info-bg: #ecfdf5;
- --info-border: #a7f3d0;
- --info-text: #065f46;
- }
-
- * {
- margin: 0;
- padding: 0;
- box-sizing: border-box;
- }
-
- body {
- font-family: var(--font-body);
- background: radial-gradient(circle at 80% 0%, rgba(241, 72, 78, 0.08), transparent 40%), var(--bg-primary);
- color: var(--text-primary);
- min-height: 100vh;
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 20px;
- }
-
- .login-container {
- background: linear-gradient(180deg, color-mix(in srgb, var(--bg-secondary) 94%, #fff) 0%, var(--bg-secondary) 100%);
- border: 1px solid var(--border-color);
- border-radius: 16px;
- 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);
- padding: 36px;
- max-width: 400px;
- width: 100%;
- }
-
- .header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-bottom: 24px;
- }
-
- .logo {
- width: 44px;
- height: 44px;
- border-radius: 10px;
- display: flex;
- align-items: center;
- justify-content: center;
- font-weight: 700;
- font-size: 18px;
- color: white;
- background: var(--accent);
- }
-
- .theme-toggle {
- width: 38px;
- height: 38px;
- border: 1px solid var(--border-color);
- border-radius: 8px;
- background: var(--bg-primary);
- color: var(--text-primary);
- display: flex;
- align-items: center;
- justify-content: center;
- text-decoration: none;
- cursor: pointer;
- }
-
- h1 {
- font-family: var(--font-heading);
- font-size: 22px;
- margin-bottom: 6px;
- }
-
- p.sub {
- color: var(--text-secondary);
- margin-bottom: 26px;
- font-size: 14px;
- }
-
- .form-group {
- margin-bottom: 18px;
- }
-
- .form-group img {
- display: block;
- margin-bottom: 10px;
- border-radius: 8px;
- border: 1px solid var(--border-color);
- max-width: 100%;
- height: auto;
- }
-
- label {
- display: block;
- margin-bottom: 7px;
- font-size: 13px;
- font-weight: 500;
- }
-
- input {
- width: 100%;
- padding: 12px 14px;
- background: var(--bg-primary);
- border: 1px solid var(--border-color);
- border-radius: 10px;
- color: var(--text-primary);
- font-size: 14px;
- box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.25);
- transition: border-color 0.2s ease, box-shadow 0.2s ease;
- }
-
- input:focus {
- outline: none;
- border-color: var(--accent);
- box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
- }
-
- button {
- width: 100%;
- padding: 12px 16px;
- background: linear-gradient(155deg, var(--accent-hover), var(--accent));
- color: white;
- border: 1px solid var(--accent);
- border-radius: 10px;
- font-size: 14px;
- font-weight: 600;
- cursor: pointer;
- box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.14), 0 1px 2px rgba(0, 0, 0, 0.2);
- transition: background 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
- }
-
- button:hover {
- background: linear-gradient(155deg, var(--accent-hover), var(--accent-hover));
- box-shadow: 0 4px 14px color-mix(in srgb, var(--accent) 40%, transparent), inset 0 1px 0 rgba(255, 255, 255, 0.14);
- transform: translateY(-1px);
- }
-
- button:active {
- transform: translateY(0) scale(0.98);
- }
-
- .error,
- .info {
- padding: 12px 16px;
- border-radius: 8px;
- font-size: 13px;
- margin-bottom: 20px;
- }
-
- .error {
- background: var(--error-bg);
- border: 1px solid var(--error-border);
- color: var(--error-text);
- }
-
- .info {
- background: var(--info-bg);
- border: 1px solid var(--info-border);
- color: var(--info-text);
- }
- </style>
- </head>
-
- <body>
- <div class="login-container">
- <div class="header">
- <div class="logo">
- <img src="logo.webp" alt="Logo" width="24" height="24" loading="lazy">
- </div>
- <a href="javascript:void(0)" class="theme-toggle" onclick="toggleTheme()">
- <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2">
- <circle cx="12" cy="12" r="5" />
- <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" />
- </svg>
- </a>
- </div>
- <h1>Welcome back</h1>
- <p class="sub">Sign in to your analytics dashboard</p>
-
- <?php if ($installed): ?>
- <div class="info">Installation completed successfully. Sign in with your credentials.</div>
- <?php endif; ?>
- <?php if ($timedOut): ?>
- <div class="info">Your session has expired. Please sign in again.</div>
- <?php endif; ?>
- <?php if ($error): ?>
- <div class="error"><?= sanitize($error) ?></div>
- <?php endif; ?>
-
- <form method="post">
- <input type="hidden" name="csrf_token" value="<?= sanitize($csrfToken) ?>">
- <div class="form-group">
- <label for="username">Username</label>
- <input type="text" id="username" name="username" required autofocus autocomplete="username">
- </div>
- <div class="form-group">
- <label for="password">Password</label>
- <input type="password" id="password" name="password" required autocomplete="current-password">
- </div>
- <div class="form-group">
- <label for="antibot_code">Verification Code</label>
- <img src="antibot.php" alt="Verification">
- <input type="text" id="antibot_code" name="antibot_code" required autocomplete="off" placeholder="Enter code from image">
- </div>
- <button type="submit">Sign In</button>
- </form>
- </div>
- <script>
- function toggleTheme() {
- const html = document.documentElement;
- const current = html.getAttribute('data-theme');
- const next = current === 'dark' ? 'light' : 'dark';
- html.setAttribute('data-theme', next);
- localStorage.setItem('pm_theme', next);
- }
- </script>
- </body>
-
- </html>