171 lines
7.3 KiB
PHP
171 lines
7.3 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
require __DIR__ . '/../../includes/bootstrap.php';
|
|
|
|
// already logged in? go dashboard
|
|
if (admin_is_logged_in()) {
|
|
header('Location: ' . url_path('/public/admin/dashboard.php'));
|
|
exit;
|
|
}
|
|
|
|
$err = '';
|
|
$remember = false;
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
$csrf = (string)($_POST['csrf'] ?? '');
|
|
$user = trim((string)($_POST['username'] ?? ''));
|
|
$pass = (string)($_POST['password'] ?? '');
|
|
$remember = isset($_POST['remember']) && (string)$_POST['remember'] !== '';
|
|
|
|
if (!csrf_check($csrf)) {
|
|
$err = 'Invalid session. Refresh and try again.';
|
|
} elseif ($user === '' || strlen($user) > 50) {
|
|
$err = 'Invalid username.';
|
|
} elseif ($pass === '') {
|
|
$err = 'Password is required.';
|
|
} else {
|
|
// Prepared statement => SQLi safe
|
|
$st = pdo()->prepare("SELECT id, username, pass_hash FROM admin_users WHERE username = ? LIMIT 1");
|
|
$st->execute([$user]);
|
|
$a = $st->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if (!$a || empty($a['pass_hash']) || !password_verify($pass, (string)$a['pass_hash'])) {
|
|
$err = 'Wrong username or password.';
|
|
} else {
|
|
$algo = defined('PASSWORD_ARGON2ID') ? PASSWORD_ARGON2ID : PASSWORD_DEFAULT;
|
|
$opts = ['memory_cost' => 1 << 17, 'time_cost' => 4, 'threads' => 2];
|
|
if (password_needs_rehash((string)$a['pass_hash'], $algo, $opts)) {
|
|
try {
|
|
$newHash = password_hash($pass, $algo, $opts);
|
|
$up = pdo()->prepare("UPDATE admin_users SET pass_hash=? WHERE id=?");
|
|
$up->execute([$newHash, (int)$a['id']]);
|
|
} catch (Throwable $e) {
|
|
// best-effort; continue login
|
|
}
|
|
}
|
|
|
|
admin_login($a, $remember);
|
|
header('Location: ' . url_path('/public/admin/dashboard.php'));
|
|
exit;
|
|
}
|
|
}
|
|
}
|
|
?>
|
|
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>Admin Login</title>
|
|
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<link href="https://cdn.jsdelivr.net/npm/remixicon@4.6.0/fonts/remixicon.css" rel="stylesheet">
|
|
|
|
<style>
|
|
.bg-aurora{position:fixed;inset:0;background:
|
|
radial-gradient(1000px 500px at 20% 20%, rgba(139,92,246,.30), transparent 60%),
|
|
radial-gradient(900px 600px at 80% 30%, rgba(217,70,239,.22), transparent 60%),
|
|
radial-gradient(900px 700px at 55% 90%, rgba(167,139,250,.18), transparent 60%);z-index:-2;}
|
|
.bg-grid{position:fixed;inset:0;background-image:
|
|
linear-gradient(rgba(255,255,255,.05) 1px, transparent 1px),
|
|
linear-gradient(90deg, rgba(255,255,255,.05) 1px, transparent 1px);
|
|
background-size:48px 48px;opacity:.12;mask-image:radial-gradient(circle at 50% 20%, #000 0%, transparent 65%);z-index:-1;}
|
|
.glass{background:rgba(16,14,26,.62);border:1px solid rgba(255,255,255,.10);
|
|
backdrop-filter:blur(18px) saturate(150%);box-shadow:0 12px 50px rgba(0,0,0,.45);}
|
|
.btn-glow{box-shadow:0 0 0 1px rgba(167,139,250,.25), 0 18px 40px rgba(139,92,246,.25);}
|
|
</style>
|
|
</head>
|
|
|
|
<body class="min-h-screen bg-black text-white flex items-center justify-center px-4">
|
|
<div class="bg-aurora"></div>
|
|
<div class="bg-grid"></div>
|
|
|
|
<div class="w-full max-w-md">
|
|
<div class="mb-5 flex items-center justify-between">
|
|
<div class="flex items-center gap-3">
|
|
<div class="h-12 w-12 grid place-items-center rounded-2xl bg-gradient-to-br from-violet-500 to-fuchsia-500 font-black text-black">GM</div>
|
|
<div>
|
|
<div class="text-2xl font-extrabold bg-gradient-to-r from-violet-300 via-fuchsia-400 to-violet-400 bg-clip-text text-transparent">
|
|
Welcome back
|
|
</div>
|
|
<div class="text-sm text-white/60">Login to manage your portfolio</div>
|
|
</div>
|
|
</div>
|
|
<div class="hidden sm:flex items-center gap-2 rounded-full border border-white/10 bg-white/5 px-3 py-2 text-xs text-white/80">
|
|
<i class="ri-shield-keyhole-line text-fuchsia-300"></i> Secure
|
|
</div>
|
|
</div>
|
|
|
|
<div class="glass rounded-3xl p-6 relative overflow-hidden">
|
|
<div class="pointer-events-none absolute inset-0 rounded-3xl ring-1 ring-white/10"></div>
|
|
|
|
<?php if ($err): ?>
|
|
<div class="mb-4 rounded-2xl border border-red-400/20 bg-red-500/10 px-4 py-3 text-sm">
|
|
<div class="flex items-start gap-2">
|
|
<i class="ri-error-warning-line mt-0.5 text-red-300"></i>
|
|
<div><?= htmlspecialchars($err) ?></div>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<form method="post" autocomplete="off" class="space-y-4">
|
|
<input type="hidden" name="csrf" value="<?= htmlspecialchars(csrf_token()) ?>">
|
|
|
|
<div>
|
|
<label class="mb-2 block text-sm text-white/70">Username</label>
|
|
<div class="relative">
|
|
<input name="username" maxlength="50" required
|
|
value="<?= htmlspecialchars((string)($_POST['username'] ?? '')) ?>"
|
|
placeholder="admin"
|
|
class="w-full rounded-2xl border border-white/10 bg-white/5 px-4 py-3 pr-11 text-white placeholder:text-white/35 outline-none
|
|
focus:border-violet-400/50 focus:ring-4 focus:ring-violet-500/20">
|
|
<i class="ri-user-3-line absolute right-4 top-1/2 -translate-y-1/2 text-white/45"></i>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="mb-2 block text-sm text-white/70">Password</label>
|
|
<div class="relative">
|
|
<input id="pw" type="password" name="password" required placeholder="Your password"
|
|
class="w-full rounded-2xl border border-white/10 bg-white/5 px-4 py-3 pr-20 text-white placeholder:text-white/35 outline-none
|
|
focus:border-violet-400/50 focus:ring-4 focus:ring-violet-500/20">
|
|
<button type="button" id="togglePw"
|
|
class="absolute right-3 top-1/2 -translate-y-1/2 rounded-xl border border-white/10 bg-white/5 px-3 py-2 text-xs text-white/80
|
|
hover:bg-white/10 active:scale-95 transition">
|
|
<span id="eyeIcon"><i class="ri-eye-line"></i></span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center justify-between text-sm text-white/80 pt-1">
|
|
<label class="inline-flex items-center gap-2 cursor-pointer select-none">
|
|
<input type="checkbox" name="remember" value="1" <?= $remember ? 'checked' : '' ?>
|
|
class="h-4 w-4 rounded border-white/20 bg-white/5 text-violet-400 focus:ring-violet-500/40">
|
|
<span>Remember me for 120 days</span>
|
|
</label>
|
|
<span class="text-white/50 text-xs">Secure cookie</span>
|
|
</div>
|
|
|
|
<button class="w-full rounded-2xl bg-gradient-to-br from-violet-500 to-fuchsia-500 py-3 font-extrabold text-black btn-glow
|
|
hover:brightness-110 active:scale-[0.99] transition">
|
|
<i class="ri-login-circle-line"></i> Login
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
|
<script>
|
|
$(function(){
|
|
$('#togglePw').on('click', function(){
|
|
const $pw = $('#pw');
|
|
const isPwd = $pw.attr('type') === 'password';
|
|
$pw.attr('type', isPwd ? 'text' : 'password');
|
|
$('#eyeIcon').html(isPwd ? '<i class="ri-eye-off-line"></i>' : '<i class="ri-eye-line"></i>');
|
|
});
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|