feat: add captcha-backed forum security hardening

This commit is contained in:
2026-03-17 16:06:28 +01:00
parent 980a15f66e
commit b3fc889452
40 changed files with 2849 additions and 108 deletions

View File

@@ -4,6 +4,7 @@ namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Http\Requests\Auth\LoginRequest;
use App\Services\Security\CaptchaVerifier;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
@@ -14,9 +15,17 @@ class AuthenticatedSessionController extends Controller
/**
* Display the login view.
*/
public function __construct(
private readonly CaptchaVerifier $captchaVerifier,
) {
}
public function create(): View
{
return view('auth.login');
return view('auth.login', [
'requiresCaptcha' => session('bot_captcha_required', false),
'captcha' => $this->captchaVerifier->frontendConfig(),
]);
}
/**

View File

@@ -8,7 +8,7 @@ use App\Models\EmailSendEvent;
use App\Models\User;
use App\Services\Auth\DisposableEmailService;
use App\Services\Auth\RegistrationVerificationTokenService;
use App\Services\Security\TurnstileVerifier;
use App\Services\Security\CaptchaVerifier;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
@@ -19,7 +19,7 @@ use Illuminate\View\View;
class RegisteredUserController extends Controller
{
public function __construct(
private readonly TurnstileVerifier $turnstileVerifier,
private readonly CaptchaVerifier $captchaVerifier,
private readonly DisposableEmailService $disposableEmailService,
private readonly RegistrationVerificationTokenService $verificationTokenService,
)
@@ -33,8 +33,8 @@ class RegisteredUserController extends Controller
{
return view('auth.register', [
'prefillEmail' => (string) $request->query('email', ''),
'requiresTurnstile' => $this->shouldRequireTurnstile($request->ip()),
'turnstileSiteKey' => (string) config('services.turnstile.site_key', ''),
'requiresCaptcha' => $this->shouldRequireCaptcha($request->ip()),
'captcha' => $this->captchaVerifier->frontendConfig(),
]);
}
@@ -56,20 +56,22 @@ class RegisteredUserController extends Controller
*/
public function store(Request $request): RedirectResponse
{
$validated = $request->validate([
$rules = [
'email' => ['required', 'string', 'lowercase', 'email', 'max:255'],
'website' => ['nullable', 'max:0'],
'cf-turnstile-response' => ['nullable', 'string'],
]);
];
$rules[$this->captchaVerifier->inputName()] = ['nullable', 'string'];
$validated = $request->validate($rules);
$email = strtolower(trim((string) $validated['email']));
$ip = $request->ip();
$this->trackRegisterAttempt($ip);
if ($this->shouldRequireTurnstile($ip)) {
$verified = $this->turnstileVerifier->verify(
(string) $request->input('cf-turnstile-response', ''),
if ($this->shouldRequireCaptcha($ip)) {
$verified = $this->captchaVerifier->verify(
(string) $request->input($this->captchaVerifier->inputName(), ''),
$ip
);
@@ -199,9 +201,9 @@ class RegisteredUserController extends Controller
]);
}
private function shouldRequireTurnstile(?string $ip): bool
private function shouldRequireCaptcha(?string $ip): bool
{
if (! $this->turnstileVerifier->isEnabled()) {
if (! $this->captchaVerifier->isEnabled()) {
return false;
}

View File

@@ -17,6 +17,7 @@ use App\Models\Artwork;
use App\Models\ProfileComment;
use App\Models\Story;
use App\Models\User;
use App\Services\Security\CaptchaVerifier;
use App\Services\AvatarService;
use App\Services\ArtworkService;
use App\Services\FollowService;
@@ -47,6 +48,7 @@ class ProfileController extends Controller
private readonly UsernameApprovalService $usernameApprovalService,
private readonly FollowService $followService,
private readonly UserStatsService $userStats,
private readonly CaptchaVerifier $captchaVerifier,
)
{
}
@@ -240,7 +242,9 @@ class ProfileController extends Controller
'flash' => [
'status' => session('status'),
'error' => session('error'),
'botCaptchaRequired' => session('bot_captcha_required', false),
],
'captcha' => $this->captchaVerifier->frontendConfig(),
])->rootView('settings');
}