mirror of
git://f0xx.org/ac/ac-deploy
synced 2026-07-29 03:38:07 +03:00
fix(cluster0): wire 2FA redirects, heartbeat session touch, TOTP reset script
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -62,4 +62,8 @@ if ($type === 'ra') {
|
|||||||
$resp['heartbeat'] = array_merge($resp['heartbeat'], $ra);
|
$resp['heartbeat'] = array_merge($resp['heartbeat'], $ra);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Auth::user()) {
|
||||||
|
Auth::touchSession();
|
||||||
|
}
|
||||||
|
|
||||||
echo json_encode($resp, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
echo json_encode($resp, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||||
|
|||||||
@@ -132,6 +132,11 @@ if ($route === '/api/rbac.php' || str_ends_with($route, '/api/rbac.php')) {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($route === '/api/media_pipelines.php' || str_ends_with($route, '/api/media_pipelines.php')) {
|
||||||
|
require __DIR__ . '/api/media_pipelines.php';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
if ($route === '/api/ticket_attachment.php' || str_ends_with($route, '/api/ticket_attachment.php')) {
|
if ($route === '/api/ticket_attachment.php' || str_ends_with($route, '/api/ticket_attachment.php')) {
|
||||||
require __DIR__ . '/api/ticket_attachment.php';
|
require __DIR__ . '/api/ticket_attachment.php';
|
||||||
exit;
|
exit;
|
||||||
@@ -159,11 +164,12 @@ if ($route === '/logout') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($route === '/login' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($route === '/login' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
Auth::captureRedirectFromRequest();
|
||||||
$user = trim($_POST['username'] ?? '');
|
$user = trim($_POST['username'] ?? '');
|
||||||
$pass = $_POST['password'] ?? '';
|
$pass = $_POST['password'] ?? '';
|
||||||
$result = Auth::login($user, $pass);
|
$result = Auth::login($user, $pass);
|
||||||
if ($result === true) {
|
if ($result === true) {
|
||||||
header('Location: ' . $base . '/');
|
header('Location: ' . Auth::postLoginRedirect($base . '/'));
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
if ($result === 'pending_2fa') {
|
if ($result === 'pending_2fa') {
|
||||||
@@ -184,10 +190,12 @@ if ($route === '/login' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($route === '/login') {
|
if ($route === '/login') {
|
||||||
|
Auth::captureRedirectFromRequest();
|
||||||
if (Auth::pending2faUserId() > 0) {
|
if (Auth::pending2faUserId() > 0) {
|
||||||
header('Location: ' . Auth::authUrl('/two-factor'));
|
header('Location: ' . Auth::authUrl('/two-factor'));
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
$loginRedirect = Auth::peekPostLoginRedirect('');
|
||||||
require __DIR__ . '/../views/login.php';
|
require __DIR__ . '/../views/login.php';
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
@@ -242,7 +250,7 @@ if ($route === '/two-factor' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
}
|
}
|
||||||
$code = trim($_POST['code'] ?? '');
|
$code = trim($_POST['code'] ?? '');
|
||||||
if (Auth::completeTotpLogin($code)) {
|
if (Auth::completeTotpLogin($code)) {
|
||||||
header('Location: ' . $base . '/');
|
header('Location: ' . Auth::postLoginRedirect($base . '/'));
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
$twofaError = 'Invalid code';
|
$twofaError = 'Invalid code';
|
||||||
@@ -278,9 +286,11 @@ if (
|
|||||||
$_SESSION['pending_2fa_exp'] = time() + 300;
|
$_SESSION['pending_2fa_exp'] = time() + 300;
|
||||||
$status = AuthApproval::pollStatus($rawToken);
|
$status = AuthApproval::pollStatus($rawToken);
|
||||||
if ($status === 'approved') {
|
if ($status === 'approved') {
|
||||||
// Attempt to complete the login server-side within this same request
|
|
||||||
if (Auth::completeTotpLoginByApproval($rawToken)) {
|
if (Auth::completeTotpLoginByApproval($rawToken)) {
|
||||||
echo json_encode(['status' => 'approved', 'redirect' => $base . '/']);
|
echo json_encode([
|
||||||
|
'status' => 'approved',
|
||||||
|
'redirect' => Auth::postLoginRedirect($base . '/'),
|
||||||
|
]);
|
||||||
} else {
|
} else {
|
||||||
echo json_encode(['status' => 'expired']);
|
echo json_encode(['status' => 'expired']);
|
||||||
}
|
}
|
||||||
@@ -292,13 +302,19 @@ if (
|
|||||||
|
|
||||||
// Approval confirmation page shown to the already-logged-in mobile user.
|
// Approval confirmation page shown to the already-logged-in mobile user.
|
||||||
if ($route === '/two-factor/approve' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($route === '/two-factor/approve' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$rawToken = trim($_POST['token'] ?? '');
|
||||||
|
$totpCode = trim($_POST['code'] ?? '');
|
||||||
$mobileUser = Auth::user();
|
$mobileUser = Auth::user();
|
||||||
if (!$mobileUser) {
|
$approved = false;
|
||||||
header('Location: ' . Auth::authUrl('/login') . '?redirect=' . urlencode(Auth::authUrl('/two-factor/approve') . '?token=' . urlencode($_POST['token'] ?? '')));
|
if ($mobileUser && $rawToken !== '') {
|
||||||
|
$approved = AuthApproval::approve($rawToken, (int) ($mobileUser['id'] ?? 0), Auth::clientIp());
|
||||||
|
} elseif ($rawToken !== '' && $totpCode !== '') {
|
||||||
|
$approved = AuthApproval::approveWithTotp($rawToken, $totpCode, Auth::clientIp());
|
||||||
|
} else {
|
||||||
|
$returnUrl = Auth::authUrl('/two-factor/approve') . '?token=' . urlencode($rawToken);
|
||||||
|
header('Location: ' . Auth::authUrl('/login') . '?redirect=' . urlencode($returnUrl));
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
$rawToken = trim($_POST['token'] ?? '');
|
|
||||||
$approved = $rawToken !== '' && AuthApproval::approve($rawToken, (int) ($mobileUser['id'] ?? 0), Auth::clientIp());
|
|
||||||
$approveResult = $approved ? 'ok' : 'error';
|
$approveResult = $approved ? 'ok' : 'error';
|
||||||
require __DIR__ . '/../views/two_factor_approve.php';
|
require __DIR__ . '/../views/two_factor_approve.php';
|
||||||
exit;
|
exit;
|
||||||
@@ -398,6 +414,15 @@ if ($view === 'short_links' && !Rbac::can('short_links_view')) {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($view === 'media_pipelines') {
|
||||||
|
require_once __DIR__ . '/../src/MediaPipelinesRepository.php';
|
||||||
|
if (!MediaPipelinesRepository::gateAllowed()) {
|
||||||
|
http_response_code(403);
|
||||||
|
echo 'Forbidden';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ($view === 'report' && isset($_GET['id'])) {
|
if ($view === 'report' && isset($_GET['id'])) {
|
||||||
try {
|
try {
|
||||||
$report = ReportRepository::getById((int) $_GET['id']);
|
$report = ReportRepository::getById((int) $_GET['id']);
|
||||||
@@ -455,6 +480,7 @@ $pageTitle = match ($view) {
|
|||||||
'live_sessions' => 'Live sessions',
|
'live_sessions' => 'Live sessions',
|
||||||
'remote_access' => 'Remote access',
|
'remote_access' => 'Remote access',
|
||||||
'short_links' => 'Short links',
|
'short_links' => 'Short links',
|
||||||
|
'media_pipelines' => 'Media pipelines',
|
||||||
'rbac' => 'Access control',
|
'rbac' => 'Access control',
|
||||||
'reports', 'report' => 'Issues',
|
'reports', 'report' => 'Issues',
|
||||||
default => 'Console',
|
default => 'Console',
|
||||||
|
|||||||
38
sim/cluster0/lab-seeds/backend/scripts/reset_user_totp.php
Normal file
38
sim/cluster0/lab-seeds/backend/scripts/reset_user_totp.php
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Re-enroll TOTP for a user (lab / key rotation). Prints otpauth URI for Google Authenticator.
|
||||||
|
*
|
||||||
|
* sudo php83 scripts/reset_user_totp.php admin
|
||||||
|
*/
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../src/bootstrap.php';
|
||||||
|
|
||||||
|
$username = trim($argv[1] ?? '');
|
||||||
|
if ($username === '') {
|
||||||
|
fwrite(STDERR, "Usage: reset_user_totp.php <username>\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$st = Database::pdo()->prepare('SELECT id, username, email_normalized FROM users WHERE username = ? LIMIT 1');
|
||||||
|
$st->execute([$username]);
|
||||||
|
$row = $st->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if (!$row) {
|
||||||
|
fwrite(STDERR, "User not found: {$username}\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$uid = (int) $row['id'];
|
||||||
|
$secret = AuthTotp::generateSecret();
|
||||||
|
AuthFactors::enrollTotp($uid, $secret);
|
||||||
|
|
||||||
|
$account = (string) ($row['email_normalized'] ?: $row['username']);
|
||||||
|
$issuer = (string) cfg('app_name', 'AndroidCast');
|
||||||
|
$uri = AuthTotp::provisioningUri($secret, $account, $issuer);
|
||||||
|
$qr = AuthTotp::qrImageUrl($uri);
|
||||||
|
|
||||||
|
echo "user={$username} id={$uid}\n";
|
||||||
|
echo "otpauth_uri={$uri}\n";
|
||||||
|
echo "qr_png={$qr}\n";
|
||||||
|
echo "Scan the QR URL in a browser or add manually in Google Authenticator.\n";
|
||||||
@@ -119,7 +119,7 @@ function resolve_console_route(string $uri): string {
|
|||||||
if ($projectRoot !== '' && str_starts_with($uri, $projectRoot)) {
|
if ($projectRoot !== '' && str_starts_with($uri, $projectRoot)) {
|
||||||
$suffix = substr($uri, strlen($projectRoot)) ?: '/';
|
$suffix = substr($uri, strlen($projectRoot)) ?: '/';
|
||||||
$candidate = rtrim(strtok($suffix, '?') ?: '/', '/') ?: '/';
|
$candidate = rtrim(strtok($suffix, '?') ?: '/', '/') ?: '/';
|
||||||
if (preg_match('#^/(login|logout|register|two-factor|verify-email)(?:/|$)#', $candidate)) {
|
if (preg_match('#^/(login|logout|register|verify-email|account-security|two-factor(?:/approve)?|api/two-factor(?:/poll)?)(/|$)#', $candidate)) {
|
||||||
return $candidate;
|
return $candidate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user