179 lines
5.7 KiB
PHP
179 lines
5.7 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Auth;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Mail\RegistrationVerificationMail;
|
|
use App\Models\User;
|
|
use App\Services\Security\RecaptchaVerifier;
|
|
use Carbon\CarbonImmutable;
|
|
use Illuminate\Http\RedirectResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Hash;
|
|
use Illuminate\Support\Facades\Mail;
|
|
use Illuminate\Support\Str;
|
|
use Illuminate\View\View;
|
|
|
|
class RegisteredUserController extends Controller
|
|
{
|
|
public function __construct(
|
|
private readonly RecaptchaVerifier $recaptchaVerifier
|
|
)
|
|
{
|
|
}
|
|
|
|
/**
|
|
* Display the registration view.
|
|
*/
|
|
public function create(Request $request): View
|
|
{
|
|
return view('auth.register', [
|
|
'prefillEmail' => (string) $request->query('email', ''),
|
|
]);
|
|
}
|
|
|
|
public function notice(Request $request): View
|
|
{
|
|
$email = (string) session('registration_email', '');
|
|
$remaining = $email === '' ? 0 : $this->resendRemainingSeconds($email);
|
|
|
|
return view('auth.register-notice', [
|
|
'email' => $email,
|
|
'resendSeconds' => $remaining,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Handle an incoming registration request.
|
|
*
|
|
* @throws \Illuminate\Validation\ValidationException
|
|
*/
|
|
public function store(Request $request): RedirectResponse
|
|
{
|
|
$validated = $request->validate([
|
|
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
|
|
'website' => ['nullable', 'max:0'],
|
|
]);
|
|
|
|
if ($this->recaptchaVerifier->isEnabled()) {
|
|
$request->validate([
|
|
'g-recaptcha-response' => ['required', 'string'],
|
|
]);
|
|
|
|
$verified = $this->recaptchaVerifier->verify(
|
|
(string) $request->input('g-recaptcha-response', ''),
|
|
$request->ip()
|
|
);
|
|
|
|
if (! $verified) {
|
|
return back()
|
|
->withInput($request->except('website'))
|
|
->withErrors(['captcha' => 'reCAPTCHA verification failed. Please try again.']);
|
|
}
|
|
}
|
|
|
|
$user = User::create([
|
|
'username' => null,
|
|
'name' => Str::before((string) $validated['email'], '@'),
|
|
'email' => $validated['email'],
|
|
'password' => Hash::make(Str::random(64)),
|
|
'is_active' => false,
|
|
'onboarding_step' => 'email',
|
|
'username_changed_at' => now(),
|
|
]);
|
|
|
|
$token = Str::random(64);
|
|
DB::table('user_verification_tokens')->insert([
|
|
'user_id' => $user->id,
|
|
'token' => $token,
|
|
'expires_at' => now()->addDay(),
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
|
|
Mail::to($user->email)->queue(new RegistrationVerificationMail($token));
|
|
|
|
$cooldown = $this->resendCooldownSeconds();
|
|
$this->setResendCooldown((string) $validated['email'], $cooldown);
|
|
|
|
return redirect(route('register.notice', absolute: false))
|
|
->with('status', 'Verification email sent. Please check your inbox.')
|
|
->with('registration_email', (string) $validated['email']);
|
|
}
|
|
|
|
public function resendVerification(Request $request): RedirectResponse
|
|
{
|
|
$validated = $request->validate([
|
|
'email' => ['required', 'string', 'lowercase', 'email', 'max:255'],
|
|
]);
|
|
|
|
$email = (string) $validated['email'];
|
|
$remaining = $this->resendRemainingSeconds($email);
|
|
if ($remaining > 0) {
|
|
return back()
|
|
->with('registration_email', $email)
|
|
->withErrors(['email' => "Please wait {$remaining} seconds before resending."]);
|
|
}
|
|
|
|
$user = User::query()
|
|
->where('email', $email)
|
|
->whereNull('email_verified_at')
|
|
->where('onboarding_step', 'email')
|
|
->first();
|
|
|
|
if (! $user) {
|
|
return back()
|
|
->with('registration_email', $email)
|
|
->withErrors(['email' => 'No pending verification found for this email.']);
|
|
}
|
|
|
|
DB::table('user_verification_tokens')->where('user_id', $user->id)->delete();
|
|
|
|
$token = Str::random(64);
|
|
DB::table('user_verification_tokens')->insert([
|
|
'user_id' => $user->id,
|
|
'token' => $token,
|
|
'expires_at' => now()->addDay(),
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
|
|
Mail::to($user->email)->queue(new RegistrationVerificationMail($token));
|
|
|
|
$cooldown = $this->resendCooldownSeconds();
|
|
$this->setResendCooldown($email, $cooldown);
|
|
|
|
return redirect(route('register.notice', absolute: false))
|
|
->with('registration_email', $email)
|
|
->with('status', 'Verification email resent. Please check your inbox.');
|
|
}
|
|
|
|
private function resendCooldownSeconds(): int
|
|
{
|
|
return max(5, (int) config('antispam.register.resend_cooldown_seconds', 60));
|
|
}
|
|
|
|
private function resendCooldownCacheKey(string $email): string
|
|
{
|
|
return 'register:resend:cooldown:' . sha1(strtolower(trim($email)));
|
|
}
|
|
|
|
private function setResendCooldown(string $email, int $seconds): void
|
|
{
|
|
$until = CarbonImmutable::now()->addSeconds($seconds)->timestamp;
|
|
Cache::put($this->resendCooldownCacheKey($email), $until, $seconds + 5);
|
|
}
|
|
|
|
private function resendRemainingSeconds(string $email): int
|
|
{
|
|
$until = (int) Cache::get($this->resendCooldownCacheKey($email), 0);
|
|
if ($until <= 0) {
|
|
return 0;
|
|
}
|
|
|
|
return max(0, $until - time());
|
|
}
|
|
}
|