WS-6: E-Mail-Verifizierung, Auth-Flow-Fixes & Legacy-Rollen-Sicherheitsfix
E-Mail-Verifizierung (Entscheidung 15.06.): - User implementiert MustVerifyEmail; Registrierung legt inaktives, rollenloses Konto an und leitet auf die Danke-/Notice-Seite; Registered-Event versendet die Verifizierungsmail. Bestätigter Link aktiviert das Konto + vergibt customer-Rolle (ActivateUserAfterVerification). Backfill-Migration setzt email_verified_at für alle Bestands-User (sonst würde die verified-Middleware ~59k aktive Legacy-User aussperren). Seeder-User verifiziert. Auth-Flow-Korrekturen: - Magic-Link-Consume: rollensicherer Redirect ohne intended() (Customer landete sonst per stale intended=/dashboard im 403-Admin-Bereich). - Guest-Redirect (bootstrap/app.php) rollen-/verifizierungsbewusst statt fix /dashboard – schließt die 403-Sackgasse auf /login und /register. - Logout auf der Notice-Seite via echtes POST-Formular statt Livewire-Action (behebt 419 beim Session-Invalidate). - Magic-Link-Anforderung über eigenes Modal mit separater E-Mail-Eingabe. - Unverifizierte Login-Versuche landen auf der Notice-Seite. Sicherheitsfix Legacy-Rollen: - UserImporter mappte Alt-Gruppe 2 (Self-Publisher) auf editor (= Admin-Zugriff). Mapping auf customer korrigiert; Daten-Migration stuft die 65.950 fälschlichen Legacy-Editoren auf customer herab. Echte admin/api-only bleiben unberührt. Tests: Registration, EmailVerification, Authentication (Guest-Redirect), MagicLinkLogin (Modal/Redirect/Regression), Legacy-Import (Gruppen-Mapping). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
c804f3bfc3
commit
94cb209a9f
18 changed files with 608 additions and 86 deletions
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Mark every pre-existing user as e-mail-verified.
|
||||
*
|
||||
* The User model now implements MustVerifyEmail and the panel routes carry
|
||||
* the `verified` middleware. Without this backfill, all already-imported
|
||||
* legacy users (verified on their original platform) would be locked out
|
||||
* the moment verification is enforced. New registrations created after this
|
||||
* migration keep `email_verified_at = null` until they confirm their e-mail.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
DB::table('users')
|
||||
->whereNull('email_verified_at')
|
||||
->update([
|
||||
'email_verified_at' => DB::raw('COALESCE(created_at, CURRENT_TIMESTAMP)'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Intentionally irreversible: we cannot tell which timestamps were
|
||||
* backfilled versus genuinely set, and re-nulling them would lock users out.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
// no-op
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Spatie\Permission\PermissionRegistrar;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Stuft fälschlich als `editor` importierte Legacy-Publisher auf `customer` herab.
|
||||
*
|
||||
* Der Legacy-Import mappte die Alt-Gruppe 2 ("editor" = Self-Publisher-Masse)
|
||||
* auf die neue Rolle `editor`, die jedoch Admin-Panel-Zugriff gewährt. Dadurch
|
||||
* konnte sich praktisch jeder Legacy-Kunde (z. B. per Magic-Link) ins Admin-
|
||||
* Panel einloggen. Diese Migration korrigiert die bestehenden Datensätze; das
|
||||
* Mapping selbst ist in UserImporter bereits auf `customer` umgestellt.
|
||||
*
|
||||
* Betroffen werden nur Legacy-User (legacy_portal gesetzt). Echtes Personal
|
||||
* mit `editor`/`admin` (manuell vergeben, ohne Legacy-Bezug) bleibt unberührt.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
$editorId = DB::table('roles')->where('name', 'editor')->value('id');
|
||||
$customerId = DB::table('roles')->where('name', 'customer')->value('id');
|
||||
|
||||
if (! $editorId || ! $customerId) {
|
||||
return;
|
||||
}
|
||||
|
||||
$morphType = (new User)->getMorphClass();
|
||||
|
||||
$legacyEditorIds = DB::table('model_has_roles as mhr')
|
||||
->join('users', 'users.id', '=', 'mhr.model_id')
|
||||
->where('mhr.role_id', $editorId)
|
||||
->where('mhr.model_type', $morphType)
|
||||
->whereNotNull('users.legacy_portal')
|
||||
->pluck('users.id');
|
||||
|
||||
if ($legacyEditorIds->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// In Blöcken arbeiten, damit kein whereIn das Placeholder-Limit sprengt.
|
||||
$legacyEditorIds->chunk(1000)->each(function ($chunk) use ($editorId, $customerId, $morphType) {
|
||||
$alreadyCustomer = DB::table('model_has_roles')
|
||||
->where('role_id', $customerId)
|
||||
->where('model_type', $morphType)
|
||||
->whereIn('model_id', $chunk)
|
||||
->pluck('model_id')
|
||||
->flip();
|
||||
|
||||
// editor-Zuweisung entfernen …
|
||||
DB::table('model_has_roles')
|
||||
->where('role_id', $editorId)
|
||||
->where('model_type', $morphType)
|
||||
->whereIn('model_id', $chunk)
|
||||
->delete();
|
||||
|
||||
// … und customer setzen, wo noch nicht vorhanden.
|
||||
$rows = $chunk
|
||||
->reject(fn ($id) => $alreadyCustomer->has($id))
|
||||
->map(fn ($id) => [
|
||||
'role_id' => $customerId,
|
||||
'model_type' => $morphType,
|
||||
'model_id' => $id,
|
||||
])
|
||||
->all();
|
||||
|
||||
if ($rows !== []) {
|
||||
DB::table('model_has_roles')->insert($rows);
|
||||
}
|
||||
});
|
||||
|
||||
app(PermissionRegistrar::class)->forgetCachedPermissions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Nicht umkehrbar: Welche customer zuvor fälschlich editor waren, lässt sich
|
||||
* nach der Korrektur nicht mehr von echten customer unterscheiden.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
// no-op
|
||||
}
|
||||
};
|
||||
|
|
@ -32,6 +32,7 @@ class DatabaseSeeder extends Seeder
|
|||
'registration_type' => RegistrationType::ExistingLegacy->value,
|
||||
'language' => 'de',
|
||||
'is_active' => true,
|
||||
'email_verified_at' => now(),
|
||||
]);
|
||||
|
||||
$rolePermissionSync->assignRoleAndSyncPermissions($adminUser, 'admin');
|
||||
|
|
@ -45,6 +46,7 @@ class DatabaseSeeder extends Seeder
|
|||
'registration_type' => RegistrationType::ExistingLegacy->value,
|
||||
'language' => 'de',
|
||||
'is_active' => true,
|
||||
'email_verified_at' => now(),
|
||||
]);
|
||||
|
||||
$rolePermissionSync->assignRoleAndSyncPermissions($testUser, 'customer');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue