with('user') ->where('token_hash', hash('sha256', $token)) ->where('purpose', 'login') ->first(); if (! $magicLink || ! $magicLink->user) { return redirect()->route('login')->with('status', __('The magic login link is invalid.')); } if ($magicLink->consumed_at !== null || $magicLink->expires_at->isPast()) { return redirect()->route('login')->with('status', __('The magic login link has expired or was already used.')); } if (! $magicLink->user->is_active) { return redirect()->route('login')->with('status', __('Your account is not active.')); } // Atomar beanspruchen: nur EIN paralleler Request darf den Link // verbrauchen. Das konditionale UPDATE greift dank Zeilen-Atomarität // genau einmal; verliert ein Race, ist consumed_at bereits gesetzt. $claimed = MagicLink::query() ->whereKey($magicLink->id) ->whereNull('consumed_at') ->update([ 'consumed_at' => now(), 'ip_consumed' => $request->ip(), ]); if ($claimed === 0) { return redirect()->route('login')->with('status', __('The magic login link has expired or was already used.')); } // Stufe 2 der Konversionsleiter. Die Herkunft kommt vom Token, nicht // aus der Session — die ist beim Weg über Mail und Domainwechsel // längst gerissen (Roadmap 0.4). $this->recorder->record( ConversionEventType::MagicLinkConsumed, $this->recorder->sourceOf($magicLink), ); $user = $magicLink->user; // 2FA aktiv: nicht direkt einloggen, sondern – wie der Passwort-Login – // in Fortifys Challenge übergeben. Der Magic-Link (Besitz des Postfachs) // ersetzt nur das Passwort, nicht den zweiten Faktor. Der Link ist bereits // atomar verbraucht; bricht der Nutzer die Challenge ab, fordert er einen // neuen an. if ($user->hasEnabledTwoFactorAuthentication()) { $request->session()->put([ 'login.id' => $user->getKey(), 'login.remember' => false, ]); $request->session()->regenerate(); return redirect()->route('two-factor.challenge'); } $user->update([ 'last_login_at' => now(), 'last_login_ip' => $request->ip(), ]); $request->attributes->set(RecordAuthActivity::AUTH_METHOD_ATTRIBUTE, 'magic-link'); Auth::guard('web')->login($user); $request->session()->regenerate(); // Rollensicherer Redirect ohne intended(): eine als Gast besuchte // Admin-URL (z. B. /dashboard) darf einen Customer nach dem // Magic-Link-Login nicht in den 403-Admin-Bereich schicken. $home = $user->canAccessAdmin() ? route('dashboard', absolute: false) : ($user->canAccessCustomer() ? route('me.dashboard', absolute: false) : '/'); // „Diesen Newsroom verwalten" (Roadmap 1b.2): direkt auf die Firma, // um die es ging — sofern der Account sie wirklich verwalten darf. $landingCompanyId = (int) ($magicLink->payload['company_id'] ?? 0); if ($landingCompanyId > 0 && $user->canAccessCustomer() && ! $user->canAccessAdmin() && $this->companies->findFor($user, $landingCompanyId) !== null) { $home = route('me.press-kits.show', ['id' => $landingCompanyId], absolute: false); // Aus der Veröffentlichungsmail (Roadmap 1c.2): direkt auf die // Meldung mit ihren Aufrufen, wenn der Account sie sehen darf. $landingPressReleaseId = (int) ($magicLink->payload['press_release_id'] ?? 0); if ($landingPressReleaseId > 0) { $pressRelease = PressRelease::query()->withoutGlobalScopes()->whereKey($landingPressReleaseId)->where('company_id', $landingCompanyId)->first(); if ($pressRelease !== null && $user->can('view', $pressRelease)) { $home = route('me.press-releases.show', ['id' => $landingPressReleaseId], absolute: false); } } } return redirect($home); } }