@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.
Files changed (52) hide show
  1. package/dist/dts/machine/actions.d.ts +13 -0
  2. package/dist/dts/machine/actions.d.ts.map +1 -1
  3. package/dist/dts/machine/actions.test.d.ts +2 -0
  4. package/dist/dts/machine/actions.test.d.ts.map +1 -0
  5. package/dist/dts/machine/dto.d.ts +34 -0
  6. package/dist/dts/machine/dto.d.ts.map +1 -0
  7. package/dist/dts/machine/dto.test.d.ts +2 -0
  8. package/dist/dts/machine/dto.test.d.ts.map +1 -0
  9. package/dist/dts/machine/errors.d.ts +5 -0
  10. package/dist/dts/machine/errors.d.ts.map +1 -1
  11. package/dist/dts/machine/guards.d.ts +2 -1
  12. package/dist/dts/machine/guards.d.ts.map +1 -1
  13. package/dist/dts/machine/guards.test.d.ts +2 -0
  14. package/dist/dts/machine/guards.test.d.ts.map +1 -0
  15. package/dist/dts/machine/machine.d.ts +4 -0
  16. package/dist/dts/machine/machine.d.ts.map +1 -1
  17. package/dist/dts/machine/types.d.ts +2 -0
  18. package/dist/dts/machine/types.d.ts.map +1 -1
  19. package/dist/dts/mapper/utils.d.ts +11 -0
  20. package/dist/dts/mapper/utils.d.ts.map +1 -1
  21. package/dist/dts/mapper/utils.test.d.ts +2 -0
  22. package/dist/dts/mapper/utils.test.d.ts.map +1 -0
  23. package/dist/dts/routes/base.d.ts +6 -0
  24. package/dist/dts/routes/base.d.ts.map +1 -1
  25. package/dist/dts/routes/forgot-password/forgot-password.d.ts.map +1 -1
  26. package/dist/dts/routes/reset-password/reset-identity.d.ts +38 -0
  27. package/dist/dts/routes/reset-password/reset-identity.d.ts.map +1 -0
  28. package/dist/dts/routes/reset-password/reset-identity.test.d.ts +2 -0
  29. package/dist/dts/routes/reset-password/reset-identity.test.d.ts.map +1 -0
  30. package/dist/dts/routes/reset-password/reset-password.d.ts +2 -0
  31. package/dist/dts/routes/reset-password/reset-password.d.ts.map +1 -1
  32. package/dist/dts/routes/reset-password/reset-password.template.d.ts.map +1 -1
  33. package/dist/esm/machine/actions.js +16 -6
  34. package/dist/esm/machine/actions.test.js +21 -0
  35. package/dist/esm/machine/dto.js +76 -0
  36. package/dist/esm/machine/dto.test.js +88 -0
  37. package/dist/esm/machine/errors.js +39 -8
  38. package/dist/esm/machine/guards.js +13 -5
  39. package/dist/esm/machine/guards.test.js +53 -0
  40. package/dist/esm/machine/machine.js +107 -45
  41. package/dist/esm/mapper/utils.js +16 -0
  42. package/dist/esm/mapper/utils.test.js +56 -0
  43. package/dist/esm/routes/base.js +21 -1
  44. package/dist/esm/routes/forgot-password/forgot-password.js +5 -0
  45. package/dist/esm/routes/reset-password/reset-identity.js +99 -0
  46. package/dist/esm/routes/reset-password/reset-identity.test.js +55 -0
  47. package/dist/esm/routes/reset-password/reset-password.js +22 -0
  48. package/dist/esm/routes/reset-password/reset-password.template.js +6 -2
  49. package/dist/esm/translation.json +1 -0
  50. package/dist/foundation-auth.api.json +175 -2
  51. package/dist/foundation-auth.d.ts +35 -1
  52. 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, isNackDTO, } from '../mapper';
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: this.dtoTransition(),
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: this.dtoTransition(),
150
- onError: 'error.login',
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: this.dtoTransition(),
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: 'feedback.forgotPassword',
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
- actions: [
299
- ({ context }) => {
300
- const params = context.params;
301
- this.cm.storeCredentials({
302
- id: params.username,
303
- name: params.username,
304
- password: params.newPassword,
305
- });
306
- },
307
- ],
308
- target: 'feedback.resetPassword',
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. These are transient and revert back to idle after a short delay.
453
- * These can help improve UX in when the backend nack text is lacking clarity, as with incorrect credentials.
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
- if (isDoneInvokeEvent(event)) {
638
+ const dto = authDtoFromMachineEvent(event);
639
+ if (dto) {
585
640
  return {
586
- dto: event.output.data,
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 }) => isErrorEvent(event) ? isUnauthorizedError(event.data) : false,
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
- var _a, _b;
681
- return isDoneInvokeEvent(event)
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,
@@ -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();
@@ -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 !error ? meta : `${meta} ${error}.`;
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',