nicer package layout, also in checkout step 1, fixed missing registration language strings, registration error handling, email verification redirect, email verification error handling and messaging,

This commit is contained in:
Codex Agent
2025-11-19 20:21:54 +01:00
parent 91d3e61b0e
commit 8d2075bdd2
24 changed files with 1000 additions and 363 deletions

View File

@@ -9,6 +9,7 @@ use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Http\Client\ConnectionException as HttpClientConnectionException;
use Illuminate\Queue\InvalidQueueException;
use Illuminate\Queue\MaxAttemptsExceededException;
use Illuminate\Routing\Exceptions\InvalidSignatureException;
use Illuminate\Validation\ValidationException;
use League\Flysystem\FilesystemException;
use PDOException;
@@ -70,6 +71,16 @@ class Handler extends ExceptionHandler
}
}
if ($e instanceof InvalidSignatureException && ! $request->expectsJson()) {
$request->session()->flash('verification', [
'status' => 'error',
'title' => __('auth.verification.expired_title'),
'message' => __('auth.verification.expired_message'),
]);
return redirect()->route('verification.notice');
}
if (! $request->expectsJson() && ! $request->inertia()) {
if ($hintKey = $this->resolveServerErrorHint($e)) {
$request->attributes->set('serverErrorHint', __($hintKey, [], app()->getLocale()));

View File

@@ -15,8 +15,18 @@ class EmailVerificationPromptController extends Controller
*/
public function __invoke(Request $request): Response|RedirectResponse
{
return $request->user()->hasVerifiedEmail()
? redirect()->intended(route('dashboard', absolute: false))
: Inertia::render('auth/verify-email', ['status' => $request->session()->get('status')]);
if ($request->user()->hasVerifiedEmail()) {
$redirectToCheckout = $request->session()->pull('checkout.verify_redirect');
if ($redirectToCheckout) {
$separator = str_contains($redirectToCheckout, '?') ? '&' : '?';
return redirect()->to($redirectToCheckout.$separator.'verified=1');
}
return redirect()->intended(route('dashboard', absolute: false));
}
return Inertia::render('auth/verify-email', ['status' => $request->session()->get('status')]);
}
}

View File

@@ -13,12 +13,61 @@ class VerifyEmailController extends Controller
*/
public function __invoke(EmailVerificationRequest $request): RedirectResponse
{
if ($request->user()->hasVerifiedEmail()) {
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
if (! $request->user()->hasVerifiedEmail()) {
$request->fulfill();
}
$request->fulfill();
return $this->redirectAfterVerification($request);
}
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
protected function redirectAfterVerification(EmailVerificationRequest $request): RedirectResponse
{
$redirectToCheckout = $request->session()->pull('checkout.verify_redirect');
if (! $redirectToCheckout && $request->user()->pending_purchase) {
$packageId = $request->session()->pull('checkout.pending_package_id');
if (! $packageId) {
$packageId = optional($request->user()->tenant)
?->packages()
->latest('tenant_packages.created_at')
->value('packages.id');
}
if ($packageId) {
$redirectToCheckout = route('checkout.show', ['package' => $packageId]);
}
}
$this->flashVerificationSuccess($request, (bool) $redirectToCheckout);
if ($redirectToCheckout) {
$request->session()->forget('checkout.pending_package_id');
$separator = str_contains($redirectToCheckout, '?') ? '&' : '?';
return redirect()->to($redirectToCheckout.$separator.'verified=1');
}
$fallbackLogin = route('marketing.login');
$separator = str_contains($fallbackLogin, '?') ? '&' : '?';
return redirect()->intended($fallbackLogin.$separator.'verified=1');
}
private function flashVerificationSuccess(EmailVerificationRequest $request, bool $forCheckout): void
{
$message = $forCheckout
? __('auth.verification.checkout_success_message')
: __('auth.verification.success_message');
$request->session()->flash('verification', [
'status' => 'success',
'title' => __('auth.verification.success_title'),
'message' => $message,
]);
if (! $forCheckout) {
$request->session()->flash('status', __('auth.verification.success_message'));
}
}
}

View File

@@ -76,7 +76,7 @@ class CheckoutController extends Controller
$package = Package::findOrFail($request->package_id);
$validated = $validator->validated();
DB::transaction(function () use ($request, $package, $validated) {
$user = DB::transaction(function () use ($request, $package, $validated) {
// User erstellen
$user = User::create([
@@ -138,10 +138,28 @@ class CheckoutController extends Controller
Mail::to($user)
->locale($user->preferred_locale ?? app()->getLocale())
->queue(new Welcome($user));
return $user;
});
Auth::login($user);
$request->session()->put('checkout.pending_package_id', $package->id);
$redirectUrl = route('checkout.show', ['package' => $package->id]);
$request->session()->put('checkout.verify_redirect', $redirectUrl);
$request->session()->put('url.intended', $redirectUrl);
return response()->json([
'success' => true,
'message' => 'Registrierung erfolgreich. Bitte überprüfen Sie Ihre E-Mail zur Verifizierung.',
'redirect' => $redirectUrl,
'user' => [
'id' => $user->id,
'email' => $user->email,
'name' => $user->name ?? trim($user->first_name.' '.$user->last_name),
'pending_purchase' => $user->pending_purchase ?? true,
'email_verified_at' => $user->email_verified_at,
],
'pending_purchase' => $user->pending_purchase ?? true,
]);
}

View File

@@ -48,13 +48,13 @@ class Kernel extends HttpKernel
];
/**
* The application's route middleware.
* The application's middleware aliases.
*
* These middleware may be assigned to groups or used individually.
*
* @var array<string, class-string|string>
*/
protected $routeMiddleware = [
protected $middlewareAliases = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,

View File

@@ -69,6 +69,9 @@ class HandleInertiaRequests extends Middleware
'profile' => __('profile'),
'dashboard' => __('dashboard'),
],
'flash' => [
'verification' => fn () => $request->session()->get('verification'),
],
];
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class NormalizeSignedUrlParameters
{
public function handle(Request $request, Closure $next)
{
$queryString = $request->server->get('QUERY_STRING');
if (is_string($queryString) && str_contains($queryString, '&amp;')) {
$normalized = str_replace('&amp;', '&', $queryString);
if ($normalized !== $queryString) {
$request->server->set('QUERY_STRING', $normalized);
parse_str($normalized, $params);
if (is_array($params) && ! empty($params)) {
$request->query->replace(array_merge($request->query->all(), $params));
}
}
}
return $next($request);
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Routing\Exceptions\InvalidSignatureException;
use Illuminate\Routing\Middleware\ValidateSignature as BaseValidateSignature;
class ValidateSignature extends BaseValidateSignature
{
public function handle($request, Closure $next, ...$args)
{
try {
return parent::handle($request, $next, ...$args);
} catch (InvalidSignatureException $exception) {
if ($request->expectsJson()) {
throw $exception;
}
if ($request->routeIs('verification.verify')) {
$request->session()->flash('verification', [
'status' => 'error',
'title' => __('auth.verification.expired_title'),
'message' => __('auth.verification.expired_message'),
]);
return redirect()->route('verification.notice');
}
throw $exception;
}
}
}

View File

@@ -7,8 +7,10 @@ use App\Models\Tenant;
use App\Models\User;
use App\Policies\PurchaseHistoryPolicy;
use App\Policies\TenantPolicy;
use Illuminate\Auth\Notifications\VerifyEmail;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\URL;
class AuthServiceProvider extends ServiceProvider
{
@@ -29,6 +31,20 @@ class AuthServiceProvider extends ServiceProvider
{
$this->registerPolicies();
VerifyEmail::createUrlUsing(function (User $notifiable): string {
$relativeUrl = URL::temporarySignedRoute(
'verification.verify',
now()->addMinutes((int) config('auth.verification.expire', 60)),
[
'id' => $notifiable->getKey(),
'hash' => sha1($notifiable->getEmailForVerification()),
],
absolute: false,
);
return URL::to($relativeUrl);
});
Gate::before(function (User $user): ?bool {
return $user->role === 'super_admin' ? true : null;
});