2026-09-24 17:27:58 +03:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Http\Controllers\Auth;
|
|
|
|
|
|
|
|
|
|
use App\Http\Controllers\Controller;
|
2026-09-25 16:19:13 +03:00
|
|
|
use App\Rules\HCaptcha;
|
2026-09-24 17:27:58 +03:00
|
|
|
use Illuminate\Http\RedirectResponse;
|
|
|
|
|
use Illuminate\Http\Request;
|
|
|
|
|
use Illuminate\Support\Facades\Auth;
|
|
|
|
|
use Illuminate\Support\Str;
|
|
|
|
|
use Illuminate\View\View;
|
|
|
|
|
use Modules\Core\Auth\Exceptions\OtpThrottledException;
|
|
|
|
|
use Modules\Core\Auth\Services\UserOtpService;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Passwordless customer login: email → emailed 6-digit code → logged in.
|
|
|
|
|
* Login and registration are the same flow — UserOtpService::generateAndSend()
|
|
|
|
|
* find-or-creates the user (and its Customer).
|
|
|
|
|
*
|
|
|
|
|
* All the security lives in UserOtpService: per-email code-request throttling
|
|
|
|
|
* (OtpThrottledException), wrong-guess lockout, the Auth::login() itself (which
|
|
|
|
|
* also regenerates the session and merges the guest cart via Lunar's Login
|
|
|
|
|
* listener) and the session-registry record. This controller only moves the
|
|
|
|
|
* shopper between the two steps, carrying the email in the session rather
|
|
|
|
|
* than the URL.
|
|
|
|
|
*/
|
|
|
|
|
class LoginController extends Controller
|
|
|
|
|
{
|
2026-09-24 19:08:13 +03:00
|
|
|
/**
|
|
|
|
|
* `?redirect=/el/checkout` (e.g. from the checkout's login tab) becomes the
|
|
|
|
|
* intended URL that verify() returns to. Only a same-site path is accepted:
|
|
|
|
|
* no scheme, no protocol-relative `//host`, so it can't redirect off-site.
|
|
|
|
|
*/
|
|
|
|
|
public function create(string $locale, Request $request): View
|
2026-09-24 17:27:58 +03:00
|
|
|
{
|
2026-09-24 19:08:13 +03:00
|
|
|
$redirect = (string) $request->query('redirect', '');
|
|
|
|
|
|
|
|
|
|
if (preg_match('#^/(?![/\\\\])#', $redirect)) {
|
|
|
|
|
$request->session()->put('url.intended', url($redirect));
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-24 17:27:58 +03:00
|
|
|
return view('auth.login');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function send(string $locale, Request $request, UserOtpService $otp): RedirectResponse
|
|
|
|
|
{
|
2026-09-25 16:19:13 +03:00
|
|
|
// Captcha only here: verify() and resend() need the email this step
|
|
|
|
|
// puts in the session, so they can't be reached without passing it.
|
2026-09-24 17:27:58 +03:00
|
|
|
$validated = $request->validate([
|
|
|
|
|
'email' => ['required', 'email', 'max:255'],
|
2026-09-25 16:19:13 +03:00
|
|
|
'h-captcha-response' => ['bail', 'required', new HCaptcha],
|
|
|
|
|
], [
|
|
|
|
|
'h-captcha-response.required' => __('storefront.auth.captcha_failed'),
|
2026-09-24 17:27:58 +03:00
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$email = Str::lower(trim($validated['email']));
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
$otp->generateAndSend($email);
|
|
|
|
|
} catch (OtpThrottledException) {
|
|
|
|
|
return back()->withInput()->withErrors([
|
|
|
|
|
'email' => __('storefront.auth.too_many_codes'),
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$request->session()->put('login.email', $email);
|
|
|
|
|
|
|
|
|
|
return redirect()->route('login.code');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function code(string $locale, Request $request): View|RedirectResponse
|
|
|
|
|
{
|
|
|
|
|
$email = $request->session()->get('login.email');
|
|
|
|
|
|
|
|
|
|
if (! $email) {
|
|
|
|
|
return redirect()->route('login');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return view('auth.login-code', ['email' => $email]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function resend(string $locale, Request $request, UserOtpService $otp): RedirectResponse
|
|
|
|
|
{
|
|
|
|
|
$email = $request->session()->get('login.email');
|
|
|
|
|
|
|
|
|
|
if (! $email) {
|
|
|
|
|
return redirect()->route('login');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
$otp->generateAndSend($email);
|
|
|
|
|
} catch (OtpThrottledException) {
|
|
|
|
|
return back()->withErrors([
|
|
|
|
|
'code' => __('storefront.auth.too_many_codes'),
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return back()->with('status', __('storefront.auth.code_resent'));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function verify(string $locale, Request $request, UserOtpService $otp): RedirectResponse
|
|
|
|
|
{
|
|
|
|
|
$email = $request->session()->get('login.email');
|
|
|
|
|
|
|
|
|
|
if (! $email) {
|
|
|
|
|
return redirect()->route('login');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$validated = $request->validate([
|
|
|
|
|
'code' => ['required', 'digits:6'],
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// Wrong, expired, or locked out after too many guesses — core doesn't
|
|
|
|
|
// say which, so neither do we; the page offers "resend code" for all three.
|
|
|
|
|
if (! $otp->validate($email, $validated['code'], $request)) {
|
|
|
|
|
return back()->withErrors([
|
|
|
|
|
'code' => __('storefront.auth.invalid_code'),
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$request->session()->forget('login.email');
|
|
|
|
|
|
|
|
|
|
return redirect()->intended(route('home'));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function destroy(string $locale, Request $request): RedirectResponse
|
|
|
|
|
{
|
|
|
|
|
Auth::logout();
|
|
|
|
|
|
|
|
|
|
$request->session()->invalidate();
|
|
|
|
|
$request->session()->regenerateToken();
|
|
|
|
|
|
|
|
|
|
return redirect()->route('home');
|
|
|
|
|
}
|
|
|
|
|
}
|