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 @@
+
+
+
+
+
+
+ {{ req.label }}
+
+
+
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
-
+
+ Er bestaat al een account met dit e-mailadres
+
+
+
+ Als je al eerder bij een evenement betrokken bent geweest,
+ kun je inloggen om je registratie automatisch te koppelen
+ aan je bestaande account.
+
+
+ Inloggen en registreren
+
+
+ Je kunt ook gewoon doorgaan met registreren — we koppelen
+ je registratie later aan je account.
+
+
+
@@ -1132,82 +1043,6 @@ async function onSubmit() {
/>
-
-
-
-
-
- Wachtwoord *
-
-
-
-
-
- Bevestig wachtwoord *
-
-
-
-
-
-
-
-
- Wachtwoord *
-
-
-
- Wachtwoord vergeten?
-
-
-
-
@@ -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)
@@ -79,6 +78,7 @@ const hasExistingAccount = computed(() => route.query.hasAccount === '1' && !isA
Je ontvangt een e-mail zodra je aanmelding is goedgekeurd.
+ Daarin vind je een link om je account te activeren.
@@ -93,45 +93,15 @@ const hasExistingAccount = computed(() => route.query.hasAccount === '1' && !isA
- Heb je al een account? Log in
+ Terug naar startpagina
-
-
-
- Er bestaat al een account met dit e-mailadres
-
-
- Log in om je aanmelding te koppelen aan je bestaande account.
- Zo kun je straks je diensten bekijken in het portaal.
-
-
-
- tabler-login
-
- Inloggen
-
-
-
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ themeConfig.app.title }}
+
+
+
+
+
+
+
+
+ Stel je wachtwoord in
+
+
+ Welkom bij Crewli! Kies een wachtwoord om je account te activeren.
+
+
+
+
+
+ {{ errorMessage }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Account activeren
+
+
+
+
+
+
+ Terug naar inloggen
+
+
+
+
+
+
+
+
+
+
+
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>,
}