@genesislcap/foundation-auth 15.14.1 → 15.14.2-FUI-2603.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dts/machine/actions.d.ts +13 -0
- package/dist/dts/machine/actions.d.ts.map +1 -1
- package/dist/dts/machine/actions.test.d.ts +2 -0
- package/dist/dts/machine/actions.test.d.ts.map +1 -0
- package/dist/dts/machine/dto.d.ts +34 -0
- package/dist/dts/machine/dto.d.ts.map +1 -0
- package/dist/dts/machine/dto.test.d.ts +2 -0
- package/dist/dts/machine/dto.test.d.ts.map +1 -0
- package/dist/dts/machine/errors.d.ts +5 -0
- package/dist/dts/machine/errors.d.ts.map +1 -1
- package/dist/dts/machine/guards.d.ts +2 -1
- package/dist/dts/machine/guards.d.ts.map +1 -1
- package/dist/dts/machine/guards.test.d.ts +2 -0
- package/dist/dts/machine/guards.test.d.ts.map +1 -0
- package/dist/dts/machine/machine.d.ts +4 -0
- package/dist/dts/machine/machine.d.ts.map +1 -1
- package/dist/dts/machine/types.d.ts +2 -0
- package/dist/dts/machine/types.d.ts.map +1 -1
- package/dist/dts/mapper/utils.d.ts +11 -0
- package/dist/dts/mapper/utils.d.ts.map +1 -1
- package/dist/dts/mapper/utils.test.d.ts +2 -0
- package/dist/dts/mapper/utils.test.d.ts.map +1 -0
- package/dist/dts/routes/base.d.ts +6 -0
- package/dist/dts/routes/base.d.ts.map +1 -1
- package/dist/dts/routes/forgot-password/forgot-password.d.ts.map +1 -1
- package/dist/dts/routes/reset-password/reset-identity.d.ts +38 -0
- package/dist/dts/routes/reset-password/reset-identity.d.ts.map +1 -0
- package/dist/dts/routes/reset-password/reset-identity.test.d.ts +2 -0
- package/dist/dts/routes/reset-password/reset-identity.test.d.ts.map +1 -0
- package/dist/dts/routes/reset-password/reset-password.d.ts +2 -0
- package/dist/dts/routes/reset-password/reset-password.d.ts.map +1 -1
- package/dist/dts/routes/reset-password/reset-password.template.d.ts.map +1 -1
- package/dist/esm/machine/actions.js +16 -6
- package/dist/esm/machine/actions.test.js +21 -0
- package/dist/esm/machine/dto.js +76 -0
- package/dist/esm/machine/dto.test.js +88 -0
- package/dist/esm/machine/errors.js +39 -8
- package/dist/esm/machine/guards.js +13 -5
- package/dist/esm/machine/guards.test.js +53 -0
- package/dist/esm/machine/machine.js +107 -45
- package/dist/esm/mapper/utils.js +16 -0
- package/dist/esm/mapper/utils.test.js +56 -0
- package/dist/esm/routes/base.js +21 -1
- package/dist/esm/routes/forgot-password/forgot-password.js +5 -0
- package/dist/esm/routes/reset-password/reset-identity.js +99 -0
- package/dist/esm/routes/reset-password/reset-identity.test.js +55 -0
- package/dist/esm/routes/reset-password/reset-password.js +22 -0
- package/dist/esm/routes/reset-password/reset-password.template.js +6 -2
- package/dist/esm/translation.json +1 -0
- package/dist/foundation-auth.api.json +175 -2
- package/dist/foundation-auth.d.ts +35 -1
- package/package.json +19 -19
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
|
|
2
|
+
import { is200Nack } from './guards';
|
|
3
|
+
import { isResetPasswordParams } from './types';
|
|
4
|
+
const Suite = createLogicSuite('auth machine password reset guards');
|
|
5
|
+
const nackDto = {
|
|
6
|
+
MESSAGE_TYPE: 'EVENT_NACK',
|
|
7
|
+
SOURCE_REF: 'ref',
|
|
8
|
+
ERROR: [
|
|
9
|
+
{
|
|
10
|
+
'@type': 'StandardError',
|
|
11
|
+
CODE: 'INTERNAL_ERROR',
|
|
12
|
+
TEXT: 'Password validation failed',
|
|
13
|
+
STATUS_CODE: '500 Internal Server Error',
|
|
14
|
+
},
|
|
15
|
+
],
|
|
16
|
+
WARNING: [],
|
|
17
|
+
};
|
|
18
|
+
Suite('is200Nack is true for FetchMachineOutput wrapping EVENT_NACK', () => {
|
|
19
|
+
assert.ok(is200Nack({
|
|
20
|
+
type: 'done.invoke.resetPasswordFetcher',
|
|
21
|
+
output: { data: nackDto, response: { ok: true } },
|
|
22
|
+
}));
|
|
23
|
+
});
|
|
24
|
+
Suite('is200Nack is true for EVENT_NACK even when HTTP is not ok', () => {
|
|
25
|
+
assert.ok(is200Nack({
|
|
26
|
+
type: 'done.invoke.resetPasswordFetcher',
|
|
27
|
+
output: { data: nackDto, response: { ok: false } },
|
|
28
|
+
}));
|
|
29
|
+
});
|
|
30
|
+
Suite('is200Nack is false for EVENT_ACK without errors', () => {
|
|
31
|
+
assert.not.ok(is200Nack({
|
|
32
|
+
type: 'done.invoke.resetPasswordFetcher',
|
|
33
|
+
output: { data: { MESSAGE_TYPE: 'EVENT_ACK', SOURCE_REF: 'ref' }, response: { ok: true } },
|
|
34
|
+
}));
|
|
35
|
+
});
|
|
36
|
+
Suite('isResetPasswordParams requires username, token and new password', () => {
|
|
37
|
+
assert.ok(isResetPasswordParams({
|
|
38
|
+
username: 'JaneDee',
|
|
39
|
+
resetToken: 'token',
|
|
40
|
+
newPassword: 'Password11*',
|
|
41
|
+
}));
|
|
42
|
+
assert.not.ok(isResetPasswordParams({
|
|
43
|
+
username: '',
|
|
44
|
+
resetToken: 'token',
|
|
45
|
+
newPassword: 'Password11*',
|
|
46
|
+
}));
|
|
47
|
+
assert.not.ok(isResetPasswordParams({
|
|
48
|
+
username: 'JaneDee',
|
|
49
|
+
resetToken: '',
|
|
50
|
+
newPassword: 'Password11*',
|
|
51
|
+
}));
|
|
52
|
+
});
|
|
53
|
+
Suite.run();
|
|
@@ -7,10 +7,12 @@ import { and, assign, createMachine, fromPromise, not, or, raise, sendTo } from
|
|
|
7
7
|
import { AuthConfig } from '../config/config';
|
|
8
8
|
import { CredentialManager, CredentialType, } from '../credential';
|
|
9
9
|
import { MESSAGE_TYPE } from '../dto';
|
|
10
|
-
import { AuthMessageMapper, createHTTPHeadersFromDTO, isLoginAckEntity, isLoginDetailsAckEntity, isLoginNackEntity, isLogoutAckEntity, isLogoutNackEntity,
|
|
10
|
+
import { AuthMessageMapper, createHTTPHeadersFromDTO, isLoginAckEntity, isLoginDetailsAckEntity, isLoginNackEntity, isLogoutAckEntity, isLogoutNackEntity, isFailedAuthDTO, } from '../mapper';
|
|
11
|
+
import { clearResetIdentity } from '../routes/reset-password/reset-identity';
|
|
11
12
|
import { AuthRouting } from '../routes/routing';
|
|
12
13
|
import { logger } from '../utils';
|
|
13
|
-
import { setError } from './actions';
|
|
14
|
+
import { clearAuthErrorState, setError } from './actions';
|
|
15
|
+
import { authDtoFromMachineEvent, isAuthNackEvent as eventHasAuthNack, isFetchFailureOutput, persistCredentials, } from './dto';
|
|
14
16
|
import { defaultAuthMachineContext, isLoginParams, isQueryParams } from './types';
|
|
15
17
|
/**
|
|
16
18
|
* Default AuthMachine.
|
|
@@ -49,6 +51,10 @@ let DefaultAuthMachine = class DefaultAuthMachine extends AbstractMachine {
|
|
|
49
51
|
actions,
|
|
50
52
|
target: '#authMachine.resetPassword',
|
|
51
53
|
},
|
|
54
|
+
dismiss: {
|
|
55
|
+
actions: ['resetContext'],
|
|
56
|
+
target: '#authMachine.idle',
|
|
57
|
+
},
|
|
52
58
|
};
|
|
53
59
|
};
|
|
54
60
|
this.dtoTransition = () => ({
|
|
@@ -122,7 +128,17 @@ let DefaultAuthMachine = class DefaultAuthMachine extends AbstractMachine {
|
|
|
122
128
|
invoke: {
|
|
123
129
|
id: 'loginDetailsFetcher',
|
|
124
130
|
src: fetchMachine.machine,
|
|
125
|
-
onDone:
|
|
131
|
+
onDone: [
|
|
132
|
+
{
|
|
133
|
+
guard: 'isUnauthorizedError',
|
|
134
|
+
target: 'loggedOut',
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
guard: 'isFetchFailureOutput',
|
|
138
|
+
target: 'error.silentLogin',
|
|
139
|
+
},
|
|
140
|
+
this.dtoTransition(),
|
|
141
|
+
],
|
|
126
142
|
onError: [
|
|
127
143
|
{
|
|
128
144
|
guard: 'isUnauthorizedError',
|
|
@@ -146,8 +162,23 @@ let DefaultAuthMachine = class DefaultAuthMachine extends AbstractMachine {
|
|
|
146
162
|
invoke: {
|
|
147
163
|
id: 'loginFetcher',
|
|
148
164
|
src: fetchMachine.machine,
|
|
149
|
-
onDone:
|
|
150
|
-
|
|
165
|
+
onDone: [
|
|
166
|
+
{
|
|
167
|
+
guard: 'isFetchFailureOutput',
|
|
168
|
+
target: 'error.login',
|
|
169
|
+
},
|
|
170
|
+
this.dtoTransition(),
|
|
171
|
+
],
|
|
172
|
+
onError: [
|
|
173
|
+
{
|
|
174
|
+
guard: 'isAuthNackEvent',
|
|
175
|
+
actions: ['setDTO'],
|
|
176
|
+
target: 'mapEntity',
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
target: 'error.login',
|
|
180
|
+
},
|
|
181
|
+
],
|
|
151
182
|
},
|
|
152
183
|
entry: [
|
|
153
184
|
sendTo('loginFetcher', ({ context }) => {
|
|
@@ -205,7 +236,13 @@ let DefaultAuthMachine = class DefaultAuthMachine extends AbstractMachine {
|
|
|
205
236
|
invoke: {
|
|
206
237
|
id: 'logoutFetcher',
|
|
207
238
|
src: fetchMachine.machine,
|
|
208
|
-
onDone:
|
|
239
|
+
onDone: [
|
|
240
|
+
{
|
|
241
|
+
guard: 'isFetchFailureOutput',
|
|
242
|
+
target: 'error.logout',
|
|
243
|
+
},
|
|
244
|
+
this.dtoTransition(),
|
|
245
|
+
],
|
|
209
246
|
onError: 'error.logout',
|
|
210
247
|
},
|
|
211
248
|
entry: [
|
|
@@ -236,7 +273,19 @@ let DefaultAuthMachine = class DefaultAuthMachine extends AbstractMachine {
|
|
|
236
273
|
invoke: {
|
|
237
274
|
id: 'forgotPasswordFetcher',
|
|
238
275
|
src: fetchMachine.machine,
|
|
239
|
-
onDone:
|
|
276
|
+
onDone: [
|
|
277
|
+
{
|
|
278
|
+
guard: 'is200NackDTO',
|
|
279
|
+
target: 'error.forgotPassword',
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
guard: 'isFetchFailureOutput',
|
|
283
|
+
target: 'error.forgotPassword',
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
target: 'feedback.forgotPassword',
|
|
287
|
+
},
|
|
288
|
+
],
|
|
240
289
|
onError: 'error.forgotPassword',
|
|
241
290
|
},
|
|
242
291
|
entry: [
|
|
@@ -261,11 +310,15 @@ let DefaultAuthMachine = class DefaultAuthMachine extends AbstractMachine {
|
|
|
261
310
|
guard: 'is200NackDTO',
|
|
262
311
|
target: 'error.changePassword',
|
|
263
312
|
},
|
|
313
|
+
{
|
|
314
|
+
guard: 'isFetchFailureOutput',
|
|
315
|
+
target: 'error.changePassword',
|
|
316
|
+
},
|
|
264
317
|
{
|
|
265
318
|
actions: [
|
|
266
319
|
({ context }) => {
|
|
267
320
|
const params = context.params;
|
|
268
|
-
this.cm.storeCredentials({
|
|
321
|
+
persistCredentials(this.cm.storeCredentials.bind(this.cm), {
|
|
269
322
|
id: params.username,
|
|
270
323
|
name: params.username,
|
|
271
324
|
password: params.newPassword,
|
|
@@ -294,19 +347,30 @@ let DefaultAuthMachine = class DefaultAuthMachine extends AbstractMachine {
|
|
|
294
347
|
invoke: {
|
|
295
348
|
id: 'resetPasswordFetcher',
|
|
296
349
|
src: fetchMachine.machine,
|
|
297
|
-
onDone:
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
350
|
+
onDone: [
|
|
351
|
+
{
|
|
352
|
+
guard: 'is200NackDTO',
|
|
353
|
+
target: 'error.resetPassword',
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
guard: 'isFetchFailureOutput',
|
|
357
|
+
target: 'error.resetPassword',
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
actions: [
|
|
361
|
+
({ context }) => {
|
|
362
|
+
const params = context.params;
|
|
363
|
+
persistCredentials(this.cm.storeCredentials.bind(this.cm), {
|
|
364
|
+
id: params.username,
|
|
365
|
+
name: params.username,
|
|
366
|
+
password: params.newPassword,
|
|
367
|
+
});
|
|
368
|
+
},
|
|
369
|
+
() => clearResetIdentity(),
|
|
370
|
+
],
|
|
371
|
+
target: 'feedback.resetPassword',
|
|
372
|
+
},
|
|
373
|
+
],
|
|
310
374
|
onError: 'error.resetPassword',
|
|
311
375
|
},
|
|
312
376
|
entry: [
|
|
@@ -449,11 +513,12 @@ let DefaultAuthMachine = class DefaultAuthMachine extends AbstractMachine {
|
|
|
449
513
|
],
|
|
450
514
|
},
|
|
451
515
|
/**
|
|
452
|
-
* Feedback states.
|
|
453
|
-
*
|
|
516
|
+
* Feedback states. Success paths auto-return after feedbackDelay.
|
|
517
|
+
* Error paths stay put so the user can retry immediately (login is handled here).
|
|
454
518
|
*/
|
|
455
519
|
feedback: {
|
|
456
520
|
initial: 'none',
|
|
521
|
+
on: this.onIdleEvents(),
|
|
457
522
|
states: {
|
|
458
523
|
none: {
|
|
459
524
|
tags: ['feedback', 'none'],
|
|
@@ -496,17 +561,6 @@ let DefaultAuthMachine = class DefaultAuthMachine extends AbstractMachine {
|
|
|
496
561
|
meta: {
|
|
497
562
|
content: 'MESSAGE_INCORRECT_CREDENTIALS',
|
|
498
563
|
},
|
|
499
|
-
after: {
|
|
500
|
-
feedbackDelay: [
|
|
501
|
-
{
|
|
502
|
-
guard: 'isMfaLoginParams',
|
|
503
|
-
target: '#authMachine.awaitingMfa',
|
|
504
|
-
},
|
|
505
|
-
{
|
|
506
|
-
target: '#authMachine.idle',
|
|
507
|
-
},
|
|
508
|
-
],
|
|
509
|
-
},
|
|
510
564
|
},
|
|
511
565
|
mfaRequired: {
|
|
512
566
|
tags: ['feedback', 'mfaRequired'],
|
|
@@ -581,9 +635,10 @@ let DefaultAuthMachine = class DefaultAuthMachine extends AbstractMachine {
|
|
|
581
635
|
params: ({ context }) => this.convertQueryParamsToLoginParams(context.params),
|
|
582
636
|
}),
|
|
583
637
|
setDTO: assign(({ event }) => {
|
|
584
|
-
|
|
638
|
+
const dto = authDtoFromMachineEvent(event);
|
|
639
|
+
if (dto) {
|
|
585
640
|
return {
|
|
586
|
-
dto
|
|
641
|
+
dto,
|
|
587
642
|
};
|
|
588
643
|
}
|
|
589
644
|
}),
|
|
@@ -616,7 +671,7 @@ let DefaultAuthMachine = class DefaultAuthMachine extends AbstractMachine {
|
|
|
616
671
|
}
|
|
617
672
|
},
|
|
618
673
|
setError,
|
|
619
|
-
resetContext: assign(() => this.startingContext),
|
|
674
|
+
resetContext: assign(({ context }) => clearAuthErrorState(context, this.startingContext)),
|
|
620
675
|
preventSilentAccess: () => __awaiter(this, void 0, void 0, function* () {
|
|
621
676
|
yield this.cm.preventSilentAccess();
|
|
622
677
|
}),
|
|
@@ -631,7 +686,7 @@ let DefaultAuthMachine = class DefaultAuthMachine extends AbstractMachine {
|
|
|
631
686
|
password: loginParams.password,
|
|
632
687
|
};
|
|
633
688
|
if (this.cm.validateUserCredentialData(data, true)) {
|
|
634
|
-
this.cm.storeCredentials(data);
|
|
689
|
+
persistCredentials(this.cm.storeCredentials.bind(this.cm), data);
|
|
635
690
|
}
|
|
636
691
|
},
|
|
637
692
|
postLoginRedirect: this.config.postLoginRedirect,
|
|
@@ -652,7 +707,16 @@ let DefaultAuthMachine = class DefaultAuthMachine extends AbstractMachine {
|
|
|
652
707
|
hasSSOToken: ({ context }) => isQueryParams(context.params) && context.params.SSO_TOKEN !== undefined,
|
|
653
708
|
hasMFACode: ({ context }) => isQueryParams(context.params) && context.params.MFA_CODE !== undefined,
|
|
654
709
|
hasWindowOpener: () => window.opener,
|
|
655
|
-
isUnauthorizedError: ({ event }) =>
|
|
710
|
+
isUnauthorizedError: ({ event }) => {
|
|
711
|
+
if (isErrorEvent(event)) {
|
|
712
|
+
return isUnauthorizedError(event.data);
|
|
713
|
+
}
|
|
714
|
+
if (isDoneInvokeEvent(event)) {
|
|
715
|
+
const output = event.output;
|
|
716
|
+
return (output === null || output === void 0 ? void 0 : output.error) ? isUnauthorizedError(output.error) : false;
|
|
717
|
+
}
|
|
718
|
+
return false;
|
|
719
|
+
},
|
|
656
720
|
isAuthenticated: ({ context }) => { var _a; return (_a = context.user) === null || _a === void 0 ? void 0 : _a.isAuthenticated; },
|
|
657
721
|
isBanned: ({ context }) => { var _a; return (_a = context.user) === null || _a === void 0 ? void 0 : _a.isBanned(); },
|
|
658
722
|
isLoginAckEntity: ({ event }) => isDoneInvokeEvent(event) ? isLoginAckEntity(event.output) : false,
|
|
@@ -675,13 +739,11 @@ let DefaultAuthMachine = class DefaultAuthMachine extends AbstractMachine {
|
|
|
675
739
|
isLogoutNackEntity: ({ event }) => isDoneInvokeEvent(event) ? isLogoutNackEntity(event.output) : false,
|
|
676
740
|
/**
|
|
677
741
|
* Required until strict http status codes are enabled on the backend by default.
|
|
742
|
+
* Treats EVENT_NACK / ERROR payloads as failure regardless of HTTP status.
|
|
678
743
|
*/
|
|
679
|
-
is200NackDTO: ({ event }) =>
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
? ((_b = (_a = event.output) === null || _a === void 0 ? void 0 : _a.response) === null || _b === void 0 ? void 0 : _b.ok) && isNackDTO(event.output.data)
|
|
683
|
-
: false;
|
|
684
|
-
},
|
|
744
|
+
is200NackDTO: ({ event }) => isFailedAuthDTO(authDtoFromMachineEvent(event)),
|
|
745
|
+
isAuthNackEvent: ({ event }) => eventHasAuthNack(event),
|
|
746
|
+
isFetchFailureOutput: ({ event }) => isFetchFailureOutput(event),
|
|
685
747
|
},
|
|
686
748
|
delays: {
|
|
687
749
|
feedbackDelay: this.config.feedbackDelay,
|
package/dist/esm/mapper/utils.js
CHANGED
|
@@ -107,6 +107,22 @@ export function isNackDTO(dto) {
|
|
|
107
107
|
var _a;
|
|
108
108
|
return (_a = dto === null || dto === void 0 ? void 0 : dto.MESSAGE_TYPE) === null || _a === void 0 ? void 0 : _a.endsWith('_NACK');
|
|
109
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* True when a Genesis payload contains at least one ERROR item.
|
|
112
|
+
* @public
|
|
113
|
+
*/
|
|
114
|
+
export function hasDTOErrors(dto) {
|
|
115
|
+
const errors = dto === null || dto === void 0 ? void 0 : dto.ERROR;
|
|
116
|
+
return Array.isArray(errors) && errors.length > 0;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* True when a Genesis payload must be treated as failure.
|
|
120
|
+
* HTTP status is intentionally ignored; MESSAGE_TYPE and ERROR are the contract.
|
|
121
|
+
* @public
|
|
122
|
+
*/
|
|
123
|
+
export function isFailedAuthDTO(dto) {
|
|
124
|
+
return isNackDTO(dto) || hasDTOErrors(dto);
|
|
125
|
+
}
|
|
110
126
|
/**
|
|
111
127
|
* isNackEntity.
|
|
112
128
|
* @param entity - Potential Nack.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { APIError } from '@genesislcap/foundation-state-machine';
|
|
2
|
+
import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
|
|
3
|
+
import { StatusCodes } from 'http-status-codes';
|
|
4
|
+
import { nackErrorFromUnknown } from '../machine/errors';
|
|
5
|
+
import { hasDTOErrors, isFailedAuthDTO, isNackDTO } from './utils';
|
|
6
|
+
const Suite = createLogicSuite('password-reset response handling');
|
|
7
|
+
const passwordValidationNack = {
|
|
8
|
+
WARNING: [],
|
|
9
|
+
ERROR: [
|
|
10
|
+
{
|
|
11
|
+
'@type': 'StandardError',
|
|
12
|
+
CODE: 'INTERNAL_ERROR',
|
|
13
|
+
TEXT: 'Password validation failed Password must be 8 or more characters in length.\nPassword must contain 1 or more special characters.',
|
|
14
|
+
STATUS_CODE: '500 Internal Server Error',
|
|
15
|
+
},
|
|
16
|
+
],
|
|
17
|
+
MESSAGE_TYPE: 'EVENT_NACK',
|
|
18
|
+
SOURCE_REF: '17605761-55cb-4b5a-a1ec-7235d3cae393',
|
|
19
|
+
};
|
|
20
|
+
const eventAck = {
|
|
21
|
+
MESSAGE_TYPE: 'EVENT_ACK',
|
|
22
|
+
SOURCE_REF: 'ack-ref',
|
|
23
|
+
};
|
|
24
|
+
Suite('detects EVENT_NACK as failure', () => {
|
|
25
|
+
assert.ok(isNackDTO(passwordValidationNack));
|
|
26
|
+
assert.ok(isFailedAuthDTO(passwordValidationNack));
|
|
27
|
+
assert.ok(hasDTOErrors(passwordValidationNack));
|
|
28
|
+
});
|
|
29
|
+
Suite('detects ERROR payload as failure even without NACK suffix', () => {
|
|
30
|
+
const ackWithErrors = Object.assign(Object.assign({}, eventAck), { ERROR: passwordValidationNack.ERROR });
|
|
31
|
+
assert.not.ok(isNackDTO(ackWithErrors));
|
|
32
|
+
assert.ok(hasDTOErrors(ackWithErrors));
|
|
33
|
+
assert.ok(isFailedAuthDTO(ackWithErrors));
|
|
34
|
+
});
|
|
35
|
+
Suite('treats EVENT_ACK without ERROR as success', () => {
|
|
36
|
+
assert.not.ok(isNackDTO(eventAck));
|
|
37
|
+
assert.not.ok(hasDTOErrors(eventAck));
|
|
38
|
+
assert.not.ok(isFailedAuthDTO(eventAck));
|
|
39
|
+
});
|
|
40
|
+
Suite('extracts strong-password error text from EVENT_NACK', () => {
|
|
41
|
+
const error = nackErrorFromUnknown(passwordValidationNack);
|
|
42
|
+
assert.ok(error);
|
|
43
|
+
assert.ok(error.message.includes('Password validation failed'));
|
|
44
|
+
assert.is(error.status, StatusCodes.INTERNAL_SERVER_ERROR);
|
|
45
|
+
});
|
|
46
|
+
Suite('extracts NACK from FetchMachineOutput and APIError wrappers', () => {
|
|
47
|
+
const fromFetch = nackErrorFromUnknown({ data: passwordValidationNack, response: { ok: true } });
|
|
48
|
+
assert.ok(fromFetch === null || fromFetch === void 0 ? void 0 : fromFetch.message.includes('Password validation failed'));
|
|
49
|
+
const fromApiError = nackErrorFromUnknown(new APIError(StatusCodes.INTERNAL_SERVER_ERROR, passwordValidationNack));
|
|
50
|
+
assert.ok(fromApiError === null || fromApiError === void 0 ? void 0 : fromApiError.message.includes('Password validation failed'));
|
|
51
|
+
});
|
|
52
|
+
Suite('does not treat EVENT_ACK as an error', () => {
|
|
53
|
+
assert.is(nackErrorFromUnknown(eventAck), null);
|
|
54
|
+
assert.is(nackErrorFromUnknown({ data: eventAck }), null);
|
|
55
|
+
});
|
|
56
|
+
Suite.run();
|
package/dist/esm/routes/base.js
CHANGED
|
@@ -53,6 +53,26 @@ export class BaseRoute extends ConfigHostElement {
|
|
|
53
53
|
super(...arguments);
|
|
54
54
|
this.onBack = () => this.routing.navigateTo('/');
|
|
55
55
|
}
|
|
56
|
+
connectedCallback() {
|
|
57
|
+
var _a, _b, _c, _d, _e;
|
|
58
|
+
super.connectedCallback();
|
|
59
|
+
(_c = (_b = (_a = this.store) === null || _a === void 0 ? void 0 : _a.formEntry) === null || _b === void 0 ? void 0 : _b.errors) === null || _c === void 0 ? void 0 : _c.clear();
|
|
60
|
+
if (this.shouldDismissOnConnect) {
|
|
61
|
+
(_e = (_d = this.store) === null || _d === void 0 ? void 0 : _d.authMachine) === null || _e === void 0 ? void 0 : _e.send({ type: 'dismiss' });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Skip dismiss while a request or MFA challenge is in flight so route
|
|
66
|
+
* (re)connect does not cancel the current auth step.
|
|
67
|
+
*/
|
|
68
|
+
get shouldDismissOnConnect() {
|
|
69
|
+
var _a;
|
|
70
|
+
const machine = (_a = this.store) === null || _a === void 0 ? void 0 : _a.authMachine;
|
|
71
|
+
if (!machine) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
return !machine.hasTag('pending') && !machine.hasTag('awaitingMfa');
|
|
75
|
+
}
|
|
56
76
|
toLocalisedText(text) {
|
|
57
77
|
return this.i18next.t(`${defaultAuthConfig.name}:${text}`, { lng: this.i18next.language });
|
|
58
78
|
}
|
|
@@ -372,7 +392,7 @@ let MessageElement = class MessageElement extends FASTElement {
|
|
|
372
392
|
const translateHelper = (metaMessage) => this.i18next.t(`${defaultAuthConfig.name}:${metaMessage}`, { lng: this.i18next.language });
|
|
373
393
|
const meta = this.store.metaMessages.map(translateHelper).join('\n');
|
|
374
394
|
const error = this.store.errorMessages.map(translateHelper).join('\n');
|
|
375
|
-
return
|
|
395
|
+
return error || meta;
|
|
376
396
|
}
|
|
377
397
|
get hasError() {
|
|
378
398
|
return this.store.errorMessages.length > 0;
|
|
@@ -3,6 +3,7 @@ import { customElement } from '@microsoft/fast-element';
|
|
|
3
3
|
import { commonStyles } from '../../styles';
|
|
4
4
|
import { logger } from '../../utils';
|
|
5
5
|
import { BaseRoute } from '../base';
|
|
6
|
+
import { persistResetIdentity } from '../reset-password/reset-identity';
|
|
6
7
|
import { ForgotPasswordTemplate as template } from './forgot-password.template';
|
|
7
8
|
/**
|
|
8
9
|
* ForgotPassword
|
|
@@ -20,6 +21,10 @@ let ForgotPassword = class ForgotPassword extends BaseRoute {
|
|
|
20
21
|
/**
|
|
21
22
|
* Example url from email: https://public-foundation.genesislab.global/reset-password?password=nc_cbGrlgCkZeJHAF043oqEidnk
|
|
22
23
|
*/
|
|
24
|
+
persistResetIdentity({
|
|
25
|
+
username: this.store.formEntry.username,
|
|
26
|
+
organisation: this.store.formEntry.organisation,
|
|
27
|
+
});
|
|
23
28
|
this.$emit('auth-forgot-password', {
|
|
24
29
|
username: this.store.formEntry.orgUsername,
|
|
25
30
|
returnUrl: this.routing.getForgotPasswordReturnUrl(),
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity captured during forgot-password so the token reset screen can
|
|
3
|
+
* submit USER_NAME without asking the user to type it again.
|
|
4
|
+
*/
|
|
5
|
+
export const RESET_IDENTITY_STORAGE_KEY = 'foundation-auth:password-reset-identity';
|
|
6
|
+
const getStorage = (storage) => {
|
|
7
|
+
if (storage) {
|
|
8
|
+
return storage;
|
|
9
|
+
}
|
|
10
|
+
try {
|
|
11
|
+
return globalThis.localStorage;
|
|
12
|
+
}
|
|
13
|
+
catch (_a) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* @public
|
|
19
|
+
*/
|
|
20
|
+
export function persistResetIdentity(identity, storage) {
|
|
21
|
+
var _a, _b;
|
|
22
|
+
if (!((_a = identity === null || identity === void 0 ? void 0 : identity.username) === null || _a === void 0 ? void 0 : _a.trim())) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const target = getStorage(storage);
|
|
26
|
+
if (!target) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
target.setItem(RESET_IDENTITY_STORAGE_KEY, JSON.stringify({
|
|
31
|
+
username: identity.username.trim(),
|
|
32
|
+
organisation: ((_b = identity.organisation) === null || _b === void 0 ? void 0 : _b.trim()) || undefined,
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
catch (_c) {
|
|
36
|
+
// Storage may be unavailable (private mode, SSR).
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* @public
|
|
41
|
+
*/
|
|
42
|
+
export function readResetIdentity(storage) {
|
|
43
|
+
var _a, _b;
|
|
44
|
+
const target = getStorage(storage);
|
|
45
|
+
if (!target) {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
const raw = target.getItem(RESET_IDENTITY_STORAGE_KEY);
|
|
50
|
+
if (!raw) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
const parsed = JSON.parse(raw);
|
|
54
|
+
if ((_a = parsed === null || parsed === void 0 ? void 0 : parsed.username) === null || _a === void 0 ? void 0 : _a.trim()) {
|
|
55
|
+
return {
|
|
56
|
+
username: parsed.username.trim(),
|
|
57
|
+
organisation: ((_b = parsed.organisation) === null || _b === void 0 ? void 0 : _b.trim()) || undefined,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch (_c) {
|
|
62
|
+
// Ignore malformed payloads.
|
|
63
|
+
}
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* @public
|
|
68
|
+
*/
|
|
69
|
+
export function clearResetIdentity(storage) {
|
|
70
|
+
const target = getStorage(storage);
|
|
71
|
+
if (!target) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
target.removeItem(RESET_IDENTITY_STORAGE_KEY);
|
|
76
|
+
}
|
|
77
|
+
catch (_a) {
|
|
78
|
+
// Storage may be unavailable.
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* @public
|
|
83
|
+
*/
|
|
84
|
+
export function resolveResetUsername(queryParams = {}, stored) {
|
|
85
|
+
return (queryParams.username ||
|
|
86
|
+
queryParams.user ||
|
|
87
|
+
queryParams.USER_NAME ||
|
|
88
|
+
(stored === null || stored === void 0 ? void 0 : stored.username) ||
|
|
89
|
+
'').trim();
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Username is required by EVENT_PASSWORD_RESET_ACTION. Hide it only when a
|
|
93
|
+
* reset token is present and the username is already known.
|
|
94
|
+
*
|
|
95
|
+
* @public
|
|
96
|
+
*/
|
|
97
|
+
export function shouldShowIdentityFields(passwordResetToken, username) {
|
|
98
|
+
return !passwordResetToken || !username.trim();
|
|
99
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
|
|
2
|
+
import { clearResetIdentity, persistResetIdentity, readResetIdentity, resolveResetUsername, RESET_IDENTITY_STORAGE_KEY, shouldShowIdentityFields, } from './reset-identity';
|
|
3
|
+
const Suite = createLogicSuite('reset-identity');
|
|
4
|
+
const createMemoryStorage = () => {
|
|
5
|
+
const map = new Map();
|
|
6
|
+
return {
|
|
7
|
+
get length() {
|
|
8
|
+
return map.size;
|
|
9
|
+
},
|
|
10
|
+
clear: () => map.clear(),
|
|
11
|
+
getItem: (key) => { var _a; return (_a = map.get(key)) !== null && _a !== void 0 ? _a : null; },
|
|
12
|
+
key: (index) => { var _a; return (_a = [...map.keys()][index]) !== null && _a !== void 0 ? _a : null; },
|
|
13
|
+
removeItem: (key) => {
|
|
14
|
+
map.delete(key);
|
|
15
|
+
},
|
|
16
|
+
setItem: (key, value) => {
|
|
17
|
+
map.set(key, value);
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
Suite('hides username when token is present and username is known', () => {
|
|
22
|
+
assert.not.ok(shouldShowIdentityFields('reset-token', 'JaneDee'));
|
|
23
|
+
});
|
|
24
|
+
Suite('shows username when token is present but username is missing', () => {
|
|
25
|
+
assert.ok(shouldShowIdentityFields('reset-token', ''));
|
|
26
|
+
assert.ok(shouldShowIdentityFields('reset-token', ' '));
|
|
27
|
+
});
|
|
28
|
+
Suite('shows username when token is absent', () => {
|
|
29
|
+
assert.ok(shouldShowIdentityFields('', 'JaneDee'));
|
|
30
|
+
assert.ok(shouldShowIdentityFields(undefined, ''));
|
|
31
|
+
});
|
|
32
|
+
Suite('resolves username from query params before stored identity', () => {
|
|
33
|
+
const stored = { username: 'stored-user' };
|
|
34
|
+
assert.is(resolveResetUsername({ username: 'query-user' }, stored), 'query-user');
|
|
35
|
+
assert.is(resolveResetUsername({ user: 'alias-user' }, stored), 'alias-user');
|
|
36
|
+
assert.is(resolveResetUsername({ USER_NAME: 'legacy-user' }, stored), 'legacy-user');
|
|
37
|
+
});
|
|
38
|
+
Suite('resolves username from stored identity when query has no username', () => {
|
|
39
|
+
assert.is(resolveResetUsername({ password: 'token' }, { username: 'stored-user' }), 'stored-user');
|
|
40
|
+
assert.is(resolveResetUsername({ password: 'token' }), '');
|
|
41
|
+
});
|
|
42
|
+
Suite('persists, reads and clears reset identity', () => {
|
|
43
|
+
const storage = createMemoryStorage();
|
|
44
|
+
persistResetIdentity({ username: ' JaneDee ', organisation: ' FBN ' }, storage);
|
|
45
|
+
assert.equal(readResetIdentity(storage), { username: 'JaneDee', organisation: 'FBN' });
|
|
46
|
+
assert.ok(storage.getItem(RESET_IDENTITY_STORAGE_KEY));
|
|
47
|
+
clearResetIdentity(storage);
|
|
48
|
+
assert.is(readResetIdentity(storage), undefined);
|
|
49
|
+
});
|
|
50
|
+
Suite('does not persist empty username', () => {
|
|
51
|
+
const storage = createMemoryStorage();
|
|
52
|
+
persistResetIdentity({ username: ' ' }, storage);
|
|
53
|
+
assert.is(readResetIdentity(storage), undefined);
|
|
54
|
+
});
|
|
55
|
+
Suite.run();
|
|
@@ -2,6 +2,7 @@ import { __awaiter, __decorate } from "tslib";
|
|
|
2
2
|
import { customElement, observable, volatile } from '@microsoft/fast-element';
|
|
3
3
|
import { commonStyles } from '../../styles';
|
|
4
4
|
import { BaseRoute } from '../base';
|
|
5
|
+
import { readResetIdentity, resolveResetUsername, shouldShowIdentityFields, } from './reset-identity';
|
|
5
6
|
import { ResetPasswordTemplate as template } from './reset-password.template';
|
|
6
7
|
/**
|
|
7
8
|
* ResetPassword
|
|
@@ -26,6 +27,21 @@ let ResetPassword = class ResetPassword extends BaseRoute {
|
|
|
26
27
|
enter(phase) {
|
|
27
28
|
this.hasPasswordExpired = !!phase.route.queryParams.expired;
|
|
28
29
|
this.passwordResetToken = phase.route.queryParams.password;
|
|
30
|
+
this.applyResetIdentity(phase.route.queryParams);
|
|
31
|
+
}
|
|
32
|
+
applyResetIdentity(queryParams) {
|
|
33
|
+
if (!this.passwordResetToken) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const stored = readResetIdentity();
|
|
37
|
+
const username = resolveResetUsername(queryParams, stored);
|
|
38
|
+
if (username) {
|
|
39
|
+
this.store.formEntry.onUsernameChanged(new CustomEvent('auth-username-changed', { detail: username }));
|
|
40
|
+
}
|
|
41
|
+
const organisation = queryParams.organisation || (stored === null || stored === void 0 ? void 0 : stored.organisation);
|
|
42
|
+
if (organisation) {
|
|
43
|
+
this.store.formEntry.onOrganisationChanged(new CustomEvent('auth-organisation-changed', { detail: organisation }));
|
|
44
|
+
}
|
|
29
45
|
}
|
|
30
46
|
onSubmit() {
|
|
31
47
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -53,6 +69,9 @@ let ResetPassword = class ResetPassword extends BaseRoute {
|
|
|
53
69
|
get headingText() {
|
|
54
70
|
return this.hasPasswordExpired ? 'You are required to change your password' : 'Change Password';
|
|
55
71
|
}
|
|
72
|
+
get showIdentityFields() {
|
|
73
|
+
return shouldShowIdentityFields(this.passwordResetToken, this.store.formEntry.username);
|
|
74
|
+
}
|
|
56
75
|
};
|
|
57
76
|
__decorate([
|
|
58
77
|
observable
|
|
@@ -63,6 +82,9 @@ __decorate([
|
|
|
63
82
|
__decorate([
|
|
64
83
|
volatile
|
|
65
84
|
], ResetPassword.prototype, "headingText", null);
|
|
85
|
+
__decorate([
|
|
86
|
+
volatile
|
|
87
|
+
], ResetPassword.prototype, "showIdentityFields", null);
|
|
66
88
|
ResetPassword = __decorate([
|
|
67
89
|
customElement({
|
|
68
90
|
name: 'foundation-auth-reset-password',
|