94 lines
3.5 KiB
PHP
94 lines
3.5 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
use App\Controllers\PublicController;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Admin console authentication.
|
||
|
|
*
|
||
|
|
* POST /api/adminauth/login { username, password } -> sets admin session
|
||
|
|
* POST /api/adminauth/logout -> clears it
|
||
|
|
* GET /api/adminauth/me -> { admin: bool, username }
|
||
|
|
*
|
||
|
|
* A dedicated single-admin tier, separate from community accounts and the
|
||
|
|
* bearer token. Login sets $_SESSION['admin']; every privileged console
|
||
|
|
* endpoint (built in later phases) will call requireAdmin() before doing
|
||
|
|
* anything. Rate-limited hard, and every attempt is written to cja_admin_log.
|
||
|
|
*
|
||
|
|
* Extends PublicController for origin + throttle; the admin check is bespoke
|
||
|
|
* (session flag), not the framework's token/api auth.
|
||
|
|
*/
|
||
|
|
class AdminAuth extends PublicController
|
||
|
|
{
|
||
|
|
/** Guard for privileged console endpoints. Emits 401 and stops if not admin. */
|
||
|
|
public static function requireAdmin(): void
|
||
|
|
{
|
||
|
|
if (empty($_SESSION['admin']['ok'])) {
|
||
|
|
http_response_code(401);
|
||
|
|
header('Content-Type: application/json; charset=UTF-8');
|
||
|
|
echo json_encode(['ok' => false, 'data' => null, 'error' => [
|
||
|
|
'code' => 'unauthorized', 'message' => 'Admin login required',
|
||
|
|
]]);
|
||
|
|
exit;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private function logAttempt(string $action, string $detail): void
|
||
|
|
{
|
||
|
|
try {
|
||
|
|
\Db::insert('cja_admin_log', [
|
||
|
|
'actor' => $_SESSION['admin']['username'] ?? 'anon',
|
||
|
|
'action' => $action,
|
||
|
|
'detail' => mb_substr($detail, 0, 500),
|
||
|
|
'ip' => $_SERVER['REMOTE_ADDR'] ?? null,
|
||
|
|
]);
|
||
|
|
} catch (\Throwable $e) {
|
||
|
|
// logging must never block auth
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public function login(): void
|
||
|
|
{
|
||
|
|
// 8 attempts / 5 min / IP — brute-force resistant, human-friendly.
|
||
|
|
$this->guardPublic('admin_login', 8, 300);
|
||
|
|
|
||
|
|
$in = json_decode((string) file_get_contents('php://input'), true) ?: [];
|
||
|
|
$username = trim((string) ($in['username'] ?? ''));
|
||
|
|
$password = (string) ($in['password'] ?? '');
|
||
|
|
|
||
|
|
$row = \Db::getRow('SELECT admin_id, username, password_hash FROM cja_admin WHERE username = ?', [$username]);
|
||
|
|
|
||
|
|
// Constant-ish work whether or not the user exists (avoid enumeration).
|
||
|
|
$hash = $row['password_hash'] ?? '$2y$10$invalidinvalidinvalidinvalidinvalidinvalidinvalidin';
|
||
|
|
$ok = $row && password_verify($password, $hash);
|
||
|
|
|
||
|
|
if (!$ok) {
|
||
|
|
$this->logAttempt('login_fail', "username={$username}");
|
||
|
|
$this->json(null, 401, ['code' => 'bad_credentials', 'message' => 'Incorrect username or password.']);
|
||
|
|
}
|
||
|
|
|
||
|
|
session_regenerate_id(true);
|
||
|
|
$_SESSION['admin'] = ['ok' => true, 'username' => $row['username'], 'at' => time()];
|
||
|
|
\Db::update('cja_admin', ['last_login_at' => date('Y-m-d H:i:s')], 'admin_id = ?', [$row['admin_id']]);
|
||
|
|
$this->logAttempt('login', "username={$row['username']}");
|
||
|
|
|
||
|
|
$this->json(['admin' => true, 'username' => $row['username']]);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function logout(): void
|
||
|
|
{
|
||
|
|
$this->logAttempt('logout', '');
|
||
|
|
unset($_SESSION['admin']);
|
||
|
|
session_regenerate_id(true);
|
||
|
|
$this->json(['admin' => false]);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function me(): void
|
||
|
|
{
|
||
|
|
$admin = $_SESSION['admin']['ok'] ?? false;
|
||
|
|
$this->json([
|
||
|
|
'admin' => (bool) $admin,
|
||
|
|
'username' => $admin ? ($_SESSION['admin']['username'] ?? null) : null,
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
}
|