added session revocation and settings menu for security measures
This commit is contained in:
@ -1,5 +1,8 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../lib/settings.php';
|
||||
require_once __DIR__ . '/../lib/login_rate_limit.php';
|
||||
|
||||
switch ($route) {
|
||||
|
||||
case 'auth/login':
|
||||
@ -15,6 +18,12 @@ case 'auth/login':
|
||||
json_error('MISSING_FIELDS', 'Username and password are required', 400);
|
||||
}
|
||||
|
||||
$securitySettings = qdb_settings_get();
|
||||
[$allowed, $retryAfter] = qdb_login_rate_limit_check($username, $securitySettings);
|
||||
if (!$allowed) {
|
||||
qdb_login_rate_limit_deny($retryAfter);
|
||||
}
|
||||
|
||||
try {
|
||||
[$pdo, $tmpDb, $lockFp] = qdb_open_read_or_fail();
|
||||
$stmt = $pdo->prepare(
|
||||
@ -26,9 +35,15 @@ case 'auth/login':
|
||||
|
||||
if (!$user || !password_verify($password, $user['passwordHash'])) {
|
||||
qdb_discard($tmpDb, $lockFp);
|
||||
$retryAfter = qdb_login_rate_limit_record_failure($username, $securitySettings);
|
||||
if ($retryAfter > 0) {
|
||||
qdb_login_rate_limit_deny($retryAfter);
|
||||
}
|
||||
json_error('INVALID_CREDENTIALS', 'Invalid username or password', 401);
|
||||
}
|
||||
|
||||
qdb_login_rate_limit_clear($username);
|
||||
|
||||
$assignedClients = [];
|
||||
if (($user['role'] ?? '') === 'coach' && ($user['entityID'] ?? '') !== '') {
|
||||
$cStmt = $pdo->prepare(
|
||||
@ -47,7 +62,7 @@ case 'auth/login':
|
||||
|
||||
if ((int)$user['mustChangePassword'] === 1) {
|
||||
$tempToken = bin2hex(random_bytes(32));
|
||||
token_add($tempToken, 10 * 60, [
|
||||
token_add($tempToken, qdb_session_ttl_seconds($securitySettings, true), [
|
||||
'role' => $user['role'],
|
||||
'entityID' => $user['entityID'],
|
||||
'userID' => $user['userID'],
|
||||
@ -62,7 +77,7 @@ case 'auth/login':
|
||||
}
|
||||
|
||||
$token = bin2hex(random_bytes(32));
|
||||
token_add($token, 30 * 24 * 60 * 60, [
|
||||
token_add($token, qdb_session_ttl_seconds($securitySettings, false), [
|
||||
'role' => $user['role'],
|
||||
'entityID' => $user['entityID'],
|
||||
'userID' => $user['userID'],
|
||||
@ -149,9 +164,11 @@ case 'auth/change-password':
|
||||
|
||||
qdb_save($tmpDb, $lockFp);
|
||||
|
||||
$securitySettings = qdb_settings_get();
|
||||
|
||||
// Issue a fresh session for this browser only
|
||||
$newToken = bin2hex(random_bytes(32));
|
||||
token_add($newToken, 30 * 24 * 60 * 60, [
|
||||
token_add($newToken, qdb_session_ttl_seconds($securitySettings, false), [
|
||||
'role' => $user['role'],
|
||||
'entityID' => $user['entityID'],
|
||||
'userID' => $user['userID'],
|
||||
|
||||
71
handlers/settings.php
Normal file
71
handlers/settings.php
Normal file
@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../lib/settings.php';
|
||||
|
||||
$tokenRec = require_valid_token_web();
|
||||
require_role(['admin'], $tokenRec);
|
||||
|
||||
const QDB_REVOKE_ALL_CONFIRM = 'REVOKE ALL SESSIONS';
|
||||
|
||||
switch ($method) {
|
||||
|
||||
case 'GET':
|
||||
try {
|
||||
[$pdo, $tmpDb, $lockFp] = qdb_open_read_or_fail();
|
||||
$settings = qdb_settings_get_on_pdo($pdo);
|
||||
$sessionCount = (int)$pdo->query('SELECT COUNT(*) FROM session')->fetchColumn();
|
||||
qdb_discard($tmpDb, $lockFp);
|
||||
json_success([
|
||||
'settings' => $settings,
|
||||
'activeSessions' => $sessionCount,
|
||||
'defaults' => qdb_settings_defaults(),
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
qdb_handler_fail($e, 'Load settings', null, $tmpDb ?? null, $lockFp ?? null);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'PUT':
|
||||
case 'PATCH':
|
||||
$body = read_json_body();
|
||||
try {
|
||||
[$pdo, $tmpDb, $lockFp] = qdb_open_write_or_fail();
|
||||
$current = qdb_settings_get_on_pdo($pdo);
|
||||
$merged = qdb_settings_validate_and_merge($body, $current);
|
||||
qdb_settings_save_on_pdo($pdo, $merged);
|
||||
qdb_save($tmpDb, $lockFp);
|
||||
json_success(['settings' => $merged]);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
json_error('INVALID_FIELD', $e->getMessage(), 400);
|
||||
} catch (Throwable $e) {
|
||||
qdb_handler_fail($e, 'Save settings', null, $tmpDb ?? null, $lockFp ?? null);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'POST':
|
||||
$body = read_json_body();
|
||||
$action = trim((string)($body['action'] ?? ''));
|
||||
if ($action !== 'revokeAllSessions') {
|
||||
json_error('BAD_REQUEST', 'Unknown action', 400);
|
||||
}
|
||||
$confirm = trim((string)($body['confirmPhrase'] ?? ''));
|
||||
if ($confirm !== QDB_REVOKE_ALL_CONFIRM) {
|
||||
json_error(
|
||||
'CONFIRMATION_REQUIRED',
|
||||
'Type exactly "' . QDB_REVOKE_ALL_CONFIRM . '" to revoke every session',
|
||||
400
|
||||
);
|
||||
}
|
||||
try {
|
||||
[$pdo, $tmpDb, $lockFp] = qdb_open_write_or_fail();
|
||||
$removed = token_revoke_all_on_pdo($pdo);
|
||||
qdb_save($tmpDb, $lockFp);
|
||||
json_success(['revokedSessions' => $removed]);
|
||||
} catch (Throwable $e) {
|
||||
qdb_handler_fail($e, 'Revoke all sessions', null, $tmpDb ?? null, $lockFp ?? null);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
json_error('METHOD_NOT_ALLOWED', 'Method not allowed', 405);
|
||||
}
|
||||
@ -167,8 +167,67 @@ case 'PUT':
|
||||
case 'PATCH':
|
||||
$body = read_json_body();
|
||||
$targetUserID = trim($body['userID'] ?? '');
|
||||
$action = trim((string)($body['action'] ?? ''));
|
||||
$newPassword = (string)($body['password'] ?? $body['newPassword'] ?? '');
|
||||
|
||||
if ($action === 'revokeSessions') {
|
||||
if ($targetUserID === '') {
|
||||
json_error('MISSING_FIELDS', 'userID is required', 400);
|
||||
}
|
||||
if ($targetUserID === ($tokenRec['userID'] ?? '')) {
|
||||
json_error('FORBIDDEN', 'Use Admin settings to revoke your own other sessions, or sign out', 400);
|
||||
}
|
||||
|
||||
try {
|
||||
[$pdo, $tmpDb, $lockFp] = qdb_open_write_or_fail();
|
||||
|
||||
$row = $pdo->prepare('SELECT userID, username, role, entityID FROM users WHERE userID = :uid');
|
||||
$row->execute([':uid' => $targetUserID]);
|
||||
$target = $row->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$target) {
|
||||
qdb_discard($tmpDb, $lockFp);
|
||||
json_error('NOT_FOUND', 'User not found', 404);
|
||||
}
|
||||
|
||||
$targetRole = $target['role'] ?? '';
|
||||
if ($callerRole === 'admin') {
|
||||
if (!in_array($targetRole, ['admin', 'supervisor', 'coach'], true)) {
|
||||
qdb_discard($tmpDb, $lockFp);
|
||||
json_error('FORBIDDEN', 'Cannot revoke sessions for this user', 403);
|
||||
}
|
||||
} elseif ($callerRole === 'supervisor') {
|
||||
if ($targetRole !== 'coach') {
|
||||
qdb_discard($tmpDb, $lockFp);
|
||||
json_error('FORBIDDEN', 'Supervisors may only revoke sessions for their coaches', 403);
|
||||
}
|
||||
$chk = $pdo->prepare(
|
||||
'SELECT coachID FROM coach WHERE coachID = :cid AND supervisorID = :svid'
|
||||
);
|
||||
$chk->execute([':cid' => $target['entityID'], ':svid' => $callerEntityID]);
|
||||
if (!$chk->fetch()) {
|
||||
qdb_discard($tmpDb, $lockFp);
|
||||
json_error('FORBIDDEN', 'Coach is not under your supervision', 403);
|
||||
}
|
||||
} else {
|
||||
qdb_discard($tmpDb, $lockFp);
|
||||
json_error('FORBIDDEN', 'Not allowed to revoke sessions', 403);
|
||||
}
|
||||
|
||||
$removed = token_revoke_all_for_user_on_pdo($pdo, $targetUserID);
|
||||
qdb_save($tmpDb, $lockFp);
|
||||
|
||||
json_success([
|
||||
'userID' => $targetUserID,
|
||||
'username' => $target['username'],
|
||||
'revokedSessions' => $removed,
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
qdb_handler_fail($e, 'Revoke user sessions', null, $tmpDb ?? null, $lockFp ?? null);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if ($newPassword !== '') {
|
||||
if ($targetUserID === '') {
|
||||
json_error('MISSING_FIELDS', 'userID is required', 400);
|
||||
|
||||
Reference in New Issue
Block a user