- R-01: ApiNewsroomAssignment hängt an eine bestehende Firma nur noch Owner, Mitglieder oder Nutzer aus NEWSROOM_API_ASSIGNMENT_USER_IDS; Pivot mit granted_via = api; Befehl company-memberships:api-assignments (Liste, --remove mit Vorschau) - K-04: OAuth-Verifikation eines offenen Kontos verwirft Passwort, remember_token und Sanctum-Tokens; SessionRevocation + Middleware EnsureSessionIsNotRevoked widerruft alte Sessions (Redis-tauglich) - K-08: LoginCompletion für Passwort, Magic Link und Google — last_login_at und Anmeldemethode auch nach 2FA, Landeziel über die Challenge, Google ohne erzwungenes remember, magic-login ohne guest für eigene Links - B-10: eigener Magic-Link-Zweck image_confirmation (Migration) - K-07: legalAcceptanceVersions #[Locked], Versionswechsel beim Absenden wird abgelehnt, Gate-Abfragen je Request zwischengespeichert Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
98 lines
3.4 KiB
PHP
98 lines
3.4 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Auth;
|
|
|
|
use App\Listeners\RecordAuthActivity;
|
|
use App\Models\User;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\Session;
|
|
|
|
/**
|
|
* Gemeinsamer Abschluss der drei Anmeldewege Passwort, Magic Link und
|
|
* Google (Plan 1.8, Umsetzungsaudit 29.09.2026 K-08).
|
|
*
|
|
* - Direkter Login: Anmeldeweg fürs Aktivitäts-Log, Session-Login,
|
|
* Session-ID erneuern, `last_login_at`/`last_login_ip`.
|
|
* - Übergabe an die 2FA-Challenge: derselbe Session-Vertrag wie Fortify
|
|
* (`login.id`, `login.remember`), zusätzlich der Anmeldeweg und bei Bedarf
|
|
* das Landeziel (`url.intended`). Nach der Challenge liest
|
|
* RecordAuthActivity den Anmeldeweg, RoleAwareLoginResponse setzt
|
|
* `last_login_at` und folgt dem Landeziel.
|
|
*
|
|
* „Angemeldet bleiben" gibt es nur beim Passwort-Login als bewusste Wahl im
|
|
* Formular. Magic Link und Google setzen keinen Remember-Cookie: Beide
|
|
* beweisen den Besitz des Postfachs bzw. des Google-Kontos für diese eine
|
|
* Anmeldung, ein Jahres-Cookie folgt daraus nicht.
|
|
*/
|
|
class LoginCompletion
|
|
{
|
|
/**
|
|
* Anmeldeweg während einer offenen 2FA-Challenge.
|
|
*/
|
|
public const METHOD_SESSION_KEY = 'login.method';
|
|
|
|
public function login(Request $request, User $user, string $method, bool $remember = false): void
|
|
{
|
|
$request->attributes->set(RecordAuthActivity::AUTH_METHOD_ATTRIBUTE, $method);
|
|
Session::forget(self::METHOD_SESSION_KEY);
|
|
|
|
Auth::guard('web')->login($user, $remember);
|
|
Session::regenerate();
|
|
|
|
$this->recordLogin($user, $request);
|
|
}
|
|
|
|
/**
|
|
* Übergabe an Fortifys 2FA-Challenge statt eines direkten Logins.
|
|
*
|
|
* @param string|null $intended Landeziel nach bestandener Challenge
|
|
* (wird von RoleAwareLoginResponse nur
|
|
* übernommen, wenn der Nutzer es
|
|
* erreichen darf).
|
|
*/
|
|
public function handOffToTwoFactor(Request $request, User $user, string $method, bool $remember = false, ?string $intended = null): void
|
|
{
|
|
// Session-Fassade statt $request->session(): auch aus Livewire-Aktionen
|
|
// (Volt-Login) nutzbar, die denselben Store verwenden.
|
|
Session::put([
|
|
'login.id' => $user->getKey(),
|
|
'login.remember' => $remember,
|
|
self::METHOD_SESSION_KEY => $method,
|
|
]);
|
|
|
|
if ($intended !== null) {
|
|
Session::put('url.intended', $intended);
|
|
}
|
|
|
|
Session::regenerate();
|
|
}
|
|
|
|
public function recordLogin(User $user, Request $request): void
|
|
{
|
|
$user->forceFill([
|
|
'last_login_at' => now(),
|
|
'last_login_ip' => $request->ip(),
|
|
])->save();
|
|
}
|
|
|
|
/**
|
|
* Anmeldeweg des gerade laufenden Logins: vom einloggenden Code gesetzt,
|
|
* beim Abschluss der 2FA-Challenge aus der Session, sonst Passwort
|
|
* (Fortify-/Volt-Login).
|
|
*/
|
|
public static function methodOf(Request $request): string
|
|
{
|
|
$method = $request->attributes->get(RecordAuthActivity::AUTH_METHOD_ATTRIBUTE);
|
|
|
|
if (is_string($method) && $method !== '') {
|
|
return $method;
|
|
}
|
|
|
|
if ($request->routeIs('two-factor.login.store')) {
|
|
return (string) Session::pull(self::METHOD_SESSION_KEY, 'password');
|
|
}
|
|
|
|
return 'password';
|
|
}
|
|
}
|