Skip to content

Commit 8d10a8d

Browse files
feat: add mapped-payload persistence coverage across auth verification and password reset flows
1 parent 31fb4ba commit 8d10a8d

File tree

48 files changed

+3461
-444
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+3461
-444
lines changed

config/authkit.php

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -593,6 +593,111 @@
593593
],
594594
],
595595

596+
/**
597+
* Payload mapper configuration.
598+
*
599+
* AuthKit payload mappers translate validated request input into the
600+
* normalized payload structure consumed by package actions.
601+
*
602+
* How it works:
603+
* - Each mapper context maps to a schema context.
604+
* - If no custom mapper class is configured, AuthKit uses its internal default mapper.
605+
* - If a custom mapper class is configured, AuthKit resolves it via the container.
606+
*
607+
* Notes:
608+
* - Mapper keys intentionally mirror AuthKit's schema and validation contexts.
609+
* - Custom mapper classes must implement the AuthKit payload mapper contract.
610+
* - Mappers work with validated data and schema field keys.
611+
*/
612+
'mappers' => [
613+
614+
/**
615+
* Mapper definitions keyed by action/form context.
616+
*
617+
* Supported values:
618+
* - class : custom mapper class-string or null to use package default
619+
* - schema : schema context key used by the mapper
620+
*/
621+
'contexts' => [
622+
'login' => [
623+
'class' => null,
624+
'schema' => 'login',
625+
],
626+
'register' => [
627+
'class' => null,
628+
'schema' => 'register',
629+
],
630+
'two_factor_challenge' => [
631+
'class' => null,
632+
'schema' => 'two_factor_challenge',
633+
],
634+
'two_factor_recovery' => [
635+
'class' => null,
636+
'schema' => 'two_factor_recovery',
637+
],
638+
'two_factor_resend' => [
639+
'class' => null,
640+
'schema' => 'two_factor_resend',
641+
],
642+
'email_verification_token' => [
643+
'class' => null,
644+
'schema' => 'email_verification_token',
645+
],
646+
'email_verification_send' => [
647+
'class' => null,
648+
'schema' => 'email_verification_send',
649+
],
650+
'password_forgot' => [
651+
'class' => null,
652+
'schema' => 'password_forgot',
653+
],
654+
'password_reset' => [
655+
'class' => null,
656+
'schema' => 'password_reset',
657+
],
658+
'password_reset_token' => [
659+
'class' => null,
660+
'schema' => 'password_reset_token',
661+
],
662+
'confirm_password' => [
663+
'class' => null,
664+
'schema' => 'confirm_password',
665+
],
666+
'confirm_two_factor' => [
667+
'class' => null,
668+
'schema' => 'confirm_two_factor',
669+
],
670+
'password_update' => [
671+
'class' => null,
672+
'schema' => 'password_update',
673+
],
674+
'two_factor_enable' => [
675+
'class' => null,
676+
'schema' => null,
677+
],
678+
'two_factor_confirm' => [
679+
'class' => null,
680+
'schema' => 'two_factor_confirm',
681+
],
682+
'two_factor_disable' => [
683+
'class' => null,
684+
'schema' => 'two_factor_disable',
685+
],
686+
'two_factor_disable_recovery' => [
687+
'class' => null,
688+
'schema' => 'two_factor_disable_recovery',
689+
],
690+
'two_factor_recovery_regenerate' => [
691+
'class' => null,
692+
'schema' => 'two_factor_recovery_regenerate',
693+
],
694+
'sessions_logout_other' => [
695+
'class' => null,
696+
'schema' => null,
697+
],
698+
],
699+
],
700+
596701
/**
597702
* Form schema configuration.
598703
*

src/Actions/Auth/LoginAction.php

Lines changed: 20 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Illuminate\Contracts\Auth\StatefulGuard;
88
use Illuminate\Contracts\Auth\UserProvider;
99
use Illuminate\Support\Facades\URL;
10+
use Xul\AuthKit\Concerns\Actions\InteractsWithMappedPayload;
1011
use Xul\AuthKit\DataTransferObjects\Actions\AuthKitActionResult;
1112
use Xul\AuthKit\DataTransferObjects\Actions\Support\AuthKitError;
1213
use Xul\AuthKit\DataTransferObjects\Actions\Support\AuthKitFlowStep;
@@ -27,6 +28,7 @@
2728
*
2829
* Responsibilities:
2930
* - Resolve the configured guard and provider.
31+
* - Consume normalized mapped login payload data.
3032
* - Validate incoming credentials against the configured provider.
3133
* - Require email verification when configured and the user is unverified.
3234
* - Require two-factor authentication when enabled for the user.
@@ -42,6 +44,8 @@
4244
*/
4345
final class LoginAction
4446
{
47+
use InteractsWithMappedPayload;
48+
4549
/**
4650
* Create a new instance.
4751
*
@@ -93,11 +97,14 @@ public function handle(array $data): AuthKitActionResult
9397
);
9498
}
9599

100+
$attributes = $this->payloadAttributes($data);
101+
$options = $this->payloadOptions($data);
102+
96103
$identityField = (string) data_get(config('authkit.identity.login', []), 'field', 'email');
97104

98-
$identity = (string) ($data[$identityField] ?? '');
99-
$password = (string) ($data['password'] ?? '');
100-
$remember = (bool) ($data['remember'] ?? false);
105+
$identity = (string) ($attributes[$identityField] ?? '');
106+
$password = (string) ($attributes['password'] ?? '');
107+
$remember = (bool) ($options['remember'] ?? false);
101108

102109
if (trim($identity) === '' || $password === '') {
103110
return AuthKitActionResult::failure(
@@ -127,6 +134,16 @@ public function handle(array $data): AuthKitActionResult
127134
);
128135
}
129136

137+
/**
138+
* Intentionally persistence-aware.
139+
*
140+
* Login does not persist fields by default because the packaged mapper
141+
* marks all login fields as non-persistable. This call remains here so
142+
* the action continues to work correctly if a consumer extends the mapper
143+
* and marks additional login attributes as persistable.
144+
*/
145+
$this->persistMappedAttributesIfSupported($user, 'login', $data);
146+
130147
if ($this->shouldRequireEmailVerification($user)) {
131148
return $this->emailVerificationRequiredResult($user, $identityField, $identity);
132149
}
@@ -324,15 +341,6 @@ protected function resolveLoginSuccessRedirect(): AuthKitRedirect
324341
/**
325342
* Determine whether the user must complete email verification before login.
326343
*
327-
* Behavior:
328-
* - Returns false when email verification is disabled in configuration.
329-
* - If the user implements MustVerifyEmail, defers to hasVerifiedEmail().
330-
* - Otherwise falls back to checking the configured verification timestamp column.
331-
*
332-
* Fallback column logic:
333-
* - The column defaults to "email_verified_at".
334-
* - A null or empty value means the user is not verified.
335-
*
336344
* @param object $user
337345
* @return bool
338346
*/
@@ -347,7 +355,6 @@ protected function shouldRequireEmailVerification(object $user): bool
347355
}
348356

349357
$column = (string) config('authkit.email_verification.columns.verified_at', 'email_verified_at');
350-
351358
$verifiedAt = $user->{$column} ?? null;
352359

353360
return $verifiedAt === null || $verifiedAt === '';
@@ -356,12 +363,6 @@ protected function shouldRequireEmailVerification(object $user): bool
356363
/**
357364
* Resolve the email address associated with the authenticated identity.
358365
*
359-
* Resolution order:
360-
* - Prefer the identity field on the user model.
361-
* - Fall back to the raw identity value used during login.
362-
*
363-
* The returned value is normalized to lowercase and trimmed.
364-
*
365366
* @param object $user
366367
* @param string $identityField
367368
* @param string $identity
@@ -379,13 +380,6 @@ protected function resolveUserEmail(object $user, string $identityField, string
379380
/**
380381
* Build the signed verification URL for the link driver.
381382
*
382-
* Route parameters:
383-
* - id: user identifier
384-
* - hash: raw verification token
385-
* - email: verification email context
386-
*
387-
* The URL is temporary and expires according to the configured TTL.
388-
*
389383
* @param object $user
390384
* @param string $email
391385
* @param int $ttlMinutes

src/Actions/Auth/LogoutAction.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@
2525
* - Perform logout on the configured guard.
2626
* - Dispatch AuthKitLoggedOut after successful logout.
2727
* - Return a standardized AuthKitActionResult for all outcomes.
28+
*
29+
* Notes:
30+
* - Session invalidation and CSRF token regeneration are intentionally not handled
31+
* here because they are HTTP transport concerns and should remain in the controller.
2832
*/
2933
final class LogoutAction
3034
{
@@ -73,9 +77,6 @@ public function handle(): AuthKitActionResult
7377

7478
$guard->logout();
7579

76-
session()->invalidate();
77-
session()->regenerateToken();
78-
7980
event(new AuthKitLoggedOut(
8081
user: $user,
8182
guard: $guardName

src/Actions/Auth/RegisterAction.php

Lines changed: 18 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
use Illuminate\Contracts\Auth\Authenticatable;
66
use Illuminate\Contracts\Auth\Factory as AuthFactory;
77
use Illuminate\Contracts\Auth\UserProvider;
8-
use Illuminate\Support\Facades\Hash;
98
use Illuminate\Support\Facades\URL;
9+
use Xul\AuthKit\Concerns\Actions\InteractsWithMappedPayload;
1010
use Xul\AuthKit\DataTransferObjects\Actions\AuthKitActionResult;
1111
use Xul\AuthKit\DataTransferObjects\Actions\Support\AuthKitError;
1212
use Xul\AuthKit\DataTransferObjects\Actions\Support\AuthKitFlowStep;
@@ -23,6 +23,8 @@
2323
*
2424
* Responsibilities:
2525
* - Create a new user using the configured auth provider when possible.
26+
* - Consume normalized mapped registration payload data.
27+
* - Persist only fields explicitly marked as persistable by the mapper layer.
2628
* - Dispatch AuthKitRegistered after successful account creation.
2729
* - Initialize email verification when an email address is available.
2830
* - Dispatch AuthKitEmailVerificationRequired for external delivery handling.
@@ -38,9 +40,16 @@
3840
* - This action never returns the raw verification token to the caller.
3941
* - This action never returns the signed verification URL to the caller.
4042
* - Verification delivery remains event-driven and package-extensible.
43+
*
44+
* Expected mapped payload shape:
45+
* - attributes: persisted/business attributes such as name, email, password
46+
* - options: behavioral flags when applicable
47+
* - meta: non-persisted supporting context
4148
*/
4249
final class RegisterAction
4350
{
51+
use InteractsWithMappedPayload;
52+
4453
/**
4554
* Create a new instance.
4655
*
@@ -60,6 +69,8 @@ public function __construct(
6069
*/
6170
public function handle(array $data): AuthKitActionResult
6271
{
72+
$attributes = $this->payloadAttributes($data);
73+
6374
$user = $this->createUser($data);
6475

6576
if (! $user) {
@@ -75,7 +86,7 @@ public function handle(array $data): AuthKitActionResult
7586

7687
event(new AuthKitRegistered($user));
7788

78-
$email = mb_strtolower(trim((string) ($data['email'] ?? '')));
89+
$email = trim((string) ($attributes['email'] ?? ''));
7990

8091
if ($email === '') {
8192
return AuthKitActionResult::success(
@@ -173,6 +184,10 @@ protected function buildSignedLinkUrl(
173184
/**
174185
* Create a user using the configured auth provider when possible.
175186
*
187+
* Persistence behavior:
188+
* - Only mapper-approved persistable attributes are written.
189+
* - The model must support AuthKit mapped persistence.
190+
*
176191
* @param array<string, mixed> $data
177192
* @return Authenticatable|null
178193
*/
@@ -191,11 +206,7 @@ protected function createUser(array $data): ?Authenticatable
191206
return null;
192207
}
193208

194-
$payload = $this->userPayload($data);
195-
196-
foreach ($payload as $key => $value) {
197-
$model->{$key} = $value;
198-
}
209+
$this->persistMappedAttributesIfSupported($model, 'register', $data);
199210

200211
if (! method_exists($model, 'save')) {
201212
return null;
@@ -222,29 +233,4 @@ protected function createProviderModel(UserProvider $provider): ?object
222233

223234
return null;
224235
}
225-
226-
/**
227-
* Prepare the user payload from request data.
228-
*
229-
* @param array<string, mixed> $data
230-
* @return array<string, mixed>
231-
*/
232-
protected function userPayload(array $data): array
233-
{
234-
$payload = [];
235-
236-
if (array_key_exists('name', $data) && is_string($data['name'])) {
237-
$payload['name'] = trim($data['name']);
238-
}
239-
240-
if (array_key_exists('email', $data) && is_string($data['email'])) {
241-
$payload['email'] = mb_strtolower(trim($data['email']));
242-
}
243-
244-
if (array_key_exists('password', $data) && is_string($data['password'])) {
245-
$payload['password'] = Hash::make($data['password']);
246-
}
247-
248-
return $payload;
249-
}
250236
}

0 commit comments

Comments
 (0)