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>
238 lines
9.2 KiB
PHP
238 lines
9.2 KiB
PHP
<?php
|
|
|
|
use App\Models\User;
|
|
use App\Services\Import\ImportContext;
|
|
use App\Services\Import\UserImporter;
|
|
use Database\Seeders\RolesAndPermissionsSeeder;
|
|
use Illuminate\Database\Schema\Blueprint;
|
|
use Illuminate\Support\Facades\Config;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use Tests\TestCase;
|
|
|
|
test('legacy user import creates linked profile data from sf guard user profile', function () {
|
|
/** @var TestCase $this */
|
|
$this->seed(RolesAndPermissionsSeeder::class);
|
|
|
|
Config::set('database.connections.mysql_presseecho', [
|
|
'driver' => 'sqlite',
|
|
'database' => ':memory:',
|
|
'prefix' => '',
|
|
'foreign_key_constraints' => false,
|
|
]);
|
|
|
|
DB::purge('mysql_presseecho');
|
|
|
|
Schema::connection('mysql_presseecho')->create('sf_guard_user', function (Blueprint $table): void {
|
|
$table->integer('id')->primary();
|
|
$table->string('username')->nullable();
|
|
$table->boolean('is_active')->default(true);
|
|
$table->boolean('is_super_admin')->default(false);
|
|
$table->timestamp('last_login')->nullable();
|
|
$table->string('ip_address')->nullable();
|
|
$table->timestamp('created_at')->nullable();
|
|
$table->timestamp('updated_at')->nullable();
|
|
});
|
|
|
|
Schema::connection('mysql_presseecho')->create('sf_guard_user_profile', function (Blueprint $table): void {
|
|
$table->integer('user_id')->primary();
|
|
$table->string('email');
|
|
$table->integer('salutation_id')->nullable();
|
|
$table->string('title')->nullable();
|
|
$table->string('first_name')->nullable();
|
|
$table->string('last_name')->nullable();
|
|
$table->string('address', 1000)->nullable();
|
|
$table->integer('country_id')->nullable();
|
|
$table->string('phone')->nullable();
|
|
$table->date('birthdate')->nullable();
|
|
$table->string('language')->nullable();
|
|
$table->string('backlink_url')->nullable();
|
|
$table->boolean('show_stats')->default(false);
|
|
$table->timestamp('validation_date')->nullable();
|
|
$table->timestamp('contract_date')->nullable();
|
|
$table->string('registration_type')->nullable();
|
|
$table->string('validate')->nullable();
|
|
$table->string('tax_id_number')->nullable();
|
|
$table->boolean('tax_exempt')->default(false);
|
|
$table->string('tax_exempt_reason', 1000)->nullable();
|
|
$table->boolean('disable_footer_code')->default(false);
|
|
});
|
|
|
|
Schema::connection('mysql_presseecho')->create('sf_guard_user_group', function (Blueprint $table): void {
|
|
$table->integer('user_id');
|
|
$table->integer('group_id');
|
|
});
|
|
|
|
DB::connection('mysql_presseecho')->table('sf_guard_user')->insert([
|
|
'id' => 42,
|
|
'username' => 'legacy-user',
|
|
'is_active' => true,
|
|
'is_super_admin' => false,
|
|
'last_login' => '2026-04-01 10:00:00',
|
|
'ip_address' => '127.0.0.1',
|
|
'created_at' => '2020-01-01 00:00:00',
|
|
'updated_at' => '2020-01-02 00:00:00',
|
|
]);
|
|
|
|
DB::connection('mysql_presseecho')->table('sf_guard_user_profile')->insert([
|
|
'user_id' => 42,
|
|
'email' => 'legacy-profile@example.com',
|
|
'salutation_id' => 2,
|
|
'title' => 'Dr.',
|
|
'first_name' => 'Legacy',
|
|
'last_name' => 'Profil',
|
|
'address' => 'Profilstrasse 7',
|
|
'country_id' => 177,
|
|
'phone' => '030123456',
|
|
'birthdate' => '1980-05-10',
|
|
'language' => 'de',
|
|
'backlink_url' => 'https://legacy.example.com',
|
|
'show_stats' => true,
|
|
'validation_date' => '2021-01-01 00:00:00',
|
|
'contract_date' => '2020-12-31 00:00:00',
|
|
'registration_type' => 'company',
|
|
'validate' => 'abc123',
|
|
'tax_id_number' => 'DE123456789',
|
|
'tax_exempt' => true,
|
|
'tax_exempt_reason' => 'Reverse Charge',
|
|
'disable_footer_code' => true,
|
|
]);
|
|
|
|
DB::connection('mysql_presseecho')->table('sf_guard_user_group')->insert([
|
|
'user_id' => 42,
|
|
'group_id' => 4,
|
|
]);
|
|
|
|
$result = app(UserImporter::class)->run(new ImportContext('presseecho', false, true));
|
|
|
|
expect($result->failed())->toBe(0);
|
|
expect($result->imported())->toBe(1);
|
|
|
|
$user = User::query()
|
|
->with('profile')
|
|
->where('email', 'legacy-profile@example.com')
|
|
->firstOrFail();
|
|
|
|
expect($user->profile)->not->toBeNull();
|
|
expect($user->profile?->salutation_key)->toBe('mrs');
|
|
expect($user->profile?->title)->toBe('Dr.');
|
|
expect($user->profile?->first_name)->toBe('Legacy');
|
|
expect($user->profile?->last_name)->toBe('Profil');
|
|
expect($user->profile?->address)->toBe('Profilstrasse 7');
|
|
expect($user->profile?->country_code)->toBe('DE');
|
|
expect($user->profile?->birthdate?->format('Y-m-d'))->toBe('1980-05-10');
|
|
expect($user->profile?->backlink_url)->toBe('https://legacy.example.com');
|
|
expect($user->profile?->show_stats)->toBeTrue();
|
|
expect($user->profile?->tax_id_number)->toBe('DE123456789');
|
|
expect($user->profile?->tax_exempt)->toBeTrue();
|
|
expect($user->profile?->tax_exempt_reason)->toBe('Reverse Charge');
|
|
expect($user->profile?->disable_footer_code)->toBeTrue();
|
|
});
|
|
|
|
/**
|
|
* Baut das minimale Legacy-Schema und importiert einen User in der gegebenen
|
|
* Alt-Gruppe. Dient der Rollen-Mapping-Absicherung.
|
|
*/
|
|
function importLegacyUserInGroup(int $groupId, string $email): void
|
|
{
|
|
Config::set('database.connections.mysql_presseecho', [
|
|
'driver' => 'sqlite',
|
|
'database' => ':memory:',
|
|
'prefix' => '',
|
|
'foreign_key_constraints' => false,
|
|
]);
|
|
|
|
DB::purge('mysql_presseecho');
|
|
|
|
Schema::connection('mysql_presseecho')->create('sf_guard_user', function (Blueprint $table): void {
|
|
$table->integer('id')->primary();
|
|
$table->string('username')->nullable();
|
|
$table->boolean('is_active')->default(true);
|
|
$table->boolean('is_super_admin')->default(false);
|
|
$table->timestamp('last_login')->nullable();
|
|
$table->string('ip_address')->nullable();
|
|
$table->timestamp('created_at')->nullable();
|
|
$table->timestamp('updated_at')->nullable();
|
|
});
|
|
|
|
Schema::connection('mysql_presseecho')->create('sf_guard_user_profile', function (Blueprint $table): void {
|
|
$table->integer('user_id')->primary();
|
|
$table->string('email');
|
|
$table->integer('salutation_id')->nullable();
|
|
$table->string('title')->nullable();
|
|
$table->string('first_name')->nullable();
|
|
$table->string('last_name')->nullable();
|
|
$table->string('address', 1000)->nullable();
|
|
$table->integer('country_id')->nullable();
|
|
$table->string('phone')->nullable();
|
|
$table->date('birthdate')->nullable();
|
|
$table->string('language')->nullable();
|
|
$table->string('backlink_url')->nullable();
|
|
$table->boolean('show_stats')->default(false);
|
|
$table->timestamp('validation_date')->nullable();
|
|
$table->timestamp('contract_date')->nullable();
|
|
$table->string('registration_type')->nullable();
|
|
$table->string('validate')->nullable();
|
|
$table->string('tax_id_number')->nullable();
|
|
$table->boolean('tax_exempt')->default(false);
|
|
$table->string('tax_exempt_reason', 1000)->nullable();
|
|
$table->boolean('disable_footer_code')->default(false);
|
|
});
|
|
|
|
Schema::connection('mysql_presseecho')->create('sf_guard_user_group', function (Blueprint $table): void {
|
|
$table->integer('user_id');
|
|
$table->integer('group_id');
|
|
});
|
|
|
|
DB::connection('mysql_presseecho')->table('sf_guard_user')->insert([
|
|
'id' => 77,
|
|
'username' => 'grp-user',
|
|
'is_active' => true,
|
|
'is_super_admin' => false,
|
|
'created_at' => '2020-01-01 00:00:00',
|
|
'updated_at' => '2020-01-02 00:00:00',
|
|
]);
|
|
|
|
DB::connection('mysql_presseecho')->table('sf_guard_user_profile')->insert([
|
|
'user_id' => 77,
|
|
'email' => $email,
|
|
'first_name' => 'Gruppen',
|
|
'last_name' => 'Nutzer',
|
|
'language' => 'de',
|
|
]);
|
|
|
|
DB::connection('mysql_presseecho')->table('sf_guard_user_group')->insert([
|
|
'user_id' => 77,
|
|
'group_id' => $groupId,
|
|
]);
|
|
|
|
app(UserImporter::class)->run(new ImportContext('presseecho', false, true));
|
|
}
|
|
|
|
test('legacy group 2 (self-publisher) imports as customer without admin access', function () {
|
|
/** @var TestCase $this */
|
|
$this->seed(RolesAndPermissionsSeeder::class);
|
|
|
|
importLegacyUserInGroup(2, 'legacy-group2@example.com');
|
|
|
|
$user = User::query()->where('email', 'legacy-group2@example.com')->firstOrFail();
|
|
|
|
// Alt-Gruppe 2 ("editor") darf NICHT die neue editor-Rolle (Admin-Zugriff)
|
|
// erhalten, sondern customer ("Mein Bereich").
|
|
expect($user->hasRole('customer'))->toBeTrue();
|
|
expect($user->hasRole('editor'))->toBeFalse();
|
|
expect($user->canAccessAdmin())->toBeFalse();
|
|
expect($user->canAccessCustomer())->toBeTrue();
|
|
});
|
|
|
|
test('legacy group 1 still imports as admin', function () {
|
|
/** @var TestCase $this */
|
|
$this->seed(RolesAndPermissionsSeeder::class);
|
|
|
|
importLegacyUserInGroup(1, 'legacy-group1@example.com');
|
|
|
|
$user = User::query()->where('email', 'legacy-group1@example.com')->firstOrFail();
|
|
|
|
expect($user->hasRole('admin'))->toBeTrue();
|
|
expect($user->canAccessAdmin())->toBeTrue();
|
|
});
|