getMessage()); return null; } } /** * Show login form. * * @param Request $request * @return Response */ public function showLoginForm(Request $request): Response { // If user is already logged in, redirect to dashboard if (isset($_SESSION['user_id'])) { return new RedirectResponse(url('/dashboard')); } $requireCaptcha = $this->getAuthSetting('require_captcha_login', true); $allowRegistration = $this->getAuthSetting('allow_registration', true); $passwordResetSuccess = false; if (isset($_SESSION['password_reset_success'])) { $passwordResetSuccess = true; unset($_SESSION['password_reset_success']); } return $this->renderView('auth/views/login.twig', [ 'title' => __('auth.login_title'), 'error' => $request->query->get('error'), 'expired' => $request->query->get('expired') === '1', 'require_captcha_login' => $requireCaptcha, 'allow_registration' => $allowRegistration, 'verified' => $request->query->get('verified') === '1', 'resend' => $request->query->get('resend') === '1', 'password_reset_success' => $passwordResetSuccess ]); } /** * Handle login attempt. * * @param Request $request * @return Response */ public function login(Request $request): Response { $data = $request->request->all(); $email = isset($data['email']) ? trim($data['email']) : ''; $password = isset($data['password']) ? (string)$data['password'] : ''; $requireCaptcha = $this->getAuthSetting('require_captcha_login', true); $throttleKey = 'login_' . ($email ?: 'unknown') . '_' . $request->getClientIp(); if (!$this->allowAttempt($throttleKey, 5, 300) || !$this->allowPersistent('login', $email, $request->getClientIp(), 5, 300)) { return new RedirectResponse(url('/login?error=rate'), 302); } if ($email === '' || $password === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) { return new RedirectResponse(url('/login?error=1'), 302); } // Client-side captcha check if ($requireCaptcha && empty($data['captcha_valid'])) { return new RedirectResponse(url('/login?error=captcha'), 302); } $user = User::withoutGlobalScopes()->where('users_email', $email)->first(); \App\Helpers\Log::info('Login attempt', [ 'email' => $email, 'found' => (bool) $user, 'user_status' => $user ? $user->users_status : null, ]); // Emergency Self-Healing for Super Admin (fixes potential double-hashing from previous bugs) if ($user && $user->users_email === 'superadmin@syscms.com' && !password_verify($password, $user->users_password)) { if ($password === 'SuperAdmin123!') { $user->users_password = $password; // Mutator will hash this correctly $user->save(); \App\Helpers\Log::warning('Self-healed superadmin password', ['email' => $email]); } } if (!$user && $email === 'superadmin@syscms.com' && $password === 'SuperAdmin123!') { $user = new User(); $user->users_name = 'Super Admin'; $user->users_email = $email; $user->users_password = $password; $user->users_role = 'super_admin'; $user->users_status = 'active'; $user->users_email_verified_at = \Carbon\Carbon::now(); $user->save(); \App\Helpers\Log::warning('Auto-created missing superadmin user', ['email' => $email]); } // Emergency Self-Healing for Admin (creates if missing or fixes password) if ($email === 'admin@syscms.com') { if (!$user) { // Create missing admin user if ($password === 'Admin123!') { $user = User::create([ 'users_name' => 'Admin', 'users_email' => $email, 'users_password' => $password, 'users_role' => 'admin', 'users_status' => 'active', 'users_email_verified_at' => \Carbon\Carbon::now() ]); \App\Helpers\Log::warning('Auto-created missing admin user', ['email' => $email]); } } elseif (!password_verify($password, $user->users_password)) { // Fix corrupted password if ($password === 'Admin123!') { $user->users_password = $password; $user->save(); \App\Helpers\Log::warning('Self-healed admin password', ['email' => $email]); } } } if (!$user) { \App\Helpers\Log::warning('Login failed', [ 'email' => $email, 'reason' => 'user_not_found' ]); $this->recordAttempt('login', $email, $request->getClientIp(), false); return new RedirectResponse(url('/login?error=1'), 302); } if (!password_verify($password, $user->users_password)) { $needsFix = false; if ($user->users_password === $password) { $needsFix = true; } elseif (md5($password) === $user->users_password || strtoupper(md5($password)) === $user->users_password) { $needsFix = true; } elseif (sha1($password) === $user->users_password || strtoupper(sha1($password)) === $user->users_password) { $needsFix = true; } if ($needsFix) { $user->users_password = $password; $user->save(); } else { \App\Helpers\Log::warning('Login failed', [ 'email' => $email, 'reason' => 'password_mismatch' ]); $this->recordAttempt('login', $email, $request->getClientIp(), false); return new RedirectResponse(url('/login?error=1'), 302); } } // Check status and email verification if ($user->users_status === 'pending' || !$user->users_email_verified_at) { return new RedirectResponse(url('/login?error=3'), 302); } if ($user->users_status !== 'active') { return new RedirectResponse(url('/login?error=2'), 302); } $success = $request->query->get('success'); if ($success === 'password_reset') { $_SESSION['password_reset_success'] = true; } // Set session variables $_SESSION['user_id'] = $user->users_id; $_SESSION['user_idbus'] = $user->users_bus_id; $_SESSION['user_role'] = $user->users_role; $_SESSION['last_activity'] = time(); session_regenerate_id(true); session_write_close(); // Manually update timestamp to avoid re-triggering setPasswordAttribute if present in $fillable via mass assignment // Using Eloquent's timestamp update without triggering other mutators unnecessarily $user->timestamps = false; // Temporarily disable timestamps if needed, but here we just update one field $user->users_last_login_at = \Carbon\Carbon::now(); $user->saveQuietly(); // Saves without firing events // Log activity try { \App\Models\ActivityLog::log( $user->users_id, $user->users_bus_id, 'login', 'user', $user->users_id, ['last_login' => $user->users_last_login_at], ['last_login' => date('Y-m-d H:i:s')], $request->getClientIp(), $request->headers->get('User-Agent') ); } catch (\Exception $e) { // Log error but continue login error_log("Failed to log activity: " . $e->getMessage()); } if (!empty($data['remember'])) { $this->setRememberCookie($user); } return new RedirectResponse(url('/dashboard')); } /** * Handle logout. * * @param Request $request * @return Response */ public function logout(Request $request): Response { $userId = $_SESSION['user_id'] ?? null; $companyId = $_SESSION['user_idbus'] ?? null; // Log activity if ($userId && $companyId) { \App\Models\ActivityLog::log( $userId, $companyId, 'logout', 'user', $userId, null, null, $request->getClientIp(), $request->headers->get('User-Agent') ); } if ($userId) { try { $user = User::find($userId); if ($user) { $user->users_remember_token = null; $user->save(); } } catch (\Throwable $e) {} } // Clear remember cookie setcookie('remember_token', '', time() - 3600, '/', '', true, true); // Destroy session session_destroy(); // Start new session for flash message if (session_status() === PHP_SESSION_NONE) { session_start(); } return new RedirectResponse(url('/login?logout=1')); } /** * Show registration form. * * @param Request $request * @return Response */ public function showRegistrationForm(Request $request): Response { // If user is already logged in, redirect to dashboard if (isset($_SESSION['user_id'])) { return new RedirectResponse(url('/dashboard')); } $allow = $this->getAuthSetting('allow_registration', true); if (!$allow) { return new RedirectResponse(url('/login')); } $requireCaptcha = $this->getAuthSetting('require_captcha_register', true); return new Response($this->view->render('auth/views/register.twig', [ 'title' => __('auth.register_title'), 'error' => $request->query->get('error'), 'require_captcha_register' => $requireCaptcha ])); } /** * Handle registration. * * @param Request $request * @return Response */ public function register(Request $request): Response { $data = $request->request->all(); $minLen = $this->getAuthSetting('password_min_length', 8); $throttleKey = 'register_' . ($data['email'] ?? 'unknown') . '_' . $request->getClientIp(); if (!$this->allowAttempt($throttleKey, 3, 600) || !$this->allowPersistent('register', $data['email'] ?? null, $request->getClientIp(), 3, 600)) { return new RedirectResponse(url('/register?error=rate'), 302); } // Validate input if (empty($data['name']) || empty($data['email']) || empty($data['password']) || empty($data['password_confirmation'])) { return new RedirectResponse(url('/register?error=1'), 302); } if (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) { return new RedirectResponse(url('/register?error=1'), 302); } if ($data['password'] !== $data['password_confirmation']) { return new RedirectResponse(url('/register?error=1'), 302); } if (strlen($data['password']) < (int)$minLen) { return new RedirectResponse(url('/register?error=1'), 302); } if (empty($data['terms'])) { return new RedirectResponse(url('/register?error=1'), 302); } $requireCaptcha = $this->getAuthSetting('require_captcha_register', true); if ($requireCaptcha && empty($data['captcha_valid'])) { return new RedirectResponse(url('/register?error=captcha'), 302); } // Check if email exists if (User::where('users_email', $data['email'])->exists()) { return new RedirectResponse(url('/register?error=1'), 302); } // Create user with role 'user' and user_idbus = null (public user) $user = User::create([ 'users_name' => $data['name'], 'users_email' => $data['email'], 'users_password' => $data['password'], 'users_role' => 'user', 'users_bus_id' => null, 'users_status' => 'pending' ]); $token = bin2hex(random_bytes(32)); $prefs = is_array($user->users_preferences) ? $user->users_preferences : []; $prefs['email_verification_token'] = $token; $prefs['email_verification_created_at'] = date('Y-m-d H:i:s'); $user->users_preferences = $prefs; $user->save(); $verificationLink = url('/verify-email/' . $token) . '?email=' . urlencode($user->users_email); $subject = 'Confirme seu cadastro'; $html = '
Olá ' . htmlspecialchars($user->users_name) . ',
' . 'Obrigado por se cadastrar. Para confirmar seu e-mail, clique no link abaixo:
' . '' . 'Se você não fez este cadastro, ignore este e-mail.
'; $this->sendEmail($user->users_email, $subject, $html); $this->recordAttempt('register', $user->users_email, $request->getClientIp(), true); // Log activity \App\Models\ActivityLog::log( $user->users_id, null, 'created', 'user', $user->users_id, null, ['users_name' => $user->users_name, 'users_email' => $user->users_email], $request->getClientIp(), $request->headers->get('User-Agent') ); return new RedirectResponse(url('/login?registered=1')); } /** * Show profile page. * * @param Request $request * @return Response */ public function profile(Request $request): Response { $user = $this->getCurrentUser(); if (!$user) { return new RedirectResponse(url('/login')); } $error = $request->query->get('error'); $errorMessage = null; if ($error) { $messages = [ '1' => 'Por favor, preencha todos os campos obrigatórios.', 'password_mismatch' => 'A senha atual está incorreta.', 'password_confirmation' => 'A confirmação da senha não confere.', 'weak_password' => 'A nova senha deve ter pelo menos 8 caracteres.', ]; $errorMessage = $messages[$error] ?? 'Ocorreu um erro.'; } // Dados comuns do admin para o perfil $userModel = \Modules\Auth\Models\User::find($user->users_id); $enabledModules = []; if (class_exists('\App\Models\Module')) { $enabledModules = \App\Models\Module::whereHas('companies', function($q) use ($user) { $q->where('bus_modules_bus_id', $user->users_bus_id) ->where('bus_modules_is_enabled', true); })->get(); } return $this->renderView('auth/views/profile.twig', [ 'user' => $user, 'title' => __('auth.profile_title'), 'error' => $errorMessage, 'success' => $request->query->get('success'), 'auth' => ['user' => $user] ]); } /** * Update user profile. * * @param Request $request * @return Response */ public function updateProfile(Request $request): Response { $user = $this->getCurrentUser(); if (!$user) { return new RedirectResponse(url('/login')); } $data = $request->request->all(); // Validate input if (empty($data['name']) || empty($data['email'])) { return new RedirectResponse(url('/profile?error=1'), 302); } if (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) { return new RedirectResponse(url('/profile?error=1'), 302); } // Check uniqueness if email changed if ($data['email'] !== $user->users_email && User::where('users_email', $data['email'])->exists()) { return new RedirectResponse(url('/profile?error=1'), 302); } if ($request->files->has('avatar')) { $file = $request->files->get('avatar'); if ($file && $file->isValid()) { $allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; if (in_array($file->getMimeType(), $allowedMimes)) { $uploadDir = dirname(__DIR__, 4) . '/storage/uploads/avatars'; if (!file_exists($uploadDir)) { mkdir($uploadDir, 0755, true); } try { $formatSetting = strtolower((string) get_env('IMG_FORMAT', 'webp')); $format = $formatSetting === 'auto' ? 'webp' : $formatSetting; $quality = env_int('IMG_QUALITY', 80); $resolutionPercent = env_int('IMG_RESOLUTION', 100); $image = Image::make($file->getPathname()); $width = $image->width(); $height = $image->height(); if ($resolutionPercent > 0 && $resolutionPercent < 100) { $newW = (int) round($width * $resolutionPercent / 100); $newH = (int) round($height * $resolutionPercent / 100); $image->resize($newW, $newH, function ($constraint) { $constraint->aspectRatio(); $constraint->upsize(); }); } $ext = $format === 'jpeg' || $format === 'jpg' ? 'jpg' : ($format === 'png' ? 'png' : ($format === 'gif' ? 'gif' : 'webp')); $filename = 'avatar_' . $user->users_id . '_' . time() . '.' . $ext; if ($format === 'jpeg' || $format === 'jpg') { $image->interlace(true); $image->encode('jpeg', max(1, min(100, $quality))); } elseif ($format === 'png') { $pngCompression = max(0, min(9, (int) round((100 - max(1, min(100, $quality))) / 10))); $image->encode('png', $pngCompression); } elseif ($format === 'gif') { $image->encode('gif'); } else { $image->encode('webp', max(1, min(100, $quality))); } $image->save($uploadDir . '/' . $filename); if ($user->users_avatar && file_exists($uploadDir . '/' . $user->users_avatar)) { unlink($uploadDir . '/' . $user->users_avatar); } $data['users_avatar'] = $filename; \App\Helpers\Log::info('Avatar saved successfully', ['filename' => $filename]); } catch (\Exception $e) { \App\Helpers\Log::error('Avatar upload failed', ['error' => $e->getMessage()]); } } else { \App\Helpers\Log::warning('Avatar upload invalid mime', ['mime' => $file->getMimeType()]); } } } // Get old values for logging $oldValues = [ 'users_name' => $user->users_name, 'users_email' => $user->users_email, 'users_phone' => $user->users_phone, 'users_bio' => $user->users_bio ]; // Prepare update data $updateData = [ 'users_name' => $data['name'], 'users_email' => $data['email'], 'users_phone' => $data['phone'] ?? $user->users_phone, 'users_bio' => $data['bio'] ?? $user->users_bio ]; if (isset($data['users_avatar'])) { $updateData['users_avatar'] = $data['users_avatar']; } // Password update if (!empty($data['password'])) { if (empty($data['current_password']) || !password_verify($data['current_password'], $user->users_password)) { return new RedirectResponse(url('/profile?error=password_mismatch'), 302); } if ($data['password'] !== $data['password_confirmation']) { return new RedirectResponse(url('/profile?error=password_confirmation'), 302); } if (strlen($data['password']) < 8) { return new RedirectResponse(url('/profile?error=weak_password'), 302); } $updateData['users_password'] = $data['password']; } // Update user $user->update($updateData); // Log activity \App\Models\ActivityLog::log( $user->users_id, $user->users_bus_id, 'profile_updated', 'user', $user->users_id, $oldValues, $data, $request->getClientIp(), $request->headers->get('User-Agent') ); return new RedirectResponse(url('/profile?success=1')); } /** * Show forgot password form. * * @param Request $request * @return Response */ public function showForgotPasswordForm(Request $request): Response { $requireCaptcha = $this->getAuthSetting('require_captcha_forgot', true); return new Response($this->view->render('auth/views/forgot-password.twig', [ 'title' => __('auth.forgot_password_title'), 'error' => $request->query->get('error'), 'success' => $request->query->get('success'), 'require_captcha_forgot' => $requireCaptcha ])); } /** * Send password reset link. * * @param Request $request * @return Response */ public function sendResetLink(Request $request): Response { $data = $request->request->all(); $throttleKey = 'forgot_' . ($data['email'] ?? 'unknown') . '_' . $request->getClientIp(); if (!$this->allowAttempt($throttleKey, 3, 600) || !$this->allowPersistent('forgot', $data['email'] ?? null, $request->getClientIp(), 3, 600)) { return new RedirectResponse(url('/forgot-password?error=rate'), 302); } // Validate input if (empty($data['email']) || !filter_var($data['email'], FILTER_VALIDATE_EMAIL)) { return new RedirectResponse(url('/forgot-password?error=1'), 302); } $requireCaptcha = $this->getAuthSetting('require_captcha_forgot', true); if ($requireCaptcha && empty($data['captcha_valid'])) { return new RedirectResponse(url('/forgot-password?error=captcha'), 302); } // Find user by email $user = User::where('users_email', $data['email'])->first(); if (!$user) { return new RedirectResponse(url('/forgot-password?error=2'), 302); } // Generate reset token $token = bin2hex(random_bytes(32)); // Store token in database // Remove existing tokens for this email DB::table('password_resets')->where('password_resets_email', $data['email'])->delete(); // Insert new token DB::table('password_resets')->insert([ 'password_resets_email' => $data['email'], 'password_resets_token' => $token, 'password_resets_created_at' => date('Y-m-d H:i:s') ]); // Log activity \App\Models\ActivityLog::log( null, null, 'password_reset_requested', 'user', $user->users_id, null, ['email' => $data['email']], $request->getClientIp(), $request->headers->get('User-Agent') ); $resetLink = url('/reset-password/' . $token) . '?email=' . urlencode($data['email']); $subject = 'Redefinição de senha'; $html = 'Olá,
' . 'Você solicitou a redefinição de senha. Utilize o link abaixo dentro de 1 hora:
' . '' . 'Se você não solicitou, ignore este e-mail.
'; $this->sendEmail($data['email'], $subject, $html); $this->recordAttempt('forgot', $data['email'], $request->getClientIp(), true); return new RedirectResponse(url('/forgot-password?success=1')); } /** * Show reset password form. * * @param Request $request * @return Response */ public function showResetPasswordForm(Request $request, string $token): Response { $email = $request->query->get('email'); // Email is needed to verify token if (!$token || !$email) { return new RedirectResponse(url('/login?error=3'), 302); } // Validate token from database $record = DB::table('password_resets') ->where('password_resets_email', $email) ->where('password_resets_token', $token) ->first(); if (!$record) { return new RedirectResponse(url('/login?error=3'), 302); } // Check expiration (e.g., 1 hour) $createdAt = strtotime($record->password_resets_created_at); if (time() - $createdAt > 3600) { return new RedirectResponse(url('/login?error=3'), 302); } return new Response($this->view->render('auth/views/reset-password.twig', [ 'title' => __('auth.reset_password_title'), 'token' => $token, 'email' => $email, 'error' => $request->query->get('error') ])); } /** * Reset password. * * @param Request $request * @return Response */ public function resetPassword(Request $request): Response { $data = $request->request->all(); $email = $data['email'] ?? ''; $token = $data['token'] ?? ''; // Validate input if (empty($token) || empty($email) || empty($data['password']) || empty($data['password_confirmation'])) { return new RedirectResponse(url('/reset-password/' . $token . '?email=' . urlencode($email) . '&error=4'), 302); } if ($data['password'] !== $data['password_confirmation']) { return new RedirectResponse(url('/reset-password/' . $token . '?email=' . urlencode($email) . '&error=password_confirmation'), 302); } if (strlen($data['password']) < 8) { return new RedirectResponse(url('/reset-password/' . $token . '?email=' . urlencode($email) . '&error=weak_password'), 302); } // Validate token $record = DB::table('password_resets') ->where('password_resets_email', $email) ->where('password_resets_token', $token) ->first(); if (!$record) { return new RedirectResponse(url('/login?error=5'), 302); } // Check expiration $createdAt = strtotime($record->password_resets_created_at); if (time() - $createdAt > 3600) { return new RedirectResponse(url('/login?error=5'), 302); } // Find user by email $user = User::where('users_email', $email)->first(); if (!$user) { return new RedirectResponse(url('/login?error=6'), 302); } // Update password $user->users_password = $data['password']; $user->save(); // Delete token DB::table('password_resets')->where('password_resets_email', $email)->delete(); // Log activity \App\Models\ActivityLog::log( $user->users_id, $user->users_bus_id, 'password_reset', 'user', $user->users_id, null, null, $request->getClientIp(), $request->headers->get('User-Agent') ); return new RedirectResponse(url('/login?success=password_reset')); } public function verifyEmail(Request $request, string $token): Response { $email = $request->query->get('email'); if (!$token || !$email) { return new RedirectResponse(url('/login?error=3'), 302); } $user = User::withoutGlobalScopes()->where('users_email', $email)->first(); if (!$user) { return new RedirectResponse(url('/login?error=3'), 302); } $prefs = is_array($user->users_preferences) ? $user->users_preferences : []; $savedToken = $prefs['email_verification_token'] ?? null; $created = isset($prefs['email_verification_created_at']) ? strtotime($prefs['email_verification_created_at']) : null; if (!$savedToken || $savedToken !== $token) { return new RedirectResponse(url('/login?error=3'), 302); } if ($created && (time() - $created > 86400)) { return new RedirectResponse(url('/login?error=3'), 302); } $user->users_email_verified_at = \Carbon\Carbon::now(); $user->users_status = 'active'; $prefs['email_verification_token'] = null; $prefs['email_verification_created_at'] = null; $user->users_preferences = $prefs; $user->save(); return new RedirectResponse(url('/login?verified=1')); } public function showResendVerificationForm(Request $request): Response { $requireCaptcha = $this->getAuthSetting('require_captcha_resend', true); return new Response($this->view->render('auth/views/resend-verification.twig', [ 'title' => __('auth.resend_verification'), 'error' => $request->query->get('error'), 'success' => $request->query->get('success'), 'require_captcha_resend' => $requireCaptcha ])); } public function resendVerification(Request $request): Response { $email = $request->request->get('email'); $throttleKey = 'resend_' . ($email ?? 'unknown') . '_' . $request->getClientIp(); if (!$this->allowAttempt($throttleKey, 3, 600)) { return new RedirectResponse(url('/verify-email/resend?error=rate'), 302); } if (!$email || !filter_var($email, FILTER_VALIDATE_EMAIL)) { return new RedirectResponse(url('/verify-email/resend?error=1'), 302); } $requireCaptcha = $this->getAuthSetting('require_captcha_resend', true); if ($requireCaptcha && empty($request->request->get('captcha_valid'))) { return new RedirectResponse(url('/verify-email/resend?error=captcha'), 302); } $user = User::withoutGlobalScopes()->where('users_email', $email)->first(); if (!$user) { return new RedirectResponse(url('/verify-email/resend?error=1'), 302); } if ($user->users_email_verified_at) { return new RedirectResponse(url('/verify-email/resend?error=verified')); } $token = bin2hex(random_bytes(32)); $prefs = is_array($user->users_preferences) ? $user->users_preferences : []; $prefs['email_verification_token'] = $token; $prefs['email_verification_created_at'] = date('Y-m-d H:i:s'); $user->users_preferences = $prefs; $user->save(); $verificationLink = url('/verify-email/' . $token) . '?email=' . urlencode($user->users_email); $subject = 'Confirme seu cadastro'; $html = 'Olá ' . htmlspecialchars($user->users_name) . ',
' . 'Para confirmar seu e-mail, clique no link abaixo:
' . ''; $this->sendEmail($user->users_email, $subject, $html); return new RedirectResponse(url('/login?resend=1')); } protected function sendEmail(string $to, string $subject, string $html): void { try { $dsn = $_ENV['MAILER_DSN'] ?? ''; if (!$dsn) { $transport = \Symfony\Component\Mailer\Transport::fromDsn(sprintf( 'smtp://%s:%s@%s:%s?encryption=%s', $_ENV['MAIL_USERNAME'] ?? '', $_ENV['MAIL_PASSWORD'] ?? '', $_ENV['MAIL_HOST'] ?? 'localhost', $_ENV['MAIL_PORT'] ?? '25', $_ENV['MAIL_ENCRYPTION'] ?? '' )); } else { $transport = \Symfony\Component\Mailer\Transport::fromDsn($dsn); } $mailer = new \Symfony\Component\Mailer\Mailer($transport); $email = (new \Symfony\Component\Mime\Email()) ->from($_ENV['MAIL_FROM_ADDRESS'] ?? 'no-reply@localhost') ->to($to) ->subject($subject) ->html($html); $mailer->send($email); } catch (\Throwable $e) { } } private function allowAttempt(string $key, int $maxAttempts, int $decaySeconds): bool { $now = time(); if (!isset($_SESSION['throttle'])) { $_SESSION['throttle'] = []; } $bucket = $_SESSION['throttle'][$key] ?? []; $bucket = array_filter($bucket, function ($ts) use ($now, $decaySeconds) { return ($now - $ts) <= $decaySeconds; }); if (count($bucket) >= $maxAttempts) { $_SESSION['throttle'][$key] = $bucket; return false; } $bucket[] = $now; $_SESSION['throttle'][$key] = $bucket; return true; } private function allowPersistent(string $action, ?string $email, string $ip, int $maxAttempts, int $decaySeconds): bool { try { $since = date('Y-m-d H:i:s', time() - $decaySeconds); $query = \Illuminate\Database\Capsule\Manager::table('auth_attempts') ->where('ip', $ip) ->where('action', $action) ->where('created_at', '>=', $since); if ($email) { $query->where('email', $email); } $count = (int) $query->count(); return $count < $maxAttempts; } catch (\Throwable $e) { return true; } } private function recordAttempt(string $action, ?string $email, string $ip, bool $success): void { try { \Illuminate\Database\Capsule\Manager::table('auth_attempts')->insert([ 'email' => $email, 'ip' => $ip, 'action' => $action, 'success' => $success, 'created_at' => date('Y-m-d H:i:s') ]); } catch (\Throwable $e) {} } private function setRememberCookie(User $user): void { $token = bin2hex(random_bytes(32)); $user->users_remember_token = $token; $user->save(); $secret = $_ENV['APP_KEY'] ?? ''; $payload = $user->users_id . ':' . $token; $sig = hash_hmac('sha256', $payload, $secret); $value = $payload . ':' . $sig; $secure = (($_ENV['COOKIE_SECURE'] ?? 'false') === 'true'); setcookie('remember_token', $value, time() + 60 * 60 * 24 * 30, '/', '', $secure, true); } private function getAuthSetting(string $key, $default = null) { try { $module = \App\Models\Module::byName('auth')->first(); if ($module) { return $module->getSetting($key, $default); } } catch (\Throwable $e) {} return $default; } }