feat: passwordless registration — defer account creation to approval

Removes password from the volunteer registration form. Account
creation is now deferred to the approval step:

Backend:
- Registration creates Person without User (user_id=null)
- On approval, system finds or creates User by person.email
- New accounts get a "set password" email with activation link
- Existing accounts get a portal link email
- Added registration_source column to persons (self/organizer)
- Fuzzy name matching skipped for self-registered persons
- person.email is always source of truth for account linking

Frontend:
- Registration form no longer collects password
- Email check shows info alert with login suggestion
- New wachtwoord-instellen.vue page for account activation
- PasswordRequirements.vue component (reused on reset page)
- Success page updated with activation messaging

Tests: 837 passed (all updated for new flow)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-16 03:27:47 +02:00
parent 0221e7f6d3
commit c4a23b6763
22 changed files with 539 additions and 493 deletions

View File

@@ -16,7 +16,6 @@ use App\Models\TimeSlot;
use App\Models\User;
use Database\Seeders\RoleSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail;
use Laravel\Sanctum\Sanctum;
use Tests\TestCase;
@@ -62,7 +61,7 @@ class VolunteerRegistrationTest extends TestCase
'first_name' => 'Jan',
'last_name' => 'de Vries',
'email' => 'jan@voorbeeld.nl',
'password' => 'Wachtwoord1',
'phone' => '+31612345678',
'tshirt_size' => 'L',
'motivation' => 'Ik wil graag helpen bij dit festival!',
@@ -88,7 +87,7 @@ class VolunteerRegistrationTest extends TestCase
'first_name' => 'Sophie',
'last_name' => 'Bakker',
'email' => 'sophie@voorbeeld.nl',
'password' => 'Wachtwoord1',
]);
$response->assertStatus(201);
@@ -116,7 +115,7 @@ class VolunteerRegistrationTest extends TestCase
'first_name' => 'Pieter',
'last_name' => 'Jansen',
'email' => 'pieter@voorbeeld.nl',
'password' => 'Wachtwoord1',
]);
$response->assertStatus(201);
@@ -136,7 +135,7 @@ class VolunteerRegistrationTest extends TestCase
'first_name' => 'Fleur',
'last_name' => 'Vermeer',
'email' => 'fleur@voorbeeld.nl',
'password' => 'Wachtwoord1',
'availabilities' => [
['time_slot_id' => $this->timeSlot->id, 'preference_level' => 4],
['time_slot_id' => $timeSlot2->id, 'preference_level' => 2],
@@ -167,7 +166,7 @@ class VolunteerRegistrationTest extends TestCase
'first_name' => 'Daan',
'last_name' => 'Mulder',
'email' => 'daan@voorbeeld.nl',
'password' => 'Wachtwoord1',
'tshirt_size' => 'XL',
'motivation' => 'Ik vind festivals geweldig.',
'section_preferences' => [
@@ -198,7 +197,7 @@ class VolunteerRegistrationTest extends TestCase
'first_name' => 'Mila',
'last_name' => 'de Boer',
'email' => 'mila@voorbeeld.nl',
'password' => 'Wachtwoord1',
'date_of_birth' => '1998-05-12',
]);
@@ -216,7 +215,7 @@ class VolunteerRegistrationTest extends TestCase
'first_name' => 'Sem',
'last_name' => 'van Beek',
'email' => 'sem@voorbeeld.nl',
'password' => 'Wachtwoord1',
]);
$response->assertStatus(201);
@@ -231,7 +230,7 @@ class VolunteerRegistrationTest extends TestCase
'first_name' => 'Tijn',
'last_name' => 'Kuiper',
'email' => 'tijn@voorbeeld.nl',
'password' => 'Wachtwoord1',
'date_of_birth' => now()->addDay()->format('Y-m-d'),
]);
@@ -247,14 +246,14 @@ class VolunteerRegistrationTest extends TestCase
'first_name' => 'Anna',
'last_name' => 'Smit',
'email' => 'anna@voorbeeld.nl',
'password' => 'Wachtwoord1',
]);
$response = $this->postJson("/api/v1/events/{$this->event->id}/volunteer-register", [
'first_name' => 'Anna',
'last_name' => 'Smit',
'email' => 'anna@voorbeeld.nl',
'password' => 'Wachtwoord1',
]);
$response->assertStatus(422);
@@ -275,7 +274,7 @@ class VolunteerRegistrationTest extends TestCase
'first_name' => 'Herkan',
'last_name' => 'Poging',
'email' => 'herkan@voorbeeld.nl',
'password' => 'Wachtwoord1',
]);
$response->assertStatus(200);
@@ -297,7 +296,7 @@ class VolunteerRegistrationTest extends TestCase
'first_name' => 'Test',
'last_name' => 'Persoon',
'email' => 'test@voorbeeld.nl',
'password' => 'Wachtwoord1',
]);
$response->assertStatus(422);
@@ -309,7 +308,7 @@ class VolunteerRegistrationTest extends TestCase
'first_name' => 'Bas',
'last_name' => 'van Dijk',
'email' => 'bas@voorbeeld.nl',
'password' => 'Wachtwoord1',
'availabilities' => [
['time_slot_id' => '01JNONEXISTENT00000000000', 'preference_level' => 3],
],
@@ -481,7 +480,7 @@ class VolunteerRegistrationTest extends TestCase
'first_name' => 'Noor',
'last_name' => 'Janssen',
'email' => 'noor@voorbeeld.nl',
'password' => 'Wachtwoord1',
'field_values' => [
$selectField->slug => 'L',
$textField->slug => 'Ik ben een ervaren vrijwilliger',
@@ -523,7 +522,7 @@ class VolunteerRegistrationTest extends TestCase
'first_name' => 'Rick',
'last_name' => 'Peters',
'email' => 'rick@voorbeeld.nl',
'password' => 'Wachtwoord1',
'section_preferences' => [
['festival_section_id' => $section1->id, 'priority' => 1],
['festival_section_id' => $section2->id, 'priority' => 2],
@@ -559,7 +558,7 @@ class VolunteerRegistrationTest extends TestCase
'first_name' => 'Femke',
'last_name' => 'de Jong',
'email' => 'femke@voorbeeld.nl',
'password' => 'Wachtwoord1',
'field_values' => [
$multiselectField->slug => ['Vegetarisch', 'Glutenvrij'],
],
@@ -581,9 +580,9 @@ class VolunteerRegistrationTest extends TestCase
$this->assertEquals(['Vegetarisch', 'Glutenvrij'], $value->selected_options);
}
// ─── User Account Creation ──────────────────────────────────────────
// ─── Passwordless Registration (account deferred to approval) ─────────
public function test_new_volunteer_registration_creates_user_account(): void
public function test_registration_creates_person_without_user_account(): void
{
Mail::fake();
@@ -591,72 +590,39 @@ class VolunteerRegistrationTest extends TestCase
'first_name' => 'Nieuwe',
'last_name' => 'Vrijwilliger',
'email' => 'nieuw@voorbeeld.nl',
'password' => 'Wachtwoord1',
]);
$response->assertStatus(201);
$this->assertDatabaseHas('users', [
'email' => 'nieuw@voorbeeld.nl',
'first_name' => 'Nieuwe',
'last_name' => 'Vrijwilliger',
]);
$user = User::where('email', 'nieuw@voorbeeld.nl')->first();
$this->assertDatabaseHas('persons', [
'email' => 'nieuw@voorbeeld.nl',
'user_id' => $user->id,
'event_id' => $this->event->id,
'user_id' => null,
'registration_source' => 'self',
]);
// No user account should be created at registration time
$this->assertDatabaseMissing('users', [
'email' => 'nieuw@voorbeeld.nl',
]);
}
public function test_returning_volunteer_with_correct_password_creates_person(): void
public function test_registration_without_password_succeeds(): void
{
Mail::fake();
$existingUser = User::factory()->create([
'first_name' => 'Terug',
'last_name' => 'Keerder',
'email' => 'terug@voorbeeld.nl',
'password' => Hash::make('BestaandWw1'),
]);
$response = $this->postJson("/api/v1/events/{$this->event->id}/volunteer-register", [
'first_name' => 'Terug',
'last_name' => 'Keerder',
'email' => 'terug@voorbeeld.nl',
'password' => 'BestaandWw1',
]);
$response->assertStatus(201);
$this->assertDatabaseHas('persons', [
'email' => 'terug@voorbeeld.nl',
'user_id' => $existingUser->id,
'event_id' => $this->event->id,
]);
// Should not have created a new user
$this->assertEquals(1, User::where('email', 'terug@voorbeeld.nl')->count());
}
public function test_returning_volunteer_with_wrong_password_returns_422(): void
{
User::factory()->create([
'email' => 'bestaand@voorbeeld.nl',
'password' => Hash::make('echtgeheim'),
]);
$response = $this->postJson("/api/v1/events/{$this->event->id}/volunteer-register", [
'first_name' => 'Fout',
'first_name' => 'Zonder',
'last_name' => 'Wachtwoord',
'email' => 'bestaand@voorbeeld.nl',
'password' => 'foutwachtwoord',
'email' => 'geenww@voorbeeld.nl',
]);
$response->assertStatus(422);
$response->assertJsonValidationErrors('password');
$response->assertStatus(201);
$this->assertDatabaseHas('persons', [
'email' => 'geenww@voorbeeld.nl',
'user_id' => null,
]);
}
public function test_registration_sends_confirmation_email(): void
@@ -667,7 +633,6 @@ class VolunteerRegistrationTest extends TestCase
'first_name' => 'Mail',
'last_name' => 'Test',
'email' => 'mailtest@voorbeeld.nl',
'password' => 'Wachtwoord1',
]);
Mail::assertQueued(RegistrationConfirmationMail::class, function ($mail) {
@@ -683,7 +648,6 @@ class VolunteerRegistrationTest extends TestCase
'first_name' => 'Hoofdletter',
'last_name' => 'Email',
'email' => 'HOOFDLETTER@VOORBEELD.NL',
'password' => 'Wachtwoord1',
]);
$response->assertStatus(201);
@@ -691,22 +655,6 @@ class VolunteerRegistrationTest extends TestCase
$this->assertDatabaseHas('persons', [
'email' => 'hoofdletter@voorbeeld.nl',
]);
$this->assertDatabaseHas('users', [
'email' => 'hoofdletter@voorbeeld.nl',
]);
}
public function test_password_required_for_unauthenticated_registration(): void
{
$response = $this->postJson("/api/v1/events/{$this->event->id}/volunteer-register", [
'first_name' => 'Zonder',
'last_name' => 'Wachtwoord',
'email' => 'geenww@voorbeeld.nl',
]);
$response->assertStatus(422);
$response->assertJsonValidationErrors('password');
}
public function test_password_not_required_for_authenticated_registration(): void