190 lines
6.4 KiB
PHP
190 lines
6.4 KiB
PHP
<?php
|
|
|
|
require_once __DIR__ . '/../lib/settings.php';
|
|
require_once __DIR__ . '/../lib/login_rate_limit.php';
|
|
|
|
switch ($route) {
|
|
|
|
case 'auth/login':
|
|
if ($method !== 'POST') {
|
|
json_error('METHOD_NOT_ALLOWED', 'Method not allowed', 405);
|
|
}
|
|
|
|
$body = read_json_body();
|
|
$username = trim($body['username'] ?? '');
|
|
$password = (string)($body['password'] ?? '');
|
|
|
|
if ($username === '' || $password === '') {
|
|
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(
|
|
"SELECT userID, passwordHash, role, entityID, mustChangePassword
|
|
FROM users WHERE username = :u"
|
|
);
|
|
$stmt->execute([':u' => $username]);
|
|
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
|
|
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(
|
|
"SELECT clientCode FROM client WHERE coachID = :cid ORDER BY clientCode"
|
|
);
|
|
$cStmt->execute([':cid' => $user['entityID']]);
|
|
$assignedClients = array_map(
|
|
fn($code) => ['clientCode' => $code],
|
|
$cStmt->fetchAll(PDO::FETCH_COLUMN)
|
|
);
|
|
}
|
|
|
|
qdb_discard($tmpDb, $lockFp);
|
|
|
|
qdb_reject_coach_web_login((string)($user['role'] ?? ''));
|
|
|
|
if ((int)$user['mustChangePassword'] === 1) {
|
|
$tempToken = bin2hex(random_bytes(32));
|
|
token_add($tempToken, qdb_session_ttl_seconds($securitySettings, true), [
|
|
'role' => $user['role'],
|
|
'entityID' => $user['entityID'],
|
|
'userID' => $user['userID'],
|
|
'temp' => true,
|
|
]);
|
|
json_success([
|
|
'mustChangePassword' => true,
|
|
'token' => $tempToken,
|
|
'user' => $username,
|
|
'role' => $user['role'],
|
|
]);
|
|
}
|
|
|
|
$token = bin2hex(random_bytes(32));
|
|
token_add($token, qdb_session_ttl_seconds($securitySettings, false), [
|
|
'role' => $user['role'],
|
|
'entityID' => $user['entityID'],
|
|
'userID' => $user['userID'],
|
|
]);
|
|
$loginData = [
|
|
'token' => $token,
|
|
'user' => $username,
|
|
'role' => $user['role'],
|
|
];
|
|
if ($assignedClients !== []) {
|
|
$loginData['clientsPayload'] = qdb_sensitive_envelope(
|
|
json_encode(['clients' => $assignedClients], JSON_UNESCAPED_UNICODE),
|
|
$token
|
|
);
|
|
}
|
|
json_success($loginData);
|
|
} catch (Throwable $e) {
|
|
qdb_handler_fail($e, 'auth', null, $tmpDb ?? null, $lockFp ?? null);
|
|
}
|
|
break;
|
|
|
|
case 'auth/change-password':
|
|
if ($method !== 'POST') {
|
|
json_error('METHOD_NOT_ALLOWED', 'Method not allowed', 405);
|
|
}
|
|
|
|
$bearerToken = get_bearer_token();
|
|
if (!$bearerToken) {
|
|
json_error('UNAUTHORIZED', 'Bearer token required', 401);
|
|
}
|
|
$tokenRec = token_get_record($bearerToken);
|
|
if (!$tokenRec) {
|
|
json_error('FORBIDDEN', 'Invalid or expired token', 403);
|
|
}
|
|
|
|
$body = read_json_body();
|
|
$username = trim($body['username'] ?? '');
|
|
$oldPassword = (string)($body['old_password'] ?? '');
|
|
$newPassword = (string)($body['new_password'] ?? '');
|
|
|
|
if ($username === '' || $oldPassword === '' || $newPassword === '') {
|
|
json_error('MISSING_FIELDS', 'username, old_password, and new_password are required', 400);
|
|
}
|
|
if (strlen($newPassword) < 6) {
|
|
json_error('PASSWORD_TOO_SHORT', 'New password must be at least 6 characters', 400);
|
|
}
|
|
|
|
if (($tokenRec['userID'] ?? '') === '') {
|
|
json_error('FORBIDDEN', 'Token not associated with a user', 403);
|
|
}
|
|
|
|
try {
|
|
[$pdo, $tmpDb, $lockFp] = qdb_open_write_or_fail();
|
|
|
|
$stmt = $pdo->prepare(
|
|
"SELECT userID, passwordHash, role, entityID FROM users WHERE username = :u"
|
|
);
|
|
$stmt->execute([':u' => $username]);
|
|
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if (!$user) {
|
|
qdb_discard($tmpDb, $lockFp);
|
|
json_error('INVALID_CREDENTIALS', 'Invalid credentials', 401);
|
|
}
|
|
|
|
if ($user['userID'] !== $tokenRec['userID']) {
|
|
qdb_discard($tmpDb, $lockFp);
|
|
json_error('FORBIDDEN', 'Token does not match user', 403);
|
|
}
|
|
|
|
qdb_reject_coach_web_login((string)($user['role'] ?? ''));
|
|
|
|
if (!password_verify($oldPassword, $user['passwordHash'])) {
|
|
qdb_discard($tmpDb, $lockFp);
|
|
json_error('INVALID_CREDENTIALS', 'Old password incorrect', 401);
|
|
}
|
|
|
|
$newHash = password_hash($newPassword, PASSWORD_DEFAULT);
|
|
$pdo->prepare(
|
|
"UPDATE users SET passwordHash = :h, mustChangePassword = 0 WHERE userID = :uid"
|
|
)->execute([':h' => $newHash, ':uid' => $user['userID']]);
|
|
|
|
token_revoke_all_for_user_on_pdo($pdo, $user['userID']);
|
|
|
|
qdb_save($tmpDb, $lockFp);
|
|
|
|
$securitySettings = qdb_settings_get();
|
|
|
|
// Issue a fresh session for this browser only
|
|
$newToken = bin2hex(random_bytes(32));
|
|
token_add($newToken, qdb_session_ttl_seconds($securitySettings, false), [
|
|
'role' => $user['role'],
|
|
'entityID' => $user['entityID'],
|
|
'userID' => $user['userID'],
|
|
]);
|
|
|
|
json_success([
|
|
'token' => $newToken,
|
|
'user' => $username,
|
|
'role' => $user['role'],
|
|
]);
|
|
} catch (Throwable $e) {
|
|
qdb_handler_fail($e, 'auth', null, $tmpDb ?? null, $lockFp ?? null);
|
|
}
|
|
break;
|
|
|
|
default:
|
|
json_error('NOT_FOUND', 'Unknown auth route', 404);
|
|
}
|