From c4a23b676384eee4cc38333631d05a05b1b0491f Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Thu, 16 Apr 2026 03:27:47 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20passwordless=20registration=20=E2=80=94?= =?UTF-8?q?=20defer=20account=20creation=20to=20approval?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- api/app/Enums/EmailTemplateType.php | 2 +- .../Controllers/Api/V1/PersonController.php | 45 +++- .../V1/VolunteerRegistrationController.php | 7 - .../Api/V1/VolunteerRegistrationRequest.php | 7 - api/app/Models/Person.php | 1 + api/app/Services/PersonIdentityService.php | 6 +- .../Services/VolunteerRegistrationService.php | 67 ++--- api/database/factories/PersonFactory.php | 10 + ...d_registration_source_to_persons_table.php | 24 ++ .../Api/V1/PortalRegistrationFlowTest.php | 214 ++++++++------- .../Api/V1/VolunteerRegistrationTest.php | 120 +++------ apps/portal/components.d.ts | 1 + .../components/auth/PasswordRequirements.vue | 31 +++ .../api/useVolunteerRegistration.ts | 1 - apps/portal/src/pages/login.vue | 13 +- .../portal/src/pages/register/[eventSlug].vue | 243 +++--------------- apps/portal/src/pages/register/success.vue | 38 +-- .../portal/src/pages/wachtwoord-instellen.vue | 195 ++++++++++++++ apps/portal/src/pages/wachtwoord-resetten.vue | 2 + apps/portal/src/plugins/1.router/guards.ts | 2 +- apps/portal/src/types/registration.ts | 2 - apps/portal/typed-router.d.ts | 1 + 22 files changed, 539 insertions(+), 493 deletions(-) create mode 100644 api/database/migrations/2026_04_17_100000_add_registration_source_to_persons_table.php create mode 100644 apps/portal/src/components/auth/PasswordRequirements.vue create mode 100644 apps/portal/src/pages/wachtwoord-instellen.vue diff --git a/api/app/Enums/EmailTemplateType.php b/api/app/Enums/EmailTemplateType.php index e41794be..38549e6b 100644 --- a/api/app/Enums/EmailTemplateType.php +++ b/api/app/Enums/EmailTemplateType.php @@ -57,7 +57,7 @@ enum EmailTemplateType: string self::REGISTRATION_APPROVED => [ 'subject' => 'Je registratie voor {event_name} is goedgekeurd!', 'heading' => 'Welkom aan boord!', - 'body_text' => 'Goed nieuws! Je registratie als vrijwilliger voor {event_name} is goedgekeurd. Je kunt nu inloggen op het portaal om je diensten te bekijken en te claimen.', + 'body_text' => 'Goed nieuws! Je registratie voor {event_name} is goedgekeurd. Klik op de knop hieronder om toegang te krijgen tot het portaal.', 'button_text' => 'Ga naar het portaal', ], self::REGISTRATION_REJECTED => [ diff --git a/api/app/Http/Controllers/Api/V1/PersonController.php b/api/app/Http/Controllers/Api/V1/PersonController.php index 372c8dd4..97a63e8a 100644 --- a/api/app/Http/Controllers/Api/V1/PersonController.php +++ b/api/app/Http/Controllers/Api/V1/PersonController.php @@ -23,6 +23,9 @@ use App\Services\TagSyncService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Gate; +use Illuminate\Support\Facades\Hash; +use Illuminate\Support\Facades\Password; +use Illuminate\Support\Str; use Illuminate\Validation\ValidationException; final class PersonController extends Controller @@ -166,9 +169,40 @@ final class PersonController extends Controller $person->update(['status' => 'approved']); + // Link person to user account (create if needed) using person.email as source of truth + $userCreated = false; + if ($person->email && ! $person->user_id) { + $user = User::where('email', strtolower($person->email))->first(); + + if (! $user) { + $user = User::create([ + 'first_name' => $person->first_name, + 'last_name' => $person->last_name, + 'email' => strtolower($person->email), + 'password' => Hash::make(Str::random(40)), + ]); + $userCreated = true; + } + + $person->user_id = $user->id; + $person->save(); + } + $this->tagSyncService->syncFromRegistration($person); if ($person->email) { + $portalUrl = config('app.frontend_portal_url'); + + if ($userCreated) { + // New account — send "set password" email with token + $user = User::find($person->user_id); + $token = Password::broker()->createToken($user); + $actionUrl = $portalUrl . '/wachtwoord-instellen?token=' . $token . '&email=' . urlencode($user->email); + } else { + // Existing account — send portal link + $actionUrl = $portalUrl . '/evenementen'; + } + $this->emailService->send( type: EmailTemplateType::REGISTRATION_APPROVED, recipientEmail: $person->email, @@ -177,7 +211,7 @@ final class PersonController extends Controller 'event_name' => $event->name, 'organisation_name' => $organisation->name, ], - actionUrl: config('app.frontend_portal_url'), + actionUrl: $actionUrl, organisation: $organisation, eventId: $event->id, personId: $person->id, @@ -185,6 +219,15 @@ final class PersonController extends Controller ); } + activity('registration') + ->causedBy(auth()->user()) + ->performedOn($person) + ->withProperties([ + 'user_created' => $userCreated, + 'user_email' => $person->email, + ]) + ->log('person.approved'); + return $this->success(new PersonResource($person->fresh()->load('crowdType'))); } diff --git a/api/app/Http/Controllers/Api/V1/VolunteerRegistrationController.php b/api/app/Http/Controllers/Api/V1/VolunteerRegistrationController.php index fa623953..a6244f7e 100644 --- a/api/app/Http/Controllers/Api/V1/VolunteerRegistrationController.php +++ b/api/app/Http/Controllers/Api/V1/VolunteerRegistrationController.php @@ -4,12 +4,10 @@ declare(strict_types=1); namespace App\Http\Controllers\Api\V1; -use App\Enums\IdentityMatchStatus; use App\Http\Controllers\Controller; use App\Http\Requests\Api\V1\VolunteerRegistrationRequest; use App\Http\Resources\Api\V1\PersonResource; use App\Models\Event; -use App\Models\PersonIdentityMatch; use App\Services\VolunteerRegistrationService; use Illuminate\Http\JsonResponse; @@ -31,13 +29,8 @@ final class VolunteerRegistrationController extends Controller $person->load('crowdType'); - $hasExistingAccount = PersonIdentityMatch::where('person_id', $person->id) - ->where('status', IdentityMatchStatus::PENDING) - ->exists(); - $responseData = [ 'person' => new PersonResource($person), - 'has_existing_account' => $hasExistingAccount, ]; if ($person->wasRecentlyCreated) { diff --git a/api/app/Http/Requests/Api/V1/VolunteerRegistrationRequest.php b/api/app/Http/Requests/Api/V1/VolunteerRegistrationRequest.php index 48da32ff..aa8d50d9 100644 --- a/api/app/Http/Requests/Api/V1/VolunteerRegistrationRequest.php +++ b/api/app/Http/Requests/Api/V1/VolunteerRegistrationRequest.php @@ -5,7 +5,6 @@ declare(strict_types=1); namespace App\Http\Requests\Api\V1; use Illuminate\Foundation\Http\FormRequest; -use Illuminate\Validation\Rules\Password; final class VolunteerRegistrationRequest extends FormRequest { @@ -59,12 +58,6 @@ final class VolunteerRegistrationRequest extends FormRequest 'field_values' => ['nullable', 'array'], ]; - // Password required for unauthenticated registrations - if ($user === null) { - $rules['password'] = ['required', 'string', Password::min(8)->mixedCase()->numbers()]; - $rules['password_confirmation'] = ['nullable', 'same:password']; - } - return $rules; } } diff --git a/api/app/Models/Person.php b/api/app/Models/Person.php index 557d5bdf..6e50d4a3 100644 --- a/api/app/Models/Person.php +++ b/api/app/Models/Person.php @@ -42,6 +42,7 @@ final class Person extends Model 'email', 'phone', 'status', + 'registration_source', 'is_blacklisted', 'admin_notes', 'remarks', diff --git a/api/app/Services/PersonIdentityService.php b/api/app/Services/PersonIdentityService.php index 4031d9b6..66bcbd8c 100644 --- a/api/app/Services/PersonIdentityService.php +++ b/api/app/Services/PersonIdentityService.php @@ -106,7 +106,11 @@ final class PersonIdentityService } // === Strategy 2: Fuzzy name match (only if no email match found) === - if ($matches->isEmpty() && $person->first_name && $person->last_name) { + // Skip fuzzy matching for self-registered persons — they provided their + // own email, so that's the canonical identity. Fuzzy name matching with + // a different user would be confusing. + if ($matches->isEmpty() && $person->first_name && $person->last_name + && ($person->registration_source ?? 'organizer') !== 'self') { $nameMatches = $orgUsers->filter(function (User $user) use ($person) { // Skip if same email (already handled above, or would be email match) if ($person->email && strtolower(trim($user->email)) === strtolower(trim($person->email))) { diff --git a/api/app/Services/VolunteerRegistrationService.php b/api/app/Services/VolunteerRegistrationService.php index 4839d905..3107fb56 100644 --- a/api/app/Services/VolunteerRegistrationService.php +++ b/api/app/Services/VolunteerRegistrationService.php @@ -12,7 +12,6 @@ use App\Models\Person; use App\Models\User; use App\Models\VolunteerAvailability; use Illuminate\Support\Facades\DB; -use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Mail; use Illuminate\Validation\ValidationException; @@ -43,11 +42,6 @@ final class VolunteerRegistrationService $this->checkDuplicateRegistration($festivalEvent, $email); - // Resolve or create user account for unauthenticated registrations - if ($user === null) { - $user = $this->resolveUserAccount($email, $validated); - } - $volunteerCrowdType = $this->resolveVolunteerCrowdType($event); $person = DB::transaction(function () use ($festivalEvent, $validated, $user, $email, $volunteerCrowdType): Person { @@ -63,6 +57,7 @@ final class VolunteerRegistrationService 'phone' => $validated['phone'] ?? null, 'date_of_birth' => $validated['date_of_birth'] ?? null, 'status' => PersonStatus::PENDING, + 'registration_source' => 'self', 'custom_fields' => [ 'tshirt_size' => $validated['tshirt_size'] ?? null, 'first_aid' => $validated['first_aid'] ?? false, @@ -74,9 +69,11 @@ final class VolunteerRegistrationService ] ); - // Set user_id explicitly (not mass-assignable) - $person->user_id = $user->id; - $person->save(); + // Link to authenticated user directly (they already have an account) + if ($user) { + $person->user_id = $user->id; + $person->save(); + } $this->syncAvailabilities($person, $festivalEvent, $validated['availabilities'] ?? []); @@ -94,10 +91,12 @@ final class VolunteerRegistrationService ); } - // Trigger tag sync — user_id is always known now - $this->tagSyncService->syncFromRegistration($person); + // Trigger tag sync if user_id is known + if ($person->user_id) { + $this->tagSyncService->syncFromRegistration($person); + } - $source = auth('sanctum')->check() ? 'authenticated_form' : 'public_form'; + $source = $user ? 'authenticated_form' : 'public_form'; $activityLogger = activity('volunteer_registration') ->performedOn($person) @@ -106,8 +105,11 @@ final class VolunteerRegistrationService 'event_id' => $festivalEvent->id, 'person_id' => $person->id, 'email' => $email, - ]) - ->causedBy($user); + ]); + + if ($user) { + $activityLogger->causedBy($user); + } $activityLogger->log('person.registered'); @@ -120,43 +122,6 @@ final class VolunteerRegistrationService return $person; } - /** - * Resolve or create user account for the registering email. - * - * @param array $validated - * - * @throws ValidationException - */ - private function resolveUserAccount(string $email, array $validated): User - { - $existingUser = User::where('email', $email)->first(); - - if ($existingUser !== null) { - // Returning volunteer: authenticate with provided password - if (!Hash::check($validated['password'], $existingUser->password)) { - throw ValidationException::withMessages([ - 'password' => ['Wachtwoord onjuist.'], - ]); - } - - return $existingUser; - } - - // New volunteer: create user account - try { - return User::create([ - 'first_name' => $validated['first_name'], - 'last_name' => $validated['last_name'], - 'email' => $email, - 'password' => Hash::make($validated['password']), - ]); - } catch (\Illuminate\Database\UniqueConstraintViolationException) { - throw ValidationException::withMessages([ - 'email' => ['Dit emailadres heeft al een account. Gebruik je bestaande wachtwoord.'], - ]); - } - } - private function resolveFestivalEvent(Event $event): Event { if ($event->isSubEvent()) { diff --git a/api/database/factories/PersonFactory.php b/api/database/factories/PersonFactory.php index 0678d3df..45852451 100644 --- a/api/database/factories/PersonFactory.php +++ b/api/database/factories/PersonFactory.php @@ -61,4 +61,14 @@ final class PersonFactory extends Factory { return $this->state(fn () => ['status' => 'rejected']); } + + public function selfRegistered(): static + { + return $this->state(fn () => ['registration_source' => 'self']); + } + + public function organizerCreated(): static + { + return $this->state(fn () => ['registration_source' => 'organizer']); + } } diff --git a/api/database/migrations/2026_04_17_100000_add_registration_source_to_persons_table.php b/api/database/migrations/2026_04_17_100000_add_registration_source_to_persons_table.php new file mode 100644 index 00000000..1913b614 --- /dev/null +++ b/api/database/migrations/2026_04_17_100000_add_registration_source_to_persons_table.php @@ -0,0 +1,24 @@ +string('registration_source', 20)->default('organizer')->after('status'); + }); + } + + public function down(): void + { + Schema::table('persons', function (Blueprint $table) { + $table->dropColumn('registration_source'); + }); + } +}; diff --git a/api/tests/Feature/Api/V1/PortalRegistrationFlowTest.php b/api/tests/Feature/Api/V1/PortalRegistrationFlowTest.php index c9578375..6b9983ea 100644 --- a/api/tests/Feature/Api/V1/PortalRegistrationFlowTest.php +++ b/api/tests/Feature/Api/V1/PortalRegistrationFlowTest.php @@ -16,9 +16,9 @@ use Laravel\Sanctum\Sanctum; use Tests\TestCase; /** - * End-to-end test: public registration → organizer approval → portal login → portal/me + * End-to-end test: public registration → organizer approval → account creation → portal access * - * Reproduces: "Portal shows 'registratie niet ophalen' after approved volunteer logs in" + * New flow: registration creates Person without User. Approval creates/links User. */ class PortalRegistrationFlowTest extends TestCase { @@ -43,40 +43,37 @@ class PortalRegistrationFlowTest extends TestCase 'status' => 'registration_open', ]); - // Create org admin for approval $this->orgAdmin = User::factory()->create(); $this->orgAdmin->assignRole('super_admin'); $this->organisation->users()->attach($this->orgAdmin, ['role' => 'org_admin']); } /** - * The golden path: register → approve → portal/me works. + * Golden path: register → person without user → approve → user created → portal works. */ - public function test_full_flow_register_approve_portal_me(): void + public function test_full_flow_register_approve_creates_user_and_portal_works(): void { Mail::fake(); - // ── Step 1: Volunteer registers via public form ── + // ── Step 1: Volunteer registers (no password) ── $regResponse = $this->postJson("/api/v1/events/{$this->event->id}/volunteer-register", [ 'first_name' => 'Vrijwilliger', 'last_name' => 'Test', 'email' => 'vrijwilliger@test.nl', - 'password' => 'Wachtwoord1', ]); $regResponse->assertStatus(201); - // Verify person was created with user_id $person = Person::where('email', 'vrijwilliger@test.nl')->first(); - $this->assertNotNull($person, 'Person record should exist'); - $this->assertNotNull($person->user_id, 'Person should have user_id after registration'); + $this->assertNotNull($person); + $this->assertNull($person->user_id, 'Person should NOT have user_id after registration'); $this->assertEquals('pending', $person->status); + $this->assertEquals('self', $person->registration_source); - $user = User::where('email', 'vrijwilliger@test.nl')->first(); - $this->assertNotNull($user, 'User account should exist'); - $this->assertEquals($user->id, $person->user_id, 'Person.user_id should match created user'); + // No user account should exist yet + $this->assertDatabaseMissing('users', ['email' => 'vrijwilliger@test.nl']); - // ── Step 2: Organizer approves the volunteer ─��� + // ── Step 2: Organizer approves ── Sanctum::actingAs($this->orgAdmin); $approveResponse = $this->postJson( @@ -85,38 +82,69 @@ class PortalRegistrationFlowTest extends TestCase $approveResponse->assertOk(); - // Verify person is now approved + // Approval should have created user account and linked it $person->refresh(); $this->assertEquals('approved', $person->status); - $this->assertNotNull($person->user_id, 'user_id should still be set after approval'); + $this->assertNotNull($person->user_id, 'user_id should be set after approval'); - // ── Step 3: Volunteer logs into portal ── + $user = User::where('email', 'vrijwilliger@test.nl')->first(); + $this->assertNotNull($user, 'User account should be created on approval'); + $this->assertEquals($person->user_id, $user->id); + + // ── Step 3: Volunteer accesses portal ── Sanctum::actingAs($user); - // Step 3a: GET /auth/me should return portal_events $meResponse = $this->getJson('/api/v1/auth/me'); - $meResponse->assertOk(); $meResponse->assertJsonCount(1, 'data.portal_events'); $meResponse->assertJsonPath('data.portal_events.0.event_id', $this->event->id); $meResponse->assertJsonPath('data.portal_events.0.person_status', 'approved'); - // Step 3b: GET /portal/me should return person details $portalMeResponse = $this->getJson("/api/v1/portal/me?event_id={$this->event->id}"); - $portalMeResponse->assertOk(); $portalMeResponse->assertJsonPath('data.email', 'vrijwilliger@test.nl'); $portalMeResponse->assertJsonPath('data.status', 'approved'); } /** - * Festival hierarchy: register via sub-event slug → portal/me with parent event_id. + * Approval links existing user by person.email. + */ + public function test_approve_links_existing_user_by_person_email(): void + { + Mail::fake(); + + // Pre-existing user account + $existingUser = User::factory()->create(['email' => 'bestaand@test.nl']); + + // Register with same email + $this->postJson("/api/v1/events/{$this->event->id}/volunteer-register", [ + 'first_name' => 'Bestaand', + 'last_name' => 'Lid', + 'email' => 'bestaand@test.nl', + ])->assertStatus(201); + + $person = Person::where('email', 'bestaand@test.nl')->first(); + $this->assertNull($person->user_id); + + // Approve + Sanctum::actingAs($this->orgAdmin); + $this->postJson( + "/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/persons/{$person->id}/approve" + )->assertOk(); + + // Should link to existing user, not create a new one + $person->refresh(); + $this->assertEquals($existingUser->id, $person->user_id); + $this->assertEquals(1, User::where('email', 'bestaand@test.nl')->count()); + } + + /** + * Festival hierarchy: register via sub-event, portal works with both IDs. */ public function test_full_flow_with_festival_sub_event(): void { Mail::fake(); - // Create festival hierarchy $festival = Event::factory()->festival()->create([ 'organisation_id' => $this->organisation->id, 'status' => 'registration_open', @@ -125,95 +153,93 @@ class PortalRegistrationFlowTest extends TestCase 'status' => 'registration_open', ]); - // ── Step 1: Register via sub-event route ── - $regResponse = $this->postJson("/api/v1/events/{$subEvent->id}/volunteer-register", [ + // Register via sub-event + $this->postJson("/api/v1/events/{$subEvent->id}/volunteer-register", [ 'first_name' => 'Festival', 'last_name' => 'Ganger', 'email' => 'festival@test.nl', - 'password' => 'Wachtwoord1', - ]); + ])->assertStatus(201); - $regResponse->assertStatus(201); - - // Person should be linked to parent (festival), not sub-event $person = Person::where('email', 'festival@test.nl')->first(); - $this->assertNotNull($person); $this->assertEquals($festival->id, $person->event_id, 'Person should be linked to parent event'); - $this->assertNotNull($person->user_id); - $user = User::where('email', 'festival@test.nl')->first(); - - // ── Step 2: Approve ── + // Approve Sanctum::actingAs($this->orgAdmin); $this->postJson( "/api/v1/organisations/{$this->organisation->id}/events/{$festival->id}/persons/{$person->id}/approve" )->assertOk(); - // ── Step 3: Portal access ── + $person->refresh(); + $user = User::find($person->user_id); + + // Portal access Sanctum::actingAs($user); - // portal/me with parent event ID should work $this->getJson("/api/v1/portal/me?event_id={$festival->id}") ->assertOk() ->assertJsonPath('data.email', 'festival@test.nl'); - // portal/me with sub-event ID should also work (controller resolves to parent) $this->getJson("/api/v1/portal/me?event_id={$subEvent->id}") ->assertOk() ->assertJsonPath('data.email', 'festival@test.nl'); } /** - * Returning volunteer: existing user registers for new event. + * Authenticated registration still links user_id directly. */ - public function test_returning_volunteer_portal_access(): void + public function test_authenticated_registration_links_user_directly(): void { Mail::fake(); - $existingUser = User::factory()->create([ - 'first_name' => 'Bestaand', - 'last_name' => 'Lid', - 'email' => 'bestaand@test.nl', - 'password' => \Illuminate\Support\Facades\Hash::make('BestaandWw1'), + $user = User::factory()->create([ + 'first_name' => 'Ingelogd', + 'last_name' => 'Gebruiker', + 'email' => 'ingelogd@test.nl', ]); + $this->organisation->users()->attach($user, ['role' => 'org_member']); + Sanctum::actingAs($user); - // Register for event with existing account - $regResponse = $this->postJson("/api/v1/events/{$this->event->id}/volunteer-register", [ - 'first_name' => 'Bestaand', - 'last_name' => 'Lid', - 'email' => 'bestaand@test.nl', - 'password' => 'BestaandWw1', - ]); + $this->postJson("/api/v1/events/{$this->event->id}/volunteer-register", []) + ->assertStatus(201); - $regResponse->assertStatus(201); + $person = Person::where('email', 'ingelogd@test.nl')->first(); + $this->assertEquals($user->id, $person->user_id, 'Authenticated registration should set user_id directly'); + } - $person = Person::where('email', 'bestaand@test.nl')->first(); - $this->assertEquals($existingUser->id, $person->user_id); + /** + * Approval skips account creation if person already has user_id (authenticated registration). + */ + public function test_approve_skips_account_creation_if_user_already_linked(): void + { + Mail::fake(); - // Approve + $user = User::factory()->create(['email' => 'al-gelinkt@test.nl']); + $this->organisation->users()->attach($user, ['role' => 'org_member']); + + // Authenticated registration sets user_id + Sanctum::actingAs($user); + $this->postJson("/api/v1/events/{$this->event->id}/volunteer-register", []) + ->assertStatus(201); + + $person = Person::where('email', 'al-gelinkt@test.nl')->first(); + $this->assertEquals($user->id, $person->user_id); + + // Approve — should not create another user Sanctum::actingAs($this->orgAdmin); $this->postJson( "/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/persons/{$person->id}/approve" )->assertOk(); - // Portal access - Sanctum::actingAs($existingUser); - - $this->getJson('/api/v1/auth/me') - ->assertOk() - ->assertJsonCount(1, 'data.portal_events'); - - $this->getJson("/api/v1/portal/me?event_id={$this->event->id}") - ->assertOk() - ->assertJsonPath('data.status', 'approved'); + $person->refresh(); + $this->assertEquals($user->id, $person->user_id); + $this->assertEquals(1, User::where('email', 'al-gelinkt@test.nl')->count()); } /** - * Organizer-created person (no user account) → identity match confirmed → portal/me. + * Organizer-created person → identity match confirmed → portal/me works. */ public function test_organizer_created_person_then_identity_linked(): void { - // ── Step 1: Organizer creates a person manually ── $user = User::factory()->create([ 'first_name' => 'Handmatig', 'last_name' => 'Toegevoegd', @@ -223,7 +249,7 @@ class PortalRegistrationFlowTest extends TestCase Sanctum::actingAs($this->orgAdmin); - $storeResponse = $this->postJson( + $this->postJson( "/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/persons", [ 'crowd_type_id' => $this->volunteerCrowdType->id, @@ -232,61 +258,57 @@ class PortalRegistrationFlowTest extends TestCase 'email' => 'handmatig@test.nl', 'status' => 'approved', ] - ); - - $storeResponse->assertStatus(201); + )->assertStatus(201); $person = Person::where('email', 'handmatig@test.nl')->first(); - $this->assertNotNull($person); - $this->assertNull($person->user_id, 'Organizer-created person should not have user_id'); + $this->assertNull($person->user_id); - // ── Step 2: Portal access should fail (no user_id link) ── + // Portal fails without user link Sanctum::actingAs($user); - $this->getJson("/api/v1/portal/me?event_id={$this->event->id}") ->assertStatus(404); - // ── Step 3: Confirm identity match → links user_id ── + // Confirm identity match $match = $person->pendingIdentityMatch; - $this->assertNotNull($match, 'Identity match should have been auto-detected'); + $this->assertNotNull($match); Sanctum::actingAs($this->orgAdmin); - $this->postJson( "/api/v1/organisations/{$this->organisation->id}/identity-matches/{$match->id}/confirm" )->assertOk(); - $person->refresh(); - $this->assertEquals($user->id, $person->user_id, 'user_id should be set after identity confirm'); - - // ── Step 4: Portal access should now work ── + // Portal now works Sanctum::actingAs($user); - $this->getJson("/api/v1/portal/me?event_id={$this->event->id}") ->assertOk() ->assertJsonPath('data.email', 'handmatig@test.nl'); } /** - * Pending volunteer can see their status via portal (before approval). + * Fuzzy name matching is skipped for self-registered persons. */ - public function test_pending_volunteer_can_access_portal_me(): void + public function test_fuzzy_name_match_skipped_for_self_registered(): void { Mail::fake(); + // Create a user with similar name but different email + $existingUser = User::factory()->create([ + 'first_name' => 'Jan', + 'last_name' => 'de Vries', + 'email' => 'jan.devries@other.nl', + ]); + $this->organisation->users()->attach($existingUser, ['role' => 'org_member']); + + // Self-register with same name but different email $this->postJson("/api/v1/events/{$this->event->id}/volunteer-register", [ - 'first_name' => 'Wachtend', - 'last_name' => 'Vrijwilliger', - 'email' => 'wachtend@test.nl', - 'password' => 'Wachtwoord1', + 'first_name' => 'Jan', + 'last_name' => 'de Vries', + 'email' => 'jan@voorbeeld.nl', ])->assertStatus(201); - $user = User::where('email', 'wachtend@test.nl')->first(); - Sanctum::actingAs($user); + $person = Person::where('email', 'jan@voorbeeld.nl')->first(); - // Even without approval, portal/me should work (just shows pending status) - $this->getJson("/api/v1/portal/me?event_id={$this->event->id}") - ->assertOk() - ->assertJsonPath('data.status', 'pending'); + // Should NOT have a fuzzy name match (self-registered) + $this->assertNull($person->pendingIdentityMatch); } } diff --git a/api/tests/Feature/Api/V1/VolunteerRegistrationTest.php b/api/tests/Feature/Api/V1/VolunteerRegistrationTest.php index 5d1d9dbb..79165774 100644 --- a/api/tests/Feature/Api/V1/VolunteerRegistrationTest.php +++ b/api/tests/Feature/Api/V1/VolunteerRegistrationTest.php @@ -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 diff --git a/apps/portal/components.d.ts b/apps/portal/components.d.ts index 3533a4a5..5f07ae12 100644 --- a/apps/portal/components.d.ts +++ b/apps/portal/components.d.ts @@ -43,6 +43,7 @@ declare module 'vue' { MoreBtn: typeof import('./src/@core/components/MoreBtn.vue')['default'] Notifications: typeof import('./src/@core/components/Notifications.vue')['default'] OverzichtTab: typeof import('./src/components/event/OverzichtTab.vue')['default'] + PasswordRequirements: typeof import('./src/components/auth/PasswordRequirements.vue')['default'] ProductDescriptionEditor: typeof import('./src/@core/components/ProductDescriptionEditor.vue')['default'] RoosterTab: typeof import('./src/components/event/RoosterTab.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] diff --git a/apps/portal/src/components/auth/PasswordRequirements.vue b/apps/portal/src/components/auth/PasswordRequirements.vue new file mode 100644 index 00000000..7aeb1e16 --- /dev/null +++ b/apps/portal/src/components/auth/PasswordRequirements.vue @@ -0,0 +1,31 @@ + + + diff --git a/apps/portal/src/composables/api/useVolunteerRegistration.ts b/apps/portal/src/composables/api/useVolunteerRegistration.ts index 2178799b..f73772fe 100644 --- a/apps/portal/src/composables/api/useVolunteerRegistration.ts +++ b/apps/portal/src/composables/api/useVolunteerRegistration.ts @@ -24,7 +24,6 @@ export function useRegistrationData(eventSlug: Ref) { export interface VolunteerRegistrationResponse { person: Record - has_existing_account: boolean } export function useSubmitRegistration() { diff --git a/apps/portal/src/pages/login.vue b/apps/portal/src/pages/login.vue index 53728686..337a0759 100644 --- a/apps/portal/src/pages/login.vue +++ b/apps/portal/src/pages/login.vue @@ -32,6 +32,7 @@ const errorMessage = ref('') const isSubmitting = ref(false) const passwordResetDone = computed(() => route.query.reset === '1') +const accountActivated = computed(() => route.query.activated === '1') // MFA challenge state const showMfaChallenge = ref(false) @@ -179,7 +180,17 @@ function onMfaCancelled() { + Account geactiveerd! Je kunt nu inloggen. + + + (null) const fieldFormData = ref>({}) const fieldErrors = ref>({}) -const password = ref('') -const passwordConfirmation = ref('') +// Email existence check (privacy-safe — only shows boolean) const emailExists = ref(null) const emailChecking = ref(false) const lastCheckedEmail = ref('') let checkEmailRequestSeq = 0 -const showPassword = ref(false) -const showPasswordConfirmation = ref(false) - -const passwordServerError = ref('') -const passwordConfirmationServerError = ref('') -const passwordClientError = ref('') -const passwordConfirmationClientError = ref('') - const { errors, defineField, validateField, setFieldValue, setFieldError } = useForm({ validationSchema: toTypedSchema(fullRegistrationSchema), initialValues: { @@ -138,84 +129,12 @@ watchDebounced( watch(email, val => { if (authStore.isAuthenticated) return - const t = (val ?? '').trim() if (t !== lastCheckedEmail.value) { emailExists.value = null - password.value = '' - passwordConfirmation.value = '' - passwordServerError.value = '' - passwordConfirmationServerError.value = '' - passwordClientError.value = '' - passwordConfirmationClientError.value = '' } }) -watch(() => authStore.isAuthenticated, authed => { - if (!authed) return - - emailExists.value = null - emailChecking.value = false - lastCheckedEmail.value = '' - password.value = '' - passwordConfirmation.value = '' - passwordServerError.value = '' - passwordConfirmationServerError.value = '' - passwordClientError.value = '' - passwordConfirmationClientError.value = '' -}) - -watch(password, () => { - passwordServerError.value = '' - passwordClientError.value = '' -}) - -watch(passwordConfirmation, () => { - passwordConfirmationServerError.value = '' - passwordConfirmationClientError.value = '' -}) - -function validatePasswordsForStep(): boolean { - passwordClientError.value = '' - passwordConfirmationClientError.value = '' - - if (authStore.isAuthenticated) return true - if (emailChecking.value) return false - - const em = (email.value ?? '').trim() - if (!isEmailFormatForCheck(em)) return true - - if (emailExists.value === null) return false - - if (emailExists.value === true) { - if (!password.value) { - passwordClientError.value = 'Verplicht' - - return false - } - - return true - } - - if (!password.value) { - passwordClientError.value = 'Verplicht' - - return false - } - if (password.value.length < 8) { - passwordClientError.value = 'Minimaal 8 tekens' - - return false - } - if (password.value !== passwordConfirmation.value) { - passwordConfirmationClientError.value = 'Wachtwoorden komen niet overeen' - - return false - } - - return true -} - const selectedSectionIds = ref([]) const selectedTimeSlotIds = ref([]) const timeSlotPreferences = ref>({}) @@ -440,9 +359,8 @@ async function validateCurrentStep(): Promise { const k = currentStepKind.value if (k === 'personal') { const results = await Promise.all(personalFieldKeys.map(f => validateField(f))) - if (!results.every(r => r.valid)) return false - return validatePasswordsForStep() + return results.every(r => r.valid) } if (k === 'dynamic') return validateDynamicFields() @@ -544,30 +462,14 @@ function applyServerValidationErrors(serverErrors: Record) { continue } - if (key === 'password') { - passwordServerError.value = msgs[0] ?? 'Ongeldige waarde.' - - continue - } - if (key === 'password_confirmation') { - passwordConfirmationServerError.value = msgs[0] ?? 'Ongeldige waarde.' - - continue - } if (isPersonalFieldKey(key)) setFieldError(key, msgs[0] ?? 'Ongeldige waarde.') } } -function isRegistrationStepOneServerErrorKey(key: string): boolean { - return isPersonalFieldKey(key) || key === 'password' || key === 'password_confirmation' -} - async function onSubmit() { submitError.value = null fieldErrors.value = {} - passwordServerError.value = '' - passwordConfirmationServerError.value = '' const rPersonal = await Promise.all(personalFieldKeys.map(f => validateField(f))) if (!rPersonal.every(x => x.valid)) { @@ -575,11 +477,6 @@ async function onSubmit() { return } - if (!validatePasswordsForStep()) { - currentStep.value = 0 - - return - } if (!validateDynamicFields()) { currentStep.value = 1 @@ -612,17 +509,11 @@ async function onSubmit() { availabilities, } - if (!authStore.isAuthenticated) { - payload.password = password.value - if (emailExists.value === false) - payload.password_confirmation = passwordConfirmation.value - } - if (field_values) payload.field_values = field_values if (section_preferences?.length) payload.section_preferences = section_preferences try { - const result = await submitRegistration({ + await submitRegistration({ eventId: registrationData.value.event.id, form: payload, }) @@ -644,7 +535,6 @@ async function onSubmit() { event: registrationData.value.event.name, banner: registrationData.value.event.registration_banner_url ?? '', authenticated: authStore.isAuthenticated ? '1' : '0', - hasAccount: result.has_existing_account ? '1' : '0', }, }) } @@ -657,7 +547,7 @@ async function onSubmit() { if (serverErrors) { applyServerValidationErrors(serverErrors) const keys = Object.keys(serverErrors) - if (keys.some(k => isRegistrationStepOneServerErrorKey(k))) { + if (keys.some(k => isPersonalFieldKey(k))) { currentStep.value = 0 return @@ -981,7 +871,7 @@ async function onSubmit() {

- +

Vul deze gegevens zorgvuldig in: we gebruiken ze om alle informatie over het evenement naar je te versturen. - Je e-mailadres is ook je gebruikersnaam voor Crewli, het systeem waar je straks alle informatie terugvindt.

@@ -1099,18 +988,40 @@ async function onSubmit() { /> -

+ + + + - ✓ We herkennen dit emailadres! Vul je wachtwoord in om verder te gaan. -

-

- Je maakt een nieuw account aan -

+ + +
@@ -1132,82 +1043,6 @@ async function onSubmit() { />
- -
@@ -1630,7 +1465,7 @@ async function onSubmit() { } /* - Vuetify’s .v-list uses overflow: auto, which shows scrollbars on this step when + Vuetify's .v-list uses overflow: auto, which shows scrollbars on this step when list rows are slightly wider than the card (e.g. checkbox + text + rating). */ .registration-availability-list { diff --git a/apps/portal/src/pages/register/success.vue b/apps/portal/src/pages/register/success.vue index 2791ba9a..40898336 100644 --- a/apps/portal/src/pages/register/success.vue +++ b/apps/portal/src/pages/register/success.vue @@ -16,7 +16,6 @@ const authStore = useAuthStore() const eventName = computed(() => (route.query.event as string) || 'het evenement') const bannerUrl = computed(() => (route.query.banner as string) || null) const isAuthenticated = computed(() => route.query.authenticated === '1' || authStore.isAuthenticated) -const hasExistingAccount = computed(() => route.query.hasAccount === '1' && !isAuthenticated.value) diff --git a/apps/portal/src/pages/wachtwoord-instellen.vue b/apps/portal/src/pages/wachtwoord-instellen.vue new file mode 100644 index 00000000..d1f18461 --- /dev/null +++ b/apps/portal/src/pages/wachtwoord-instellen.vue @@ -0,0 +1,195 @@ + + + + + diff --git a/apps/portal/src/pages/wachtwoord-resetten.vue b/apps/portal/src/pages/wachtwoord-resetten.vue index fc06362c..a4d248d0 100644 --- a/apps/portal/src/pages/wachtwoord-resetten.vue +++ b/apps/portal/src/pages/wachtwoord-resetten.vue @@ -3,6 +3,7 @@ import authV1BottomShape from '@images/svg/auth-v1-bottom-shape.svg?raw' import authV1TopShape from '@images/svg/auth-v1-top-shape.svg?raw' import { VNodeRenderer } from '@layouts/components/VNodeRenderer' import { themeConfig } from '@themeConfig' +import PasswordRequirements from '@/components/auth/PasswordRequirements.vue' import { apiClient } from '@/lib/axios' definePage({ @@ -120,6 +121,7 @@ async function onSubmit(): Promise { :append-inner-icon="showPassword ? 'tabler-eye-off' : 'tabler-eye'" @click:append-inner="showPassword = !showPassword" /> + diff --git a/apps/portal/src/plugins/1.router/guards.ts b/apps/portal/src/plugins/1.router/guards.ts index 4a51d0b6..ad492e66 100644 --- a/apps/portal/src/plugins/1.router/guards.ts +++ b/apps/portal/src/plugins/1.router/guards.ts @@ -2,7 +2,7 @@ import type { Router } from 'vue-router' import { useAuthStore } from '@/stores/useAuthStore' import { usePortalStore } from '@/stores/usePortalStore' -const guestOnlyPaths = ['/login', '/wachtwoord-vergeten', '/wachtwoord-resetten', '/verify-email-change'] +const guestOnlyPaths = ['/login', '/wachtwoord-vergeten', '/wachtwoord-resetten', '/wachtwoord-instellen', '/verify-email-change'] // Old dashboard routes that need backward-compat redirects const dashboardRedirects: Record = { diff --git a/apps/portal/src/types/registration.ts b/apps/portal/src/types/registration.ts index 76212d3c..9f50b526 100644 --- a/apps/portal/src/types/registration.ts +++ b/apps/portal/src/types/registration.ts @@ -79,8 +79,6 @@ export interface VolunteerRegistrationForm { date_of_birth: string email: string phone: string - password?: string - password_confirmation?: string field_values?: Record section_preferences?: SectionPreference[] availabilities: VolunteerAvailability[] diff --git a/apps/portal/typed-router.d.ts b/apps/portal/typed-router.d.ts index 0ff7002b..a89a2e1b 100644 --- a/apps/portal/typed-router.d.ts +++ b/apps/portal/typed-router.d.ts @@ -30,6 +30,7 @@ declare module 'vue-router/auto-routes' { 'volunteer-register-info': RouteRecordInfo<'volunteer-register-info', '/registreren', Record, Record>, 'portal-shifts': RouteRecordInfo<'portal-shifts', '/shifts', Record, Record>, 'verify-email-change': RouteRecordInfo<'verify-email-change', '/verify-email-change', Record, Record>, + 'set-password': RouteRecordInfo<'set-password', '/wachtwoord-instellen', Record, Record>, 'reset-password': RouteRecordInfo<'reset-password', '/wachtwoord-resetten', Record, Record>, 'forgot-password': RouteRecordInfo<'forgot-password', '/wachtwoord-vergeten', Record, Record>, }