WS-6: Google-Login via Laravel Socialite

- Socialite installiert; oauth_provider/oauth_provider_id an users (Migration).
- GoogleController (redirect/callback) + SocialAuthService: De-Dup über E-Mail,
  neuer User aktiv + verifiziert + customer (Verifizierung über den Google-
  Kanal), offener Selbst-Registrierer wird onboardet, deaktivierter Account wird
  NICHT reaktiviert. Abschluss über die gemeinsame LoginRedirect-Logik
  (rollengerecht, 403-sicher).
- Routen /auth/google/redirect + /auth/google/callback (guest), "Mit Google
  anmelden/registrieren"-Buttons auf Login und Register.
- config/services.php google + .env.example-Keys; Sicherheits-/Deployment-Doku
  ergänzt (Keys, Redirect-URI, Migration).

Tests: neuer User, De-Dup bestehender User, deaktivierter Account blockiert,
unverifizierter Registrierer onboardet, fehlgeschlagener Callback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-06-16 10:39:19 +00:00
parent ae79d5bee4
commit 068a5a4b49
13 changed files with 715 additions and 1 deletions

View file

@ -0,0 +1,62 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Services\Auth\SocialAuthService;
use App\Support\LoginRedirect;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Laravel\Socialite\Facades\Socialite;
/**
* Google-Login (WS-6). Erst-Login legt de-dupliziert über die E-Mail einen
* aktiven, verifizierten customer-Account an; bestehende Accounts werden nur
* verknüpft. Deaktivierte Accounts werden nicht reaktiviert.
*/
class GoogleController extends Controller
{
public function redirect(): RedirectResponse
{
return Socialite::driver('google')->redirect();
}
public function callback(Request $request, SocialAuthService $social): RedirectResponse
{
try {
$googleUser = Socialite::driver('google')->user();
} catch (\Throwable) {
return redirect()->route('login')->withErrors([
'email' => __('Die Anmeldung mit Google ist fehlgeschlagen. Bitte versuchen Sie es erneut.'),
]);
}
$user = $social->resolveUser(
'google',
(string) $googleUser->getId(),
$googleUser->getEmail(),
$googleUser->getName(),
);
if (! $user || ! $user->is_active) {
return redirect()->route('login')->withErrors([
'email' => __('Ihr Konto ist nicht aktiv. Bitte wenden Sie sich an den Support.'),
]);
}
Auth::login($user, remember: true);
$request->session()->regenerate();
$user->forceFill([
'last_login_at' => now(),
'last_login_ip' => $request->ip(),
])->save();
return redirect(LoginRedirect::safeTarget(
$user,
$request->session()->pull('url.intended'),
LoginRedirect::homeFor($user),
));
}
}

View file

@ -45,6 +45,8 @@ class User extends Authenticatable implements MustVerifyEmail
'legacy_portal',
'legacy_id',
'password',
'oauth_provider',
'oauth_provider_id',
'press_release_quota_used_this_month',
];

View file

@ -0,0 +1,83 @@
<?php
namespace App\Services\Auth;
use App\Enums\RegistrationType;
use App\Models\User;
/**
* Auflösung eines Social-Logins (Google, ggf. weitere) auf einen lokalen User.
*
* Identität ist die E-Mail (De-Dup darüber, keine Dubletten). Der Provider
* bestätigt die E-Mail, daher gilt die Verifizierung über den Kanal als erfüllt
* (Entscheidung 15.06.) kein zusätzlicher E-Mail-Verifizierungsschritt.
*/
class SocialAuthService
{
public function __construct(private readonly UserRolePermissionSyncService $roleSync) {}
/**
* Liefert den (ggf. neu angelegten) User zur Social-Identität. Ein
* deaktivierter Bestands-Account wird NICHT reaktiviert der Aufrufer prüft
* danach is_active und blockiert.
*/
public function resolveUser(string $provider, string $providerId, ?string $email, ?string $name): ?User
{
$email = $email ? mb_strtolower(trim($email)) : null;
if ($email === null || $email === '') {
return null;
}
$user = User::query()->whereRaw('LOWER(email) = ?', [$email])->first();
if (! $user) {
return $this->createUser($provider, $providerId, $email, $name);
}
$user->forceFill([
'oauth_provider' => $provider,
'oauth_provider_id' => $providerId,
]);
// Noch nicht verifiziert (offener Selbst-Registrierer): der Provider
// bestätigt die E-Mail → Onboarding abschließen (aktiv + customer).
if ($user->email_verified_at === null) {
$user->forceFill([
'email_verified_at' => now(),
'is_active' => true,
])->save();
if ($user->roles()->doesntExist()) {
$this->roleSync->assignRoleAndSyncPermissions($user, 'customer');
}
return $user;
}
// Bereits verifiziert: nur Provider verknüpfen. is_active bleibt
// unangetastet (deaktivierte Accounts werden nicht reaktiviert).
$user->save();
return $user;
}
private function createUser(string $provider, string $providerId, string $email, ?string $name): User
{
$name = $name !== null ? trim($name) : '';
$user = User::create([
'name' => $name !== '' ? $name : $email,
'email' => $email,
'registration_type' => RegistrationType::Company->value,
'is_active' => true,
'oauth_provider' => $provider,
'oauth_provider_id' => $providerId,
]);
$user->forceFill(['email_verified_at' => now()])->save();
$this->roleSync->assignRoleAndSyncPermissions($user, 'customer');
return $user;
}
}