This commit is contained in:
DIGIDEV\corer committed 2026-10-20 13:16:38 +09:00
commit 3e4a056b14
67 files changed
+13926

No files matched your search

+151
View File
@@ -0,0 +1,151 @@
<?php
declare(strict_types=1);
require __DIR__ . '/../includes/auth.php';
require __DIR__ . '/../includes/captcha.php';
send_security_headers(true);
if (current_admin()) {
redirect(base_url('admin/'));
}
if (pending_login()) {
redirect(base_url('admin/2fa.php'));
}
$setup = admin_count() === 0; // first run: create the first admin account
// The first-admin form is only usable with the setup key from config.php
// (or create the admin from the server console). Never open to the internet.
$setupKey = (string)security_config()['setup_key'];
$setupAllowed = $setup && strlen($setupKey) >= 16;
$error = null;
// Why the previous session ended (whitelisted values only).
$notices = [
'elsewhere' => 'Akun Anda masuk dari perangkat atau browser lain, sehingga sesi ini diakhiri. Masuk lagi untuk melanjutkan.',
'password' => 'Password akun ini telah diganti. Silakan masuk dengan password baru.',
'inactive' => 'Sesi diakhiri karena akun tidak aktif.',
'expired' => 'Waktu verifikasi habis. Silakan masuk lagi.',
'otp' => 'Terlalu banyak kode OTP salah. Silakan masuk lagi dalam ' . LOGIN_WINDOW_MIN . ' menit.',
];
$notice = $notices[input_str($_GET['reason'] ?? '')] ?? null;
$username = '';
$name = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
csrf_check();
$username = strtolower(trim(input_str($_POST['username'] ?? '')));
$password = input_str($_POST['password'] ?? '');
// The CAPTCHA is checked first, before any password is verified.
$captchaOk = captcha_check(input_str($_POST['captcha'] ?? ''));
if (login_locked()) {
$error = 'Terlalu banyak percobaan gagal dari jaringan ini. Coba lagi dalam ' . LOGIN_WINDOW_MIN . ' menit.';
} elseif (!$captchaOk) {
$error = 'Kode keamanan (CAPTCHA) salah atau kedaluwarsa. Ketik kode pada gambar yang baru.';
} elseif ($setup) {
$name = trim(input_str($_POST['full_name'] ?? ''));
$sentKey = input_str($_POST['setup_key'] ?? '');
if (!$setupAllowed) {
$error = 'Pembuatan admin pertama tidak diizinkan dari sini.';
} elseif (!hash_equals($setupKey, $sentKey)) {
db()->prepare('INSERT INTO login_attempts (ip, username, success) VALUES (?,?,0)')->execute([client_ip(), 'setup']);
$error = 'Kode setup salah.';
} else {
$error = validate_new_admin($username, $name, $password, input_str($_POST['password_confirm'] ?? ''));
}
if (!$error && admin_count() === 0) {
// The first admin is the super admin; the authenticator is set up next.
db()->prepare("INSERT INTO admins (username, full_name, role, password_hash) VALUES (?,?,'super_admin',?)")
->execute([$username, $name, password_hash($password, PASSWORD_DEFAULT)]);
db()->prepare("INSERT INTO audit_log (action, summary, ip) VALUES ('user.create', ?, ?)")
->execute(["Membuat akun admin pertama: $username", client_ip()]);
begin_second_factor(verify_credentials($username, $password));
redirect(base_url('admin/2fa.php'));
}
} elseif ($row = verify_credentials($username, $password)) {
begin_second_factor($row);
redirect(base_url('admin/2fa.php'));
} else {
$error = 'Username atau password salah.';
}
}
captcha_new(); // every rendered form gets a fresh challenge
$captchaQuestion = captcha_question();
?>
<!doctype html>
<html lang="id">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="color-scheme" content="light dark">
<meta name="robots" content="noindex">
<title><?= $setup ? 'Buat Akun Admin' : 'Masuk Admin' ?> · SERUBI</title>
<?= theme_boot_script() ?>
<link rel="icon" href="<?= e(asset('assets/favicon.svg')) ?>" type="image/svg+xml">
<link rel="stylesheet" href="<?= e(asset('assets/css/base.css')) ?>">
<link rel="stylesheet" href="<?= e(asset('assets/css/admin.css')) ?>">
</head>
<body class="page-login">
<main class="login">
<a class="brand" href="<?= e(base_url()) ?>" aria-label="Bank Papua SERUBI, kembali ke kalkulator"><?= bank_logo('auto', 'logo-lg') ?><span class="brand-prog">SERUBI</span></a>
<form class="login-card" method="post" novalidate>
<?= csrf_field() ?>
<h1><?= $setup ? 'Buat akun admin pertama' : 'Masuk ke admin parameter' ?></h1>
<p class="muted"><?= $setup
? 'Belum ada admin. Akun ini dapat mengubah parameter perhitungan dan menambah admin lain.'
: 'Ubah rate ALCO dan parameter perhitungan kalkulator cashback.' ?></p>
<?php if ($setup && !$setupAllowed): ?>
<p class="alert alert-info" role="status"><?= icon('info') ?><span>Belum ada admin. Buat admin pertama dari konsol server:<br><code>php database/migrate.php --create-admin</code><br>atau isi <code>security.setup_key</code> (minimal 16 karakter) di config.php.</span></p>
<?php endif; ?>
<?php if ($notice && !$error): ?>
<p class="alert alert-info" role="status"><?= icon('info') ?><span><?= e($notice) ?></span></p>
<?php endif; ?>
<?php if ($error): ?>
<p class="alert alert-error" role="alert"><?= icon('alert') ?><span><?= e($error) ?></span></p>
<?php endif; ?>
<div class="form-field">
<label for="username">Username</label>
<input id="username" name="username" autocomplete="username" required autocapitalize="none" spellcheck="false"
value="<?= e($username) ?>" <?= $username === '' ? 'autofocus' : '' ?>>
</div>
<?php if ($setupAllowed): ?>
<div class="form-field">
<label for="setup_key">Kode setup</label>
<input id="setup_key" name="setup_key" type="password" required autocomplete="off">
<p class="hint">Kode dari <code>security.setup_key</code> di config.php.</p>
</div>
<?php endif; ?>
<?php if ($setup): ?>
<div class="form-field">
<label for="full_name">Nama lengkap</label>
<input id="full_name" name="full_name" autocomplete="name" required value="<?= e($name) ?>">
</div>
<?php endif; ?>
<div class="form-field">
<label for="password">Password</label>
<input id="password" name="password" type="password" required autocomplete="<?= $setup ? 'new-password' : 'current-password' ?>" <?= $username !== '' ? 'autofocus' : '' ?>>
<?php if ($setup): ?><p class="hint">Minimal 10 karakter, berisi huruf, angka, dan simbol.</p><?php endif; ?>
</div>
<?php if ($setup): ?>
<div class="form-field">
<label for="password_confirm">Ulangi password</label>
<input id="password_confirm" name="password_confirm" type="password" required autocomplete="new-password">
</div>
<?php endif; ?>
<?php if (!$setup || $setupAllowed): ?>
<div class="form-field">
<label for="captcha">Kode keamanan</label>
<?php if ($captchaQuestion): ?>
<p class="hint"><?= e($captchaQuestion) ?></p>
<?php else: ?>
<img class="captcha-img" src="<?= e(base_url('admin/captcha.php') . '?r=' . bin2hex(random_bytes(4))) ?>" width="200" height="64" alt="Kode keamanan berupa gambar 5 karakter">
<p class="hint">Ketik 5 karakter pada gambar. <a href="<?= e(base_url('admin/login.php')) ?>">Ganti gambar</a></p>
<?php endif; ?>
<input id="captcha" name="captcha" required autocomplete="off" autocapitalize="characters" spellcheck="false" inputmode="<?= $captchaQuestion ? 'numeric' : 'text' ?>" maxlength="8">
</div>
<button class="btn btn-primary btn-block" type="submit"><?= icon($setup ? 'check' : 'lock') ?><span><?= $setup ? 'Buat akun & masuk' : 'Masuk' ?></span></button>
<?php endif; ?>
</form>
<a class="back-link" href="<?= e(base_url()) ?>"><?= icon('calc') ?><span>Kembali ke kalkulator</span></a>
</main>
</body>
</html>