setFailClosed(); $rateLimiter->enforce('login', 10); if ($_SERVER['REQUEST_METHOD'] !== 'POST') { errorResponse('Metoda není povolena', 405); } $input = getJsonInput(); $username = trim($input['username'] ?? ''); $password = $input['password'] ?? ''; $remember = (bool) ($input['remember'] ?? false); if (empty($username)) { errorResponse('Uživatelské jméno je povinné'); } if (empty($password)) { errorResponse('Heslo je povinné'); } try { $pdo = db(); $stmt = $pdo->prepare(' SELECT u.id, u.username, u.email, u.password_hash, u.first_name, u.last_name, u.role_id, u.failed_login_attempts, u.locked_until, u.is_active, u.totp_enabled, r.name as role_name, r.display_name as role_display_name FROM users u LEFT JOIN roles r ON u.role_id = r.id WHERE u.username = ? OR u.email = ? '); $stmt->execute([$username, $username]); $user = $stmt->fetch(); if (!$user) { AuditLog::logLoginFailed($username, 'invalid_credentials'); errorResponse('Neplatné uživatelské jméno nebo heslo', 401); } if (!$user['is_active']) { AuditLog::logLoginFailed($username, 'account_deactivated'); errorResponse('Neplatné uživatelské jméno nebo heslo', 401); } if ($user['locked_until'] && strtotime($user['locked_until']) > time()) { AuditLog::logLoginFailed($username, 'account_locked'); errorResponse('Neplatné uživatelské jméno nebo heslo', 401); } if (!password_verify($password, $user['password_hash'])) { $attempts = $user['failed_login_attempts'] + 1; $lockUntil = null; if ($attempts >= MAX_LOGIN_ATTEMPTS) { $lockUntil = date('Y-m-d H:i:s', time() + (LOCKOUT_MINUTES * 60)); $attempts = 0; // Reset after lockout } $stmt = $pdo->prepare(' UPDATE users SET failed_login_attempts = ?, locked_until = ? WHERE id = ? '); $stmt->execute([$attempts, $lockUntil, $user['id']]); AuditLog::logLoginFailed($username, 'invalid_credentials'); errorResponse('Neplatné uživatelské jméno nebo heslo', 401); } $role = ['name' => $user['role_name'], 'display_name' => $user['role_display_name']]; // 2FA - neresit failed_attempts, az po overeni if ($user['totp_enabled']) { $loginToken = bin2hex(random_bytes(32)); $hashedLoginToken = hash('sha256', $loginToken); $loginTokenExpiry = date('Y-m-d H:i:s', time() + 300); $stmt = $pdo->prepare('DELETE FROM totp_login_tokens WHERE user_id = ? OR expires_at < NOW()'); $stmt->execute([$user['id']]); $stmt = $pdo->prepare(' INSERT INTO totp_login_tokens (user_id, token_hash, expires_at) VALUES (?, ?, ?) '); $stmt->execute([$user['id'], $hashedLoginToken, $loginTokenExpiry]); successResponse([ 'requires_2fa' => true, 'login_token' => $loginToken, ]); } // Bez 2FA - reset failed attempts a pokracovat $stmt = $pdo->prepare(' UPDATE users SET failed_login_attempts = 0, locked_until = NULL, last_login = NOW() WHERE id = ? '); $stmt->execute([$user['id']]); $userData = [ 'id' => $user['id'], 'username' => $user['username'], 'email' => $user['email'], 'first_name' => $user['first_name'], 'last_name' => $user['last_name'], 'role' => $role['name'] ?? null, 'role_display' => $role['display_name'] ?? $role['name'] ?? null, 'is_admin' => ($role['name'] ?? '') === 'admin', ]; $accessToken = JWTAuth::generateAccessToken($userData); JWTAuth::generateRefreshToken($user['id'], $remember); AuditLog::logLogin($user['id'], $user['username']); $require2FA = false; try { $stmt = $pdo->query("SELECT require_2fa FROM company_settings LIMIT 1"); $require2FA = (bool) $stmt->fetchColumn(); } catch (PDOException $e) { } $permissions = JWTAuth::getUserPermissions($user['id']); successResponse([ 'access_token' => $accessToken, 'expires_in' => JWTAuth::getAccessTokenExpiry(), 'user' => [ 'id' => $userData['id'], 'username' => $userData['username'], 'email' => $userData['email'], 'full_name' => trim($userData['first_name'] . ' ' . $userData['last_name']), 'role' => $userData['role'], 'role_display' => $userData['role_display'], 'is_admin' => $userData['is_admin'], 'permissions' => $permissions, 'totp_enabled' => false, 'require_2fa' => $require2FA, ], ], 'Přihlášení úspěšné'); } catch (PDOException $e) { error_log('Login PDO error: ' . $e->getMessage()); errorResponse('Došlo k systémové chybě. Zkuste to prosím později.', 500); } catch (Exception $e) { error_log('Login error: ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine()); errorResponse('Došlo k systémové chybě. Zkuste to prosím později.', 500); }