@newheap/platform-common 1.3.0 → 1.4.0
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/README.md +22 -0
- package/fesm2022/newheap-platform-common-two-factor.mjs +896 -0
- package/fesm2022/newheap-platform-common-two-factor.mjs.map +1 -0
- package/fesm2022/newheap-platform-common.mjs +867 -11
- package/fesm2022/newheap-platform-common.mjs.map +1 -1
- package/index.d.ts +401 -3
- package/package.json +18 -11
- package/two-factor/i18n/en.json +93 -0
- package/two-factor/i18n/nl.json +93 -0
- package/two-factor/index.d.ts +261 -0
|
@@ -0,0 +1,896 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { inject, DestroyRef, Injectable, Injector, viewChild, input, output, signal, computed, afterNextRender, ChangeDetectionStrategy, Component, makeEnvironmentProviders, provideEnvironmentInitializer } from '@angular/core';
|
|
3
|
+
import { DatePipe } from '@angular/common';
|
|
4
|
+
import { TranslateService, TranslatePipe } from '@ngx-translate/core';
|
|
5
|
+
import { NhTwoFactorFailureCodes, NhAuthService, NhTwoFactorMethods, NhTwoFactorVerifyModel, NhTwoFactorReauthentication, NhPasskey } from '@newheap/platform-common';
|
|
6
|
+
import { merge } from 'rxjs';
|
|
7
|
+
|
|
8
|
+
var en = {
|
|
9
|
+
"nh-two-factor": {
|
|
10
|
+
required: "Enter a code to finish signing in.",
|
|
11
|
+
"enrollment-required": "Set up two-step verification to continue.",
|
|
12
|
+
"invalid-code": "The code is not valid. Try the next code.",
|
|
13
|
+
"challenge-expired": "The sign-in expired. Sign in again.",
|
|
14
|
+
"locked-out": "Too many attempts. The account is temporarily locked.",
|
|
15
|
+
"method-not-allowed": "This method cannot be used for this account.",
|
|
16
|
+
"not-enabled": "Two-step verification is not turned on.",
|
|
17
|
+
"already-enabled": "Two-step verification is already turned on.",
|
|
18
|
+
"setup-not-started": "Start the setup first.",
|
|
19
|
+
"reauthentication-required": "Confirm your password or a code to continue.",
|
|
20
|
+
"reauthentication-failed": "The password or code is not correct.",
|
|
21
|
+
"required-by-policy": "Your organization requires two-step verification.",
|
|
22
|
+
"not-allowed-while-impersonating": "Two-step verification cannot be changed while impersonating.",
|
|
23
|
+
"configuration-invalid": "Two-step verification is not configured correctly.",
|
|
24
|
+
"email-cooldown": "Wait a moment before you request another code.",
|
|
25
|
+
"email-unavailable": "No confirmed e-mail address is available.",
|
|
26
|
+
"user-not-found": "The user was not found.",
|
|
27
|
+
"passkey-invalid": "The passkey could not be verified.",
|
|
28
|
+
"passkey-not-found": "The passkey was not found.",
|
|
29
|
+
"passkey-unavailable": "The passkey prompt was cancelled, or this browser does not support passkeys.",
|
|
30
|
+
ui: {
|
|
31
|
+
error: "Something went wrong. Try again.",
|
|
32
|
+
"challenge-title": "Two-step verification",
|
|
33
|
+
"settings-title": "Two-step verification",
|
|
34
|
+
"enrollment-title": "Set up two-step verification",
|
|
35
|
+
"enrollment-instructions": "Your organization requires a second step when you sign in. Choose how you want to confirm it is you.",
|
|
36
|
+
"method-label": "Verification method",
|
|
37
|
+
method: {
|
|
38
|
+
authenticator: "Authenticator app",
|
|
39
|
+
"recovery-code": "Recovery code",
|
|
40
|
+
email: "E-mail code",
|
|
41
|
+
passkey: "Passkey"
|
|
42
|
+
},
|
|
43
|
+
"code-label": {
|
|
44
|
+
authenticator: "Code from your authenticator app",
|
|
45
|
+
"recovery-code": "Recovery code",
|
|
46
|
+
email: "Code from the e-mail"
|
|
47
|
+
},
|
|
48
|
+
verify: "Verify",
|
|
49
|
+
verifying: "Verifying…",
|
|
50
|
+
cancel: "Cancel",
|
|
51
|
+
"remember-device": "Remember this device",
|
|
52
|
+
"send-email-code": "E-mail me a code",
|
|
53
|
+
"resend-email-code": "Send a new code",
|
|
54
|
+
"email-code-sent": "We e-mailed a code. It expires at {{time}}.",
|
|
55
|
+
"use-passkey": "Use a passkey",
|
|
56
|
+
"passkey-verify-instructions": "Your browser asks you to confirm with a passkey on this device, your phone or a security key.",
|
|
57
|
+
"sign-in-with-passkey": "Sign in with a passkey",
|
|
58
|
+
"authenticator-instructions": "Scan the QR code with your authenticator app, or enter the key by hand. Then enter the code the app shows.",
|
|
59
|
+
"shared-key": "Key",
|
|
60
|
+
"qr-code-alt": "QR code for your authenticator app",
|
|
61
|
+
confirm: "Confirm",
|
|
62
|
+
"email-instructions": "We send a code to your confirmed e-mail address.",
|
|
63
|
+
"passkey-instructions": "Create a passkey with the screen lock of this device, a password manager or a security key.",
|
|
64
|
+
"passkey-name": "Passkey name",
|
|
65
|
+
"create-passkey": "Create passkey",
|
|
66
|
+
"recovery-codes-title": "Recovery codes",
|
|
67
|
+
"recovery-codes-instructions": "Store these codes somewhere safe. Each code signs you in once when you cannot use your other methods. They are only shown now.",
|
|
68
|
+
copy: "Copy",
|
|
69
|
+
copied: "Copied",
|
|
70
|
+
download: "Download",
|
|
71
|
+
"recovery-codes-saved": "I saved these codes",
|
|
72
|
+
"continue": "Continue",
|
|
73
|
+
loading: "Loading…",
|
|
74
|
+
"status-enabled": "On",
|
|
75
|
+
"status-disabled": "Off",
|
|
76
|
+
"status-required": "Required by your organization",
|
|
77
|
+
"recovery-codes-left": "{{count}} recovery codes left",
|
|
78
|
+
"set-up-authenticator": "Set up authenticator app",
|
|
79
|
+
"change-authenticator": "Change authenticator app",
|
|
80
|
+
"set-up-email": "Use e-mail codes",
|
|
81
|
+
"passkeys-title": "Passkeys",
|
|
82
|
+
"no-passkeys": "No passkeys yet.",
|
|
83
|
+
"add-passkey": "Add passkey",
|
|
84
|
+
rename: "Rename",
|
|
85
|
+
save: "Save",
|
|
86
|
+
remove: "Remove",
|
|
87
|
+
"passkey-created": "Added on {{date}}",
|
|
88
|
+
"passkey-synced": "Synced",
|
|
89
|
+
"regenerate-recovery-codes": "Create new recovery codes",
|
|
90
|
+
"forget-devices": "Forget remembered devices",
|
|
91
|
+
disable: "Turn off two-step verification",
|
|
92
|
+
"reauthentication-title": "Confirm it is you",
|
|
93
|
+
"reauthentication-method": "Confirm with",
|
|
94
|
+
password: "Password",
|
|
95
|
+
"devices-forgotten": "Every device asks for the second step again.",
|
|
96
|
+
"session-renewed": "Your other sessions were signed out.",
|
|
97
|
+
"change-saved": "Your change was saved."
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
var nl = {
|
|
103
|
+
"nh-two-factor": {
|
|
104
|
+
required: "Voer een code in om het aanmelden af te ronden.",
|
|
105
|
+
"enrollment-required": "Stel tweestapsverificatie in om verder te gaan.",
|
|
106
|
+
"invalid-code": "De code is ongeldig. Probeer de volgende code.",
|
|
107
|
+
"challenge-expired": "De aanmelding is verlopen. Meld je opnieuw aan.",
|
|
108
|
+
"locked-out": "Te veel pogingen. Het account is tijdelijk geblokkeerd.",
|
|
109
|
+
"method-not-allowed": "Deze methode kan niet voor dit account worden gebruikt.",
|
|
110
|
+
"not-enabled": "Tweestapsverificatie staat niet aan.",
|
|
111
|
+
"already-enabled": "Tweestapsverificatie staat al aan.",
|
|
112
|
+
"setup-not-started": "Start eerst het instellen.",
|
|
113
|
+
"reauthentication-required": "Bevestig je wachtwoord of een code om door te gaan.",
|
|
114
|
+
"reauthentication-failed": "Het wachtwoord of de code klopt niet.",
|
|
115
|
+
"required-by-policy": "Je organisatie vereist tweestapsverificatie.",
|
|
116
|
+
"not-allowed-while-impersonating": "Je kunt tweestapsverificatie niet wijzigen tijdens impersonatie.",
|
|
117
|
+
"configuration-invalid": "Tweestapsverificatie is niet goed geconfigureerd.",
|
|
118
|
+
"email-cooldown": "Wacht even voordat je een nieuwe code aanvraagt.",
|
|
119
|
+
"email-unavailable": "Er is geen bevestigd e-mailadres beschikbaar.",
|
|
120
|
+
"user-not-found": "De gebruiker is niet gevonden.",
|
|
121
|
+
"passkey-invalid": "De passkey kon niet worden gecontroleerd.",
|
|
122
|
+
"passkey-not-found": "De passkey is niet gevonden.",
|
|
123
|
+
"passkey-unavailable": "De passkey-vraag is geannuleerd, of deze browser ondersteunt geen passkeys.",
|
|
124
|
+
ui: {
|
|
125
|
+
error: "Er ging iets mis. Probeer het opnieuw.",
|
|
126
|
+
"challenge-title": "Tweestapsverificatie",
|
|
127
|
+
"settings-title": "Tweestapsverificatie",
|
|
128
|
+
"enrollment-title": "Tweestapsverificatie instellen",
|
|
129
|
+
"enrollment-instructions": "Je organisatie vereist een tweede stap bij het aanmelden. Kies hoe je wilt bevestigen dat jij het bent.",
|
|
130
|
+
"method-label": "Verificatiemethode",
|
|
131
|
+
method: {
|
|
132
|
+
authenticator: "Authenticator-app",
|
|
133
|
+
"recovery-code": "Herstelcode",
|
|
134
|
+
email: "E-mailcode",
|
|
135
|
+
passkey: "Passkey"
|
|
136
|
+
},
|
|
137
|
+
"code-label": {
|
|
138
|
+
authenticator: "Code uit je authenticator-app",
|
|
139
|
+
"recovery-code": "Herstelcode",
|
|
140
|
+
email: "Code uit de e-mail"
|
|
141
|
+
},
|
|
142
|
+
verify: "Controleren",
|
|
143
|
+
verifying: "Bezig met controleren…",
|
|
144
|
+
cancel: "Annuleren",
|
|
145
|
+
"remember-device": "Dit apparaat onthouden",
|
|
146
|
+
"send-email-code": "Stuur mij een code",
|
|
147
|
+
"resend-email-code": "Stuur een nieuwe code",
|
|
148
|
+
"email-code-sent": "We hebben een code gemaild. De code verloopt om {{time}}.",
|
|
149
|
+
"use-passkey": "Passkey gebruiken",
|
|
150
|
+
"passkey-verify-instructions": "Je browser vraagt je te bevestigen met een passkey op dit apparaat, je telefoon of een beveiligingssleutel.",
|
|
151
|
+
"sign-in-with-passkey": "Aanmelden met een passkey",
|
|
152
|
+
"authenticator-instructions": "Scan de QR-code met je authenticator-app, of voer de sleutel handmatig in. Voer daarna de code in die de app toont.",
|
|
153
|
+
"shared-key": "Sleutel",
|
|
154
|
+
"qr-code-alt": "QR-code voor je authenticator-app",
|
|
155
|
+
confirm: "Bevestigen",
|
|
156
|
+
"email-instructions": "We sturen een code naar je bevestigde e-mailadres.",
|
|
157
|
+
"passkey-instructions": "Maak een passkey met de schermvergrendeling van dit apparaat, een wachtwoordmanager of een beveiligingssleutel.",
|
|
158
|
+
"passkey-name": "Naam van de passkey",
|
|
159
|
+
"create-passkey": "Passkey maken",
|
|
160
|
+
"recovery-codes-title": "Herstelcodes",
|
|
161
|
+
"recovery-codes-instructions": "Bewaar deze codes op een veilige plek. Met elke code kun je je één keer aanmelden als je je andere methodes niet kunt gebruiken. Ze worden alleen nu getoond.",
|
|
162
|
+
copy: "Kopiëren",
|
|
163
|
+
copied: "Gekopieerd",
|
|
164
|
+
download: "Downloaden",
|
|
165
|
+
"recovery-codes-saved": "Ik heb deze codes bewaard",
|
|
166
|
+
"continue": "Doorgaan",
|
|
167
|
+
loading: "Laden…",
|
|
168
|
+
"status-enabled": "Aan",
|
|
169
|
+
"status-disabled": "Uit",
|
|
170
|
+
"status-required": "Vereist door je organisatie",
|
|
171
|
+
"recovery-codes-left": "Nog {{count}} herstelcodes",
|
|
172
|
+
"set-up-authenticator": "Authenticator-app instellen",
|
|
173
|
+
"change-authenticator": "Authenticator-app wijzigen",
|
|
174
|
+
"set-up-email": "E-mailcodes gebruiken",
|
|
175
|
+
"passkeys-title": "Passkeys",
|
|
176
|
+
"no-passkeys": "Nog geen passkeys.",
|
|
177
|
+
"add-passkey": "Passkey toevoegen",
|
|
178
|
+
rename: "Naam wijzigen",
|
|
179
|
+
save: "Opslaan",
|
|
180
|
+
remove: "Verwijderen",
|
|
181
|
+
"passkey-created": "Toegevoegd op {{date}}",
|
|
182
|
+
"passkey-synced": "Gesynchroniseerd",
|
|
183
|
+
"regenerate-recovery-codes": "Nieuwe herstelcodes maken",
|
|
184
|
+
"forget-devices": "Onthouden apparaten vergeten",
|
|
185
|
+
disable: "Tweestapsverificatie uitzetten",
|
|
186
|
+
"reauthentication-title": "Bevestig dat jij het bent",
|
|
187
|
+
"reauthentication-method": "Bevestigen met",
|
|
188
|
+
password: "Wachtwoord",
|
|
189
|
+
"devices-forgotten": "Elk apparaat vraagt weer om de tweede stap.",
|
|
190
|
+
"session-renewed": "Je andere sessies zijn afgemeld.",
|
|
191
|
+
"change-saved": "Je wijziging is opgeslagen."
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
/** The entry point's own translation bundles, keyed by language. All keys live under `nh-two-factor.`. */
|
|
197
|
+
const NH_TWO_FACTOR_TRANSLATIONS = { en, nl };
|
|
198
|
+
const rootKey = 'nh-two-factor';
|
|
199
|
+
function isTree(value) {
|
|
200
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
201
|
+
}
|
|
202
|
+
/** Deep merge where values of `override` win. */
|
|
203
|
+
function mergeTrees(base, override) {
|
|
204
|
+
const result = { ...base };
|
|
205
|
+
for (const [key, value] of Object.entries(override)) {
|
|
206
|
+
const current = result[key];
|
|
207
|
+
result[key] = isTree(current) && isTree(value) ? mergeTrees(current, value) : value;
|
|
208
|
+
}
|
|
209
|
+
return result;
|
|
210
|
+
}
|
|
211
|
+
function hasAllKeys(bundle, candidate) {
|
|
212
|
+
if (!isTree(candidate)) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
return Object.entries(bundle).every(([key, value]) => isTree(value) ? hasAllKeys(value, candidate[key]) : key in candidate);
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Merges the bundled texts into `TranslateService` whenever the host loads or replaces a
|
|
219
|
+
* language. Host loaders never lose these texts, and keys the host defines under
|
|
220
|
+
* `nh-two-factor.` win over the bundle. Languages without a bundle receive the English texts.
|
|
221
|
+
*/
|
|
222
|
+
class NhTwoFactorTranslationMerger {
|
|
223
|
+
constructor() {
|
|
224
|
+
this.translate = inject(TranslateService, { optional: true });
|
|
225
|
+
this.destroyRef = inject(DestroyRef);
|
|
226
|
+
this.started = false;
|
|
227
|
+
}
|
|
228
|
+
start() {
|
|
229
|
+
const translate = this.translate;
|
|
230
|
+
if (this.started || !translate) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
this.started = true;
|
|
234
|
+
const subscription = merge(translate.onTranslationChange, translate.onLangChange).subscribe(event => {
|
|
235
|
+
if (event.lang && isTree(event.translations)) {
|
|
236
|
+
this.ensure(event.lang, event.translations[rootKey]);
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
this.destroyRef.onDestroy(() => subscription.unsubscribe());
|
|
240
|
+
// The current language is loaded or already requested by the host, so merging into it
|
|
241
|
+
// never pre-empts the host loader; a later load triggers a new merge.
|
|
242
|
+
const current = translate.getCurrentLang();
|
|
243
|
+
if (current) {
|
|
244
|
+
this.ensure(current, translate.instant(rootKey));
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
ensure(language, existing) {
|
|
248
|
+
const bundle = (NH_TWO_FACTOR_TRANSLATIONS[language] ?? NH_TWO_FACTOR_TRANSLATIONS.en);
|
|
249
|
+
const bundleRoot = bundle[rootKey];
|
|
250
|
+
if (hasAllKeys(bundleRoot, existing)) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
const merged = mergeTrees(bundleRoot, isTree(existing) ? existing : {});
|
|
254
|
+
this.translate?.setTranslation(language, { [rootKey]: merged }, true);
|
|
255
|
+
}
|
|
256
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhTwoFactorTranslationMerger, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
257
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhTwoFactorTranslationMerger, providedIn: 'root' }); }
|
|
258
|
+
}
|
|
259
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhTwoFactorTranslationMerger, decorators: [{
|
|
260
|
+
type: Injectable,
|
|
261
|
+
args: [{ providedIn: 'root' }]
|
|
262
|
+
}] });
|
|
263
|
+
|
|
264
|
+
const failureCodePrefix = 'two-factor-';
|
|
265
|
+
const translationPrefix = 'nh-two-factor.';
|
|
266
|
+
/**
|
|
267
|
+
* Returns the translation keys for a failed two-factor result. Two-factor failure codes map
|
|
268
|
+
* to `nh-two-factor.<suffix>`; other failures use a generic message.
|
|
269
|
+
*/
|
|
270
|
+
function nhTwoFactorErrorKeys(result) {
|
|
271
|
+
const keys = result.items.map(item => {
|
|
272
|
+
if (item.name.startsWith(failureCodePrefix)) {
|
|
273
|
+
return translationPrefix + item.name.substring(failureCodePrefix.length);
|
|
274
|
+
}
|
|
275
|
+
const message = item.errorMessages[0];
|
|
276
|
+
return message?.startsWith(translationPrefix) ? message : translationPrefix + 'ui.error';
|
|
277
|
+
});
|
|
278
|
+
return keys.length > 0 ? [...new Set(keys)] : [translationPrefix + 'ui.error'];
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Whether the server ended the pending sign-in step, so the user has to sign in again.
|
|
282
|
+
*/
|
|
283
|
+
function nhTwoFactorStepEnded(result) {
|
|
284
|
+
return result.items.some(item => item.name === NhTwoFactorFailureCodes.ChallengeExpired || item.name === NhTwoFactorFailureCodes.LockedOut);
|
|
285
|
+
}
|
|
286
|
+
/** Reads the value of the input that raised an event. */
|
|
287
|
+
function nhTwoFactorInputValue(event) {
|
|
288
|
+
return event.target.value;
|
|
289
|
+
}
|
|
290
|
+
/** Reads the checked state of the checkbox that raised an event. */
|
|
291
|
+
function nhTwoFactorInputChecked(event) {
|
|
292
|
+
return event.target.checked;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
let nextId$4 = 0;
|
|
296
|
+
/**
|
|
297
|
+
* Completes a second-factor challenge with a code, an e-mailed code, a recovery code or a
|
|
298
|
+
* passkey. The session is stored by the auth service before `authenticated` emits.
|
|
299
|
+
*/
|
|
300
|
+
class NhTwoFactorChallengeComponent {
|
|
301
|
+
constructor() {
|
|
302
|
+
this.auth = inject(NhAuthService);
|
|
303
|
+
this.injector = inject(Injector);
|
|
304
|
+
this.codeInput = viewChild('codeInput', ...(ngDevMode ? [{ debugName: "codeInput" }] : []));
|
|
305
|
+
/** The pending challenge from the login response. */
|
|
306
|
+
this.challenge = input.required(...(ngDevMode ? [{ debugName: "challenge" }] : []));
|
|
307
|
+
/** Whether to offer "remember this device" when the server allows it for this challenge. */
|
|
308
|
+
this.allowRememberDevice = input(true, ...(ngDevMode ? [{ debugName: "allowRememberDevice" }] : []));
|
|
309
|
+
this.authenticated = output();
|
|
310
|
+
/** The challenge expired or the account was locked; the user has to sign in again. */
|
|
311
|
+
this.expired = output();
|
|
312
|
+
this.cancelled = output();
|
|
313
|
+
this.id = `nh-two-factor-challenge-${nextId$4++}`;
|
|
314
|
+
this.passkeySupported = this.auth.isPasskeySupported();
|
|
315
|
+
this.selectedMethod = signal(null, ...(ngDevMode ? [{ debugName: "selectedMethod" }] : []));
|
|
316
|
+
this.code = signal('', ...(ngDevMode ? [{ debugName: "code" }] : []));
|
|
317
|
+
this.rememberDevice = signal(false, ...(ngDevMode ? [{ debugName: "rememberDevice" }] : []));
|
|
318
|
+
this.busy = signal(false, ...(ngDevMode ? [{ debugName: "busy" }] : []));
|
|
319
|
+
this.errorKeys = signal([], ...(ngDevMode ? [{ debugName: "errorKeys" }] : []));
|
|
320
|
+
this.emailSent = signal(null, ...(ngDevMode ? [{ debugName: "emailSent" }] : []));
|
|
321
|
+
this.methods = computed(() => this.challenge().methods
|
|
322
|
+
.filter(method => method !== NhTwoFactorMethods.passkey || this.passkeySupported), ...(ngDevMode ? [{ debugName: "methods" }] : []));
|
|
323
|
+
this.method = computed(() => this.selectedMethod() ?? this.defaultMethod(), ...(ngDevMode ? [{ debugName: "method" }] : []));
|
|
324
|
+
this.usesCode = computed(() => this.method() !== NhTwoFactorMethods.passkey, ...(ngDevMode ? [{ debugName: "usesCode" }] : []));
|
|
325
|
+
this.numericCode = computed(() => this.method() !== NhTwoFactorMethods.recoveryCode, ...(ngDevMode ? [{ debugName: "numericCode" }] : []));
|
|
326
|
+
this.offerRememberDevice = computed(() => this.allowRememberDevice() && this.challenge().rememberDeviceAvailable, ...(ngDevMode ? [{ debugName: "offerRememberDevice" }] : []));
|
|
327
|
+
this.value = nhTwoFactorInputValue;
|
|
328
|
+
this.checked = nhTwoFactorInputChecked;
|
|
329
|
+
inject(NhTwoFactorTranslationMerger).start();
|
|
330
|
+
}
|
|
331
|
+
selectMethod(method) {
|
|
332
|
+
this.selectedMethod.set(method);
|
|
333
|
+
this.code.set('');
|
|
334
|
+
this.errorKeys.set([]);
|
|
335
|
+
afterNextRender(() => this.codeInput()?.nativeElement.focus(), { injector: this.injector });
|
|
336
|
+
}
|
|
337
|
+
async verify() {
|
|
338
|
+
if (this.busy()) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
if (!this.usesCode()) {
|
|
342
|
+
await this.run(() => this.auth.verifyTwoFactorWithPasskey(this.challenge().challengeToken, this.rememberDevice()));
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
const code = this.code().trim();
|
|
346
|
+
if (!code) {
|
|
347
|
+
this.errorKeys.set(['nh-two-factor.invalid-code']);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
await this.run(() => this.auth.verifyTwoFactor(new NhTwoFactorVerifyModel({
|
|
351
|
+
challengeToken: this.challenge().challengeToken,
|
|
352
|
+
method: this.method(),
|
|
353
|
+
code: code,
|
|
354
|
+
rememberDevice: this.rememberDevice()
|
|
355
|
+
})));
|
|
356
|
+
}
|
|
357
|
+
async sendEmailCode() {
|
|
358
|
+
if (this.busy()) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
this.busy.set(true);
|
|
362
|
+
this.errorKeys.set([]);
|
|
363
|
+
const result = await this.auth.sendTwoFactorEmailCode(this.challenge().challengeToken);
|
|
364
|
+
this.busy.set(false);
|
|
365
|
+
if (!result.isSuccess) {
|
|
366
|
+
this.fail(result);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
this.emailSent.set(result.data ?? null);
|
|
370
|
+
afterNextRender(() => this.codeInput()?.nativeElement.focus(), { injector: this.injector });
|
|
371
|
+
}
|
|
372
|
+
async run(verification) {
|
|
373
|
+
this.busy.set(true);
|
|
374
|
+
this.errorKeys.set([]);
|
|
375
|
+
const result = await verification();
|
|
376
|
+
this.busy.set(false);
|
|
377
|
+
if (!result.isSuccess || !result.data) {
|
|
378
|
+
this.code.set('');
|
|
379
|
+
this.fail(result);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
this.authenticated.emit(result.data);
|
|
383
|
+
}
|
|
384
|
+
fail(result) {
|
|
385
|
+
this.errorKeys.set(nhTwoFactorErrorKeys(result));
|
|
386
|
+
if (nhTwoFactorStepEnded(result)) {
|
|
387
|
+
this.expired.emit();
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
defaultMethod() {
|
|
391
|
+
const methods = this.methods();
|
|
392
|
+
return methods.find(method => method !== NhTwoFactorMethods.recoveryCode)
|
|
393
|
+
?? methods[0]
|
|
394
|
+
?? NhTwoFactorMethods.authenticator;
|
|
395
|
+
}
|
|
396
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhTwoFactorChallengeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
397
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.28", type: NhTwoFactorChallengeComponent, isStandalone: true, selector: "nh-two-factor-challenge", inputs: { challenge: { classPropertyName: "challenge", publicName: "challenge", isSignal: true, isRequired: true, transformFunction: null }, allowRememberDevice: { classPropertyName: "allowRememberDevice", publicName: "allowRememberDevice", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { authenticated: "authenticated", expired: "expired", cancelled: "cancelled" }, viewQueries: [{ propertyName: "codeInput", first: true, predicate: ["codeInput"], descendants: true, isSignal: true }], ngImport: i0, template: "<form class=\"nh-two-factor\" [attr.aria-labelledby]=\"id + '-title'\" [attr.aria-busy]=\"busy()\" (submit)=\"$event.preventDefault(); verify()\">\n <h2 class=\"nh-two-factor__title\" [id]=\"id + '-title'\">{{ 'nh-two-factor.ui.challenge-title' | translate }}</h2>\n\n @if (methods().length > 1) {\n <fieldset class=\"nh-two-factor__methods\">\n <legend class=\"nh-two-factor__legend\">{{ 'nh-two-factor.ui.method-label' | translate }}</legend>\n @for (option of methods(); track option) {\n <label class=\"nh-two-factor__method\">\n <input\n type=\"radio\"\n [name]=\"id + '-method'\"\n [value]=\"option\"\n [checked]=\"method() === option\"\n [disabled]=\"busy()\"\n (change)=\"selectMethod(option)\" />\n <span>{{ 'nh-two-factor.ui.method.' + option | translate }}</span>\n </label>\n }\n </fieldset>\n }\n\n @if (method() === 'email') {\n <p class=\"nh-two-factor__hint\">{{ 'nh-two-factor.ui.email-instructions' | translate }}</p>\n <div class=\"nh-two-factor__actions\">\n <button type=\"button\" class=\"nh-two-factor__button\" [disabled]=\"busy()\" (click)=\"sendEmailCode()\">\n {{ (emailSent() ? 'nh-two-factor.ui.resend-email-code' : 'nh-two-factor.ui.send-email-code') | translate }}\n </button>\n </div>\n @if (emailSent(); as sent) {\n <p class=\"nh-two-factor__hint\" role=\"status\">\n {{ 'nh-two-factor.ui.email-code-sent' | translate: { time: (sent.expiresAt | date: 'shortTime') } }}\n </p>\n }\n }\n\n @if (usesCode()) {\n <div class=\"nh-two-factor__field\">\n <label [for]=\"id + '-code'\">{{ 'nh-two-factor.ui.code-label.' + method() | translate }}</label>\n <input\n #codeInput\n type=\"text\"\n class=\"nh-two-factor__input nh-two-factor__input--code\"\n autocomplete=\"one-time-code\"\n spellcheck=\"false\"\n [id]=\"id + '-code'\"\n [attr.inputmode]=\"numericCode() ? 'numeric' : 'text'\"\n [value]=\"code()\"\n [disabled]=\"busy()\"\n [attr.aria-invalid]=\"errorKeys().length > 0 ? true : null\"\n [attr.aria-describedby]=\"errorKeys().length > 0 ? id + '-errors' : null\"\n (input)=\"code.set(value($event))\" />\n </div>\n } @else {\n <p class=\"nh-two-factor__hint\">{{ 'nh-two-factor.ui.passkey-verify-instructions' | translate }}</p>\n }\n\n @if (offerRememberDevice()) {\n <label class=\"nh-two-factor__check\">\n <input type=\"checkbox\" [checked]=\"rememberDevice()\" [disabled]=\"busy()\" (change)=\"rememberDevice.set(checked($event))\" />\n <span>{{ 'nh-two-factor.ui.remember-device' | translate }}</span>\n </label>\n }\n\n <div [id]=\"id + '-errors'\" aria-live=\"assertive\">\n @if (errorKeys().length > 0) {\n <ul class=\"nh-two-factor__errors\" role=\"alert\">\n @for (key of errorKeys(); track key) {\n <li>{{ key | translate }}</li>\n }\n </ul>\n }\n </div>\n\n <div class=\"nh-two-factor__actions\">\n <button type=\"submit\" class=\"nh-two-factor__button nh-two-factor__button--primary\" [disabled]=\"busy()\">\n @if (busy()) {\n {{ 'nh-two-factor.ui.verifying' | translate }}\n } @else {\n {{ (usesCode() ? 'nh-two-factor.ui.verify' : 'nh-two-factor.ui.use-passkey') | translate }}\n }\n </button>\n <button type=\"button\" class=\"nh-two-factor__button\" [disabled]=\"busy()\" (click)=\"cancelled.emit()\">\n {{ 'nh-two-factor.ui.cancel' | translate }}\n </button>\n </div>\n</form>\n", styles: [":host{display:block;min-width:0;color:var(--nh-color-text, #1f2933)}.nh-two-factor{display:flex;flex-direction:column;gap:1rem;min-width:0}.nh-two-factor__title{margin:0;font-size:1.125rem;font-weight:650}.nh-two-factor__subtitle{margin:0;font-size:1rem;font-weight:650}.nh-two-factor__text,.nh-two-factor__hint{margin:0;overflow-wrap:anywhere}.nh-two-factor__hint{color:var(--nh-color-muted, #52606d);font-size:.875rem}.nh-two-factor__notice{margin:0;padding:.5rem .75rem;border-radius:.5rem;background:var(--nh-color-success-background, #e7f6ec);color:var(--nh-color-success, #1e6b3a)}.nh-two-factor__methods{display:flex;flex-wrap:wrap;gap:.5rem;min-width:0;margin:0;padding:0;border:0}.nh-two-factor__legend{width:100%;margin-bottom:.25rem;padding:0;font-weight:600}.nh-two-factor__method{position:relative}.nh-two-factor__method input{position:absolute;inset:0;opacity:0;cursor:pointer}.nh-two-factor__method span{display:inline-flex;padding:.5rem .75rem;border:1px solid var(--nh-color-border, #cbd2d9);border-radius:.5rem;background:var(--nh-color-surface, #fff)}.nh-two-factor__method input:checked+span{border-color:var(--nh-color-primary, #2563eb);box-shadow:inset 0 0 0 1px var(--nh-color-primary, #2563eb);font-weight:600}.nh-two-factor__method input:focus-visible+span{outline:2px solid var(--nh-color-focus, #2563eb);outline-offset:2px}.nh-two-factor__method input:disabled+span{opacity:.6}.nh-two-factor__field{display:flex;flex-direction:column;gap:.375rem;min-width:0}.nh-two-factor__field label{font-weight:600}.nh-two-factor__input{width:100%;max-width:20rem;padding:.5rem .75rem;border:1px solid var(--nh-color-border, #cbd2d9);border-radius:.5rem;background:var(--nh-color-input-background, #fff);color:inherit;font:inherit}.nh-two-factor__input:focus-visible{outline:2px solid var(--nh-color-focus, #2563eb);outline-offset:1px}.nh-two-factor__input[aria-invalid=true]{border-color:var(--nh-color-danger, #b91c1c)}.nh-two-factor__input--code{font-variant-numeric:tabular-nums;letter-spacing:.15em}.nh-two-factor__check{display:flex;align-items:center;gap:.5rem}.nh-two-factor__actions{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem}.nh-two-factor__button{padding:.5rem 1rem;border:1px solid var(--nh-color-border, #cbd2d9);border-radius:.5rem;background:var(--nh-color-surface, #fff);color:inherit;font:inherit;cursor:pointer}.nh-two-factor__button:focus-visible{outline:2px solid var(--nh-color-focus, #2563eb);outline-offset:2px}.nh-two-factor__button:disabled{opacity:.6;cursor:not-allowed}.nh-two-factor__button--primary{border-color:var(--nh-color-primary, #2563eb);background:var(--nh-color-primary, #2563eb);color:var(--nh-color-on-primary, #fff)}.nh-two-factor__button--danger{border-color:var(--nh-color-danger, #b91c1c);color:var(--nh-color-danger, #b91c1c)}.nh-two-factor__errors{margin:0;padding:.5rem .75rem;border-radius:.5rem;background:var(--nh-color-danger-background, #fdecec);color:var(--nh-color-danger, #b91c1c);list-style:none}.nh-two-factor__setup{display:flex;flex-wrap:wrap;align-items:flex-start;gap:1rem}.nh-two-factor__qr{width:11rem;height:11rem;padding:.5rem;border-radius:.5rem;background:#fff;image-rendering:pixelated}.nh-two-factor__key{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;overflow-wrap:anywhere;-webkit-user-select:all;user-select:all}.nh-two-factor__codes{display:grid;grid-template-columns:repeat(auto-fill,minmax(9rem,1fr));gap:.5rem;margin:0;padding:0;list-style:none;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}.nh-two-factor__codes li{padding:.375rem .5rem;border:1px dashed var(--nh-color-border, #cbd2d9);border-radius:.375rem;text-align:center}.nh-two-factor__section{display:flex;flex-direction:column;gap:.75rem;padding-top:1rem;border-top:1px solid var(--nh-color-border, #cbd2d9)}.nh-two-factor__status{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem}.nh-two-factor__badge{padding:.125rem .5rem;border-radius:999px;background:var(--nh-color-status-background, #eef2f7);font-size:.8125rem;font-weight:600}.nh-two-factor__badge--on{background:var(--nh-color-success-background, #e7f6ec);color:var(--nh-color-success, #1e6b3a)}.nh-two-factor__list{display:flex;flex-direction:column;gap:.5rem;margin:0;padding:0;list-style:none}.nh-two-factor__item{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:.5rem;min-width:0;padding:.75rem;border:1px solid var(--nh-color-border, #cbd2d9);border-radius:.5rem}.nh-two-factor__item>div{min-width:0}.visually-hidden{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}\n"], dependencies: [{ kind: "pipe", type: TranslatePipe, name: "translate" }, { kind: "pipe", type: DatePipe, name: "date" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
398
|
+
}
|
|
399
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhTwoFactorChallengeComponent, decorators: [{
|
|
400
|
+
type: Component,
|
|
401
|
+
args: [{ selector: 'nh-two-factor-challenge', standalone: true, imports: [TranslatePipe, DatePipe], changeDetection: ChangeDetectionStrategy.OnPush, template: "<form class=\"nh-two-factor\" [attr.aria-labelledby]=\"id + '-title'\" [attr.aria-busy]=\"busy()\" (submit)=\"$event.preventDefault(); verify()\">\n <h2 class=\"nh-two-factor__title\" [id]=\"id + '-title'\">{{ 'nh-two-factor.ui.challenge-title' | translate }}</h2>\n\n @if (methods().length > 1) {\n <fieldset class=\"nh-two-factor__methods\">\n <legend class=\"nh-two-factor__legend\">{{ 'nh-two-factor.ui.method-label' | translate }}</legend>\n @for (option of methods(); track option) {\n <label class=\"nh-two-factor__method\">\n <input\n type=\"radio\"\n [name]=\"id + '-method'\"\n [value]=\"option\"\n [checked]=\"method() === option\"\n [disabled]=\"busy()\"\n (change)=\"selectMethod(option)\" />\n <span>{{ 'nh-two-factor.ui.method.' + option | translate }}</span>\n </label>\n }\n </fieldset>\n }\n\n @if (method() === 'email') {\n <p class=\"nh-two-factor__hint\">{{ 'nh-two-factor.ui.email-instructions' | translate }}</p>\n <div class=\"nh-two-factor__actions\">\n <button type=\"button\" class=\"nh-two-factor__button\" [disabled]=\"busy()\" (click)=\"sendEmailCode()\">\n {{ (emailSent() ? 'nh-two-factor.ui.resend-email-code' : 'nh-two-factor.ui.send-email-code') | translate }}\n </button>\n </div>\n @if (emailSent(); as sent) {\n <p class=\"nh-two-factor__hint\" role=\"status\">\n {{ 'nh-two-factor.ui.email-code-sent' | translate: { time: (sent.expiresAt | date: 'shortTime') } }}\n </p>\n }\n }\n\n @if (usesCode()) {\n <div class=\"nh-two-factor__field\">\n <label [for]=\"id + '-code'\">{{ 'nh-two-factor.ui.code-label.' + method() | translate }}</label>\n <input\n #codeInput\n type=\"text\"\n class=\"nh-two-factor__input nh-two-factor__input--code\"\n autocomplete=\"one-time-code\"\n spellcheck=\"false\"\n [id]=\"id + '-code'\"\n [attr.inputmode]=\"numericCode() ? 'numeric' : 'text'\"\n [value]=\"code()\"\n [disabled]=\"busy()\"\n [attr.aria-invalid]=\"errorKeys().length > 0 ? true : null\"\n [attr.aria-describedby]=\"errorKeys().length > 0 ? id + '-errors' : null\"\n (input)=\"code.set(value($event))\" />\n </div>\n } @else {\n <p class=\"nh-two-factor__hint\">{{ 'nh-two-factor.ui.passkey-verify-instructions' | translate }}</p>\n }\n\n @if (offerRememberDevice()) {\n <label class=\"nh-two-factor__check\">\n <input type=\"checkbox\" [checked]=\"rememberDevice()\" [disabled]=\"busy()\" (change)=\"rememberDevice.set(checked($event))\" />\n <span>{{ 'nh-two-factor.ui.remember-device' | translate }}</span>\n </label>\n }\n\n <div [id]=\"id + '-errors'\" aria-live=\"assertive\">\n @if (errorKeys().length > 0) {\n <ul class=\"nh-two-factor__errors\" role=\"alert\">\n @for (key of errorKeys(); track key) {\n <li>{{ key | translate }}</li>\n }\n </ul>\n }\n </div>\n\n <div class=\"nh-two-factor__actions\">\n <button type=\"submit\" class=\"nh-two-factor__button nh-two-factor__button--primary\" [disabled]=\"busy()\">\n @if (busy()) {\n {{ 'nh-two-factor.ui.verifying' | translate }}\n } @else {\n {{ (usesCode() ? 'nh-two-factor.ui.verify' : 'nh-two-factor.ui.use-passkey') | translate }}\n }\n </button>\n <button type=\"button\" class=\"nh-two-factor__button\" [disabled]=\"busy()\" (click)=\"cancelled.emit()\">\n {{ 'nh-two-factor.ui.cancel' | translate }}\n </button>\n </div>\n</form>\n", styles: [":host{display:block;min-width:0;color:var(--nh-color-text, #1f2933)}.nh-two-factor{display:flex;flex-direction:column;gap:1rem;min-width:0}.nh-two-factor__title{margin:0;font-size:1.125rem;font-weight:650}.nh-two-factor__subtitle{margin:0;font-size:1rem;font-weight:650}.nh-two-factor__text,.nh-two-factor__hint{margin:0;overflow-wrap:anywhere}.nh-two-factor__hint{color:var(--nh-color-muted, #52606d);font-size:.875rem}.nh-two-factor__notice{margin:0;padding:.5rem .75rem;border-radius:.5rem;background:var(--nh-color-success-background, #e7f6ec);color:var(--nh-color-success, #1e6b3a)}.nh-two-factor__methods{display:flex;flex-wrap:wrap;gap:.5rem;min-width:0;margin:0;padding:0;border:0}.nh-two-factor__legend{width:100%;margin-bottom:.25rem;padding:0;font-weight:600}.nh-two-factor__method{position:relative}.nh-two-factor__method input{position:absolute;inset:0;opacity:0;cursor:pointer}.nh-two-factor__method span{display:inline-flex;padding:.5rem .75rem;border:1px solid var(--nh-color-border, #cbd2d9);border-radius:.5rem;background:var(--nh-color-surface, #fff)}.nh-two-factor__method input:checked+span{border-color:var(--nh-color-primary, #2563eb);box-shadow:inset 0 0 0 1px var(--nh-color-primary, #2563eb);font-weight:600}.nh-two-factor__method input:focus-visible+span{outline:2px solid var(--nh-color-focus, #2563eb);outline-offset:2px}.nh-two-factor__method input:disabled+span{opacity:.6}.nh-two-factor__field{display:flex;flex-direction:column;gap:.375rem;min-width:0}.nh-two-factor__field label{font-weight:600}.nh-two-factor__input{width:100%;max-width:20rem;padding:.5rem .75rem;border:1px solid var(--nh-color-border, #cbd2d9);border-radius:.5rem;background:var(--nh-color-input-background, #fff);color:inherit;font:inherit}.nh-two-factor__input:focus-visible{outline:2px solid var(--nh-color-focus, #2563eb);outline-offset:1px}.nh-two-factor__input[aria-invalid=true]{border-color:var(--nh-color-danger, #b91c1c)}.nh-two-factor__input--code{font-variant-numeric:tabular-nums;letter-spacing:.15em}.nh-two-factor__check{display:flex;align-items:center;gap:.5rem}.nh-two-factor__actions{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem}.nh-two-factor__button{padding:.5rem 1rem;border:1px solid var(--nh-color-border, #cbd2d9);border-radius:.5rem;background:var(--nh-color-surface, #fff);color:inherit;font:inherit;cursor:pointer}.nh-two-factor__button:focus-visible{outline:2px solid var(--nh-color-focus, #2563eb);outline-offset:2px}.nh-two-factor__button:disabled{opacity:.6;cursor:not-allowed}.nh-two-factor__button--primary{border-color:var(--nh-color-primary, #2563eb);background:var(--nh-color-primary, #2563eb);color:var(--nh-color-on-primary, #fff)}.nh-two-factor__button--danger{border-color:var(--nh-color-danger, #b91c1c);color:var(--nh-color-danger, #b91c1c)}.nh-two-factor__errors{margin:0;padding:.5rem .75rem;border-radius:.5rem;background:var(--nh-color-danger-background, #fdecec);color:var(--nh-color-danger, #b91c1c);list-style:none}.nh-two-factor__setup{display:flex;flex-wrap:wrap;align-items:flex-start;gap:1rem}.nh-two-factor__qr{width:11rem;height:11rem;padding:.5rem;border-radius:.5rem;background:#fff;image-rendering:pixelated}.nh-two-factor__key{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;overflow-wrap:anywhere;-webkit-user-select:all;user-select:all}.nh-two-factor__codes{display:grid;grid-template-columns:repeat(auto-fill,minmax(9rem,1fr));gap:.5rem;margin:0;padding:0;list-style:none;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}.nh-two-factor__codes li{padding:.375rem .5rem;border:1px dashed var(--nh-color-border, #cbd2d9);border-radius:.375rem;text-align:center}.nh-two-factor__section{display:flex;flex-direction:column;gap:.75rem;padding-top:1rem;border-top:1px solid var(--nh-color-border, #cbd2d9)}.nh-two-factor__status{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem}.nh-two-factor__badge{padding:.125rem .5rem;border-radius:999px;background:var(--nh-color-status-background, #eef2f7);font-size:.8125rem;font-weight:600}.nh-two-factor__badge--on{background:var(--nh-color-success-background, #e7f6ec);color:var(--nh-color-success, #1e6b3a)}.nh-two-factor__list{display:flex;flex-direction:column;gap:.5rem;margin:0;padding:0;list-style:none}.nh-two-factor__item{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:.5rem;min-width:0;padding:.75rem;border:1px solid var(--nh-color-border, #cbd2d9);border-radius:.5rem}.nh-two-factor__item>div{min-width:0}.visually-hidden{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}\n"] }]
|
|
402
|
+
}], ctorParameters: () => [], propDecorators: { codeInput: [{ type: i0.ViewChild, args: ['codeInput', { isSignal: true }] }], challenge: [{ type: i0.Input, args: [{ isSignal: true, alias: "challenge", required: true }] }], allowRememberDevice: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowRememberDevice", required: false }] }], authenticated: [{ type: i0.Output, args: ["authenticated"] }], expired: [{ type: i0.Output, args: ["expired"] }], cancelled: [{ type: i0.Output, args: ["cancelled"] }] } });
|
|
403
|
+
|
|
404
|
+
let nextId$3 = 0;
|
|
405
|
+
/**
|
|
406
|
+
* Shows recovery codes once, with copy and download actions. The user confirms that the
|
|
407
|
+
* codes were saved before continuing.
|
|
408
|
+
*/
|
|
409
|
+
class NhTwoFactorRecoveryCodesComponent {
|
|
410
|
+
constructor() {
|
|
411
|
+
this.codes = input.required(...(ngDevMode ? [{ debugName: "codes" }] : []));
|
|
412
|
+
/** File name of the downloaded codes. */
|
|
413
|
+
this.fileName = input('recovery-codes.txt', ...(ngDevMode ? [{ debugName: "fileName" }] : []));
|
|
414
|
+
this.acknowledged = output();
|
|
415
|
+
this.id = `nh-two-factor-recovery-codes-${nextId$3++}`;
|
|
416
|
+
this.saved = signal(false, ...(ngDevMode ? [{ debugName: "saved" }] : []));
|
|
417
|
+
this.copied = signal(false, ...(ngDevMode ? [{ debugName: "copied" }] : []));
|
|
418
|
+
this.checked = nhTwoFactorInputChecked;
|
|
419
|
+
inject(NhTwoFactorTranslationMerger).start();
|
|
420
|
+
}
|
|
421
|
+
async copy() {
|
|
422
|
+
try {
|
|
423
|
+
await navigator.clipboard.writeText(this.codes().join('\n'));
|
|
424
|
+
this.copied.set(true);
|
|
425
|
+
}
|
|
426
|
+
catch {
|
|
427
|
+
// Clipboard access can be denied; the codes stay visible for manual copying.
|
|
428
|
+
this.copied.set(false);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
download() {
|
|
432
|
+
const url = URL.createObjectURL(new Blob([this.codes().join('\r\n') + '\r\n'], { type: 'text/plain' }));
|
|
433
|
+
const link = document.createElement('a');
|
|
434
|
+
link.href = url;
|
|
435
|
+
link.download = this.fileName();
|
|
436
|
+
link.click();
|
|
437
|
+
URL.revokeObjectURL(url);
|
|
438
|
+
}
|
|
439
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhTwoFactorRecoveryCodesComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
440
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.28", type: NhTwoFactorRecoveryCodesComponent, isStandalone: true, selector: "nh-two-factor-recovery-codes", inputs: { codes: { classPropertyName: "codes", publicName: "codes", isSignal: true, isRequired: true, transformFunction: null }, fileName: { classPropertyName: "fileName", publicName: "fileName", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { acknowledged: "acknowledged" }, ngImport: i0, template: "<section class=\"nh-two-factor\" [attr.aria-labelledby]=\"id + '-title'\">\n <h3 class=\"nh-two-factor__subtitle\" [id]=\"id + '-title'\">{{ 'nh-two-factor.ui.recovery-codes-title' | translate }}</h3>\n <p class=\"nh-two-factor__text\">{{ 'nh-two-factor.ui.recovery-codes-instructions' | translate }}</p>\n\n <ul class=\"nh-two-factor__codes\" [attr.aria-labelledby]=\"id + '-title'\">\n @for (code of codes(); track code) {\n <li>{{ code }}</li>\n }\n </ul>\n\n <div class=\"nh-two-factor__actions\">\n <button type=\"button\" class=\"nh-two-factor__button\" (click)=\"copy()\">\n {{ (copied() ? 'nh-two-factor.ui.copied' : 'nh-two-factor.ui.copy') | translate }}\n </button>\n <button type=\"button\" class=\"nh-two-factor__button\" (click)=\"download()\">\n {{ 'nh-two-factor.ui.download' | translate }}\n </button>\n </div>\n\n <label class=\"nh-two-factor__check\">\n <input type=\"checkbox\" [checked]=\"saved()\" (change)=\"saved.set(checked($event))\" />\n <span>{{ 'nh-two-factor.ui.recovery-codes-saved' | translate }}</span>\n </label>\n\n <div class=\"nh-two-factor__actions\">\n <button type=\"button\" class=\"nh-two-factor__button nh-two-factor__button--primary\" [disabled]=\"!saved()\" (click)=\"acknowledged.emit()\">\n {{ 'nh-two-factor.ui.continue' | translate }}\n </button>\n </div>\n</section>\n", styles: [":host{display:block;min-width:0;color:var(--nh-color-text, #1f2933)}.nh-two-factor{display:flex;flex-direction:column;gap:1rem;min-width:0}.nh-two-factor__title{margin:0;font-size:1.125rem;font-weight:650}.nh-two-factor__subtitle{margin:0;font-size:1rem;font-weight:650}.nh-two-factor__text,.nh-two-factor__hint{margin:0;overflow-wrap:anywhere}.nh-two-factor__hint{color:var(--nh-color-muted, #52606d);font-size:.875rem}.nh-two-factor__notice{margin:0;padding:.5rem .75rem;border-radius:.5rem;background:var(--nh-color-success-background, #e7f6ec);color:var(--nh-color-success, #1e6b3a)}.nh-two-factor__methods{display:flex;flex-wrap:wrap;gap:.5rem;min-width:0;margin:0;padding:0;border:0}.nh-two-factor__legend{width:100%;margin-bottom:.25rem;padding:0;font-weight:600}.nh-two-factor__method{position:relative}.nh-two-factor__method input{position:absolute;inset:0;opacity:0;cursor:pointer}.nh-two-factor__method span{display:inline-flex;padding:.5rem .75rem;border:1px solid var(--nh-color-border, #cbd2d9);border-radius:.5rem;background:var(--nh-color-surface, #fff)}.nh-two-factor__method input:checked+span{border-color:var(--nh-color-primary, #2563eb);box-shadow:inset 0 0 0 1px var(--nh-color-primary, #2563eb);font-weight:600}.nh-two-factor__method input:focus-visible+span{outline:2px solid var(--nh-color-focus, #2563eb);outline-offset:2px}.nh-two-factor__method input:disabled+span{opacity:.6}.nh-two-factor__field{display:flex;flex-direction:column;gap:.375rem;min-width:0}.nh-two-factor__field label{font-weight:600}.nh-two-factor__input{width:100%;max-width:20rem;padding:.5rem .75rem;border:1px solid var(--nh-color-border, #cbd2d9);border-radius:.5rem;background:var(--nh-color-input-background, #fff);color:inherit;font:inherit}.nh-two-factor__input:focus-visible{outline:2px solid var(--nh-color-focus, #2563eb);outline-offset:1px}.nh-two-factor__input[aria-invalid=true]{border-color:var(--nh-color-danger, #b91c1c)}.nh-two-factor__input--code{font-variant-numeric:tabular-nums;letter-spacing:.15em}.nh-two-factor__check{display:flex;align-items:center;gap:.5rem}.nh-two-factor__actions{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem}.nh-two-factor__button{padding:.5rem 1rem;border:1px solid var(--nh-color-border, #cbd2d9);border-radius:.5rem;background:var(--nh-color-surface, #fff);color:inherit;font:inherit;cursor:pointer}.nh-two-factor__button:focus-visible{outline:2px solid var(--nh-color-focus, #2563eb);outline-offset:2px}.nh-two-factor__button:disabled{opacity:.6;cursor:not-allowed}.nh-two-factor__button--primary{border-color:var(--nh-color-primary, #2563eb);background:var(--nh-color-primary, #2563eb);color:var(--nh-color-on-primary, #fff)}.nh-two-factor__button--danger{border-color:var(--nh-color-danger, #b91c1c);color:var(--nh-color-danger, #b91c1c)}.nh-two-factor__errors{margin:0;padding:.5rem .75rem;border-radius:.5rem;background:var(--nh-color-danger-background, #fdecec);color:var(--nh-color-danger, #b91c1c);list-style:none}.nh-two-factor__setup{display:flex;flex-wrap:wrap;align-items:flex-start;gap:1rem}.nh-two-factor__qr{width:11rem;height:11rem;padding:.5rem;border-radius:.5rem;background:#fff;image-rendering:pixelated}.nh-two-factor__key{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;overflow-wrap:anywhere;-webkit-user-select:all;user-select:all}.nh-two-factor__codes{display:grid;grid-template-columns:repeat(auto-fill,minmax(9rem,1fr));gap:.5rem;margin:0;padding:0;list-style:none;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}.nh-two-factor__codes li{padding:.375rem .5rem;border:1px dashed var(--nh-color-border, #cbd2d9);border-radius:.375rem;text-align:center}.nh-two-factor__section{display:flex;flex-direction:column;gap:.75rem;padding-top:1rem;border-top:1px solid var(--nh-color-border, #cbd2d9)}.nh-two-factor__status{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem}.nh-two-factor__badge{padding:.125rem .5rem;border-radius:999px;background:var(--nh-color-status-background, #eef2f7);font-size:.8125rem;font-weight:600}.nh-two-factor__badge--on{background:var(--nh-color-success-background, #e7f6ec);color:var(--nh-color-success, #1e6b3a)}.nh-two-factor__list{display:flex;flex-direction:column;gap:.5rem;margin:0;padding:0;list-style:none}.nh-two-factor__item{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:.5rem;min-width:0;padding:.75rem;border:1px solid var(--nh-color-border, #cbd2d9);border-radius:.5rem}.nh-two-factor__item>div{min-width:0}.visually-hidden{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}\n"], dependencies: [{ kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
441
|
+
}
|
|
442
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhTwoFactorRecoveryCodesComponent, decorators: [{
|
|
443
|
+
type: Component,
|
|
444
|
+
args: [{ selector: 'nh-two-factor-recovery-codes', standalone: true, imports: [TranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, template: "<section class=\"nh-two-factor\" [attr.aria-labelledby]=\"id + '-title'\">\n <h3 class=\"nh-two-factor__subtitle\" [id]=\"id + '-title'\">{{ 'nh-two-factor.ui.recovery-codes-title' | translate }}</h3>\n <p class=\"nh-two-factor__text\">{{ 'nh-two-factor.ui.recovery-codes-instructions' | translate }}</p>\n\n <ul class=\"nh-two-factor__codes\" [attr.aria-labelledby]=\"id + '-title'\">\n @for (code of codes(); track code) {\n <li>{{ code }}</li>\n }\n </ul>\n\n <div class=\"nh-two-factor__actions\">\n <button type=\"button\" class=\"nh-two-factor__button\" (click)=\"copy()\">\n {{ (copied() ? 'nh-two-factor.ui.copied' : 'nh-two-factor.ui.copy') | translate }}\n </button>\n <button type=\"button\" class=\"nh-two-factor__button\" (click)=\"download()\">\n {{ 'nh-two-factor.ui.download' | translate }}\n </button>\n </div>\n\n <label class=\"nh-two-factor__check\">\n <input type=\"checkbox\" [checked]=\"saved()\" (change)=\"saved.set(checked($event))\" />\n <span>{{ 'nh-two-factor.ui.recovery-codes-saved' | translate }}</span>\n </label>\n\n <div class=\"nh-two-factor__actions\">\n <button type=\"button\" class=\"nh-two-factor__button nh-two-factor__button--primary\" [disabled]=\"!saved()\" (click)=\"acknowledged.emit()\">\n {{ 'nh-two-factor.ui.continue' | translate }}\n </button>\n </div>\n</section>\n", styles: [":host{display:block;min-width:0;color:var(--nh-color-text, #1f2933)}.nh-two-factor{display:flex;flex-direction:column;gap:1rem;min-width:0}.nh-two-factor__title{margin:0;font-size:1.125rem;font-weight:650}.nh-two-factor__subtitle{margin:0;font-size:1rem;font-weight:650}.nh-two-factor__text,.nh-two-factor__hint{margin:0;overflow-wrap:anywhere}.nh-two-factor__hint{color:var(--nh-color-muted, #52606d);font-size:.875rem}.nh-two-factor__notice{margin:0;padding:.5rem .75rem;border-radius:.5rem;background:var(--nh-color-success-background, #e7f6ec);color:var(--nh-color-success, #1e6b3a)}.nh-two-factor__methods{display:flex;flex-wrap:wrap;gap:.5rem;min-width:0;margin:0;padding:0;border:0}.nh-two-factor__legend{width:100%;margin-bottom:.25rem;padding:0;font-weight:600}.nh-two-factor__method{position:relative}.nh-two-factor__method input{position:absolute;inset:0;opacity:0;cursor:pointer}.nh-two-factor__method span{display:inline-flex;padding:.5rem .75rem;border:1px solid var(--nh-color-border, #cbd2d9);border-radius:.5rem;background:var(--nh-color-surface, #fff)}.nh-two-factor__method input:checked+span{border-color:var(--nh-color-primary, #2563eb);box-shadow:inset 0 0 0 1px var(--nh-color-primary, #2563eb);font-weight:600}.nh-two-factor__method input:focus-visible+span{outline:2px solid var(--nh-color-focus, #2563eb);outline-offset:2px}.nh-two-factor__method input:disabled+span{opacity:.6}.nh-two-factor__field{display:flex;flex-direction:column;gap:.375rem;min-width:0}.nh-two-factor__field label{font-weight:600}.nh-two-factor__input{width:100%;max-width:20rem;padding:.5rem .75rem;border:1px solid var(--nh-color-border, #cbd2d9);border-radius:.5rem;background:var(--nh-color-input-background, #fff);color:inherit;font:inherit}.nh-two-factor__input:focus-visible{outline:2px solid var(--nh-color-focus, #2563eb);outline-offset:1px}.nh-two-factor__input[aria-invalid=true]{border-color:var(--nh-color-danger, #b91c1c)}.nh-two-factor__input--code{font-variant-numeric:tabular-nums;letter-spacing:.15em}.nh-two-factor__check{display:flex;align-items:center;gap:.5rem}.nh-two-factor__actions{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem}.nh-two-factor__button{padding:.5rem 1rem;border:1px solid var(--nh-color-border, #cbd2d9);border-radius:.5rem;background:var(--nh-color-surface, #fff);color:inherit;font:inherit;cursor:pointer}.nh-two-factor__button:focus-visible{outline:2px solid var(--nh-color-focus, #2563eb);outline-offset:2px}.nh-two-factor__button:disabled{opacity:.6;cursor:not-allowed}.nh-two-factor__button--primary{border-color:var(--nh-color-primary, #2563eb);background:var(--nh-color-primary, #2563eb);color:var(--nh-color-on-primary, #fff)}.nh-two-factor__button--danger{border-color:var(--nh-color-danger, #b91c1c);color:var(--nh-color-danger, #b91c1c)}.nh-two-factor__errors{margin:0;padding:.5rem .75rem;border-radius:.5rem;background:var(--nh-color-danger-background, #fdecec);color:var(--nh-color-danger, #b91c1c);list-style:none}.nh-two-factor__setup{display:flex;flex-wrap:wrap;align-items:flex-start;gap:1rem}.nh-two-factor__qr{width:11rem;height:11rem;padding:.5rem;border-radius:.5rem;background:#fff;image-rendering:pixelated}.nh-two-factor__key{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;overflow-wrap:anywhere;-webkit-user-select:all;user-select:all}.nh-two-factor__codes{display:grid;grid-template-columns:repeat(auto-fill,minmax(9rem,1fr));gap:.5rem;margin:0;padding:0;list-style:none;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}.nh-two-factor__codes li{padding:.375rem .5rem;border:1px dashed var(--nh-color-border, #cbd2d9);border-radius:.375rem;text-align:center}.nh-two-factor__section{display:flex;flex-direction:column;gap:.75rem;padding-top:1rem;border-top:1px solid var(--nh-color-border, #cbd2d9)}.nh-two-factor__status{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem}.nh-two-factor__badge{padding:.125rem .5rem;border-radius:999px;background:var(--nh-color-status-background, #eef2f7);font-size:.8125rem;font-weight:600}.nh-two-factor__badge--on{background:var(--nh-color-success-background, #e7f6ec);color:var(--nh-color-success, #1e6b3a)}.nh-two-factor__list{display:flex;flex-direction:column;gap:.5rem;margin:0;padding:0;list-style:none}.nh-two-factor__item{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:.5rem;min-width:0;padding:.75rem;border:1px solid var(--nh-color-border, #cbd2d9);border-radius:.5rem}.nh-two-factor__item>div{min-width:0}.visually-hidden{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}\n"] }]
|
|
445
|
+
}], ctorParameters: () => [], propDecorators: { codes: [{ type: i0.Input, args: [{ isSignal: true, alias: "codes", required: true }] }], fileName: [{ type: i0.Input, args: [{ isSignal: true, alias: "fileName", required: false }] }], acknowledged: [{ type: i0.Output, args: ["acknowledged"] }] } });
|
|
446
|
+
|
|
447
|
+
let nextId$2 = 0;
|
|
448
|
+
/**
|
|
449
|
+
* Enrolls a second factor for a user whom the policy requires to use one, during sign-in.
|
|
450
|
+
* The session is stored by the auth service; `completed` emits after the user saved the
|
|
451
|
+
* recovery codes.
|
|
452
|
+
*/
|
|
453
|
+
class NhTwoFactorEnrollmentComponent {
|
|
454
|
+
constructor() {
|
|
455
|
+
this.auth = inject(NhAuthService);
|
|
456
|
+
this.injector = inject(Injector);
|
|
457
|
+
this.codeInput = viewChild('codeInput', ...(ngDevMode ? [{ debugName: "codeInput" }] : []));
|
|
458
|
+
/** The enrollment step from the login response (status `enrollment-required`). */
|
|
459
|
+
this.enrollment = input.required(...(ngDevMode ? [{ debugName: "enrollment" }] : []));
|
|
460
|
+
this.completed = output();
|
|
461
|
+
/** The enrollment expired or the account was locked; the user has to sign in again. */
|
|
462
|
+
this.expired = output();
|
|
463
|
+
this.cancelled = output();
|
|
464
|
+
this.id = `nh-two-factor-enrollment-${nextId$2++}`;
|
|
465
|
+
this.passkeySupported = this.auth.isPasskeySupported();
|
|
466
|
+
this.selectedMethod = signal(null, ...(ngDevMode ? [{ debugName: "selectedMethod" }] : []));
|
|
467
|
+
this.setup = signal(null, ...(ngDevMode ? [{ debugName: "setup" }] : []));
|
|
468
|
+
this.emailSent = signal(null, ...(ngDevMode ? [{ debugName: "emailSent" }] : []));
|
|
469
|
+
this.code = signal('', ...(ngDevMode ? [{ debugName: "code" }] : []));
|
|
470
|
+
this.passkeyName = signal('', ...(ngDevMode ? [{ debugName: "passkeyName" }] : []));
|
|
471
|
+
this.busy = signal(false, ...(ngDevMode ? [{ debugName: "busy" }] : []));
|
|
472
|
+
this.errorKeys = signal([], ...(ngDevMode ? [{ debugName: "errorKeys" }] : []));
|
|
473
|
+
this.result = signal(null, ...(ngDevMode ? [{ debugName: "result" }] : []));
|
|
474
|
+
this.methods = computed(() => this.enrollment().methods
|
|
475
|
+
.filter(method => method !== NhTwoFactorMethods.passkey || this.passkeySupported), ...(ngDevMode ? [{ debugName: "methods" }] : []));
|
|
476
|
+
this.method = computed(() => this.selectedMethod() ?? this.methods()[0] ?? NhTwoFactorMethods.authenticator, ...(ngDevMode ? [{ debugName: "method" }] : []));
|
|
477
|
+
this.value = nhTwoFactorInputValue;
|
|
478
|
+
inject(NhTwoFactorTranslationMerger).start();
|
|
479
|
+
}
|
|
480
|
+
ngOnInit() {
|
|
481
|
+
if (this.method() === NhTwoFactorMethods.authenticator) {
|
|
482
|
+
void this.startAuthenticator();
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
selectMethod(method) {
|
|
486
|
+
this.selectedMethod.set(method);
|
|
487
|
+
this.code.set('');
|
|
488
|
+
this.errorKeys.set([]);
|
|
489
|
+
if (method === NhTwoFactorMethods.authenticator && !this.setup()) {
|
|
490
|
+
void this.startAuthenticator();
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
async startAuthenticator() {
|
|
494
|
+
const setup = await this.run(() => this.auth.beginEnrollmentAuthenticatorSetup(this.token()));
|
|
495
|
+
if (setup) {
|
|
496
|
+
this.setup.set(setup);
|
|
497
|
+
this.focusCode();
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
async sendEmailCode() {
|
|
501
|
+
const sent = await this.run(() => this.auth.sendEnrollmentEmailCode(this.token()));
|
|
502
|
+
if (sent) {
|
|
503
|
+
this.emailSent.set(sent);
|
|
504
|
+
this.focusCode();
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
async confirm() {
|
|
508
|
+
if (this.busy()) {
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
const method = this.method();
|
|
512
|
+
if (method === NhTwoFactorMethods.passkey) {
|
|
513
|
+
this.complete(await this.run(() => this.auth.enrollPasskey(this.token(), this.passkeyName().trim() || undefined)));
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
const code = this.code().trim();
|
|
517
|
+
if (!code) {
|
|
518
|
+
this.errorKeys.set(['nh-two-factor.invalid-code']);
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
const confirmation = method === NhTwoFactorMethods.email
|
|
522
|
+
? () => this.auth.confirmEnrollmentEmail(this.token(), code)
|
|
523
|
+
: () => this.auth.confirmEnrollmentAuthenticator(this.token(), code);
|
|
524
|
+
this.complete(await this.run(confirmation));
|
|
525
|
+
}
|
|
526
|
+
/** Emits the result after the user saved the recovery codes. */
|
|
527
|
+
finish() {
|
|
528
|
+
const result = this.result();
|
|
529
|
+
if (result) {
|
|
530
|
+
this.completed.emit(result);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
complete(result) {
|
|
534
|
+
if (!result) {
|
|
535
|
+
this.code.set('');
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
if (result.recoveryCodes?.length) {
|
|
539
|
+
this.result.set(result);
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
this.completed.emit(result);
|
|
543
|
+
}
|
|
544
|
+
async run(action) {
|
|
545
|
+
this.busy.set(true);
|
|
546
|
+
this.errorKeys.set([]);
|
|
547
|
+
const result = await action();
|
|
548
|
+
this.busy.set(false);
|
|
549
|
+
if (!result.isSuccess) {
|
|
550
|
+
this.errorKeys.set(nhTwoFactorErrorKeys(result));
|
|
551
|
+
if (nhTwoFactorStepEnded(result)) {
|
|
552
|
+
this.expired.emit();
|
|
553
|
+
}
|
|
554
|
+
return undefined;
|
|
555
|
+
}
|
|
556
|
+
return result.data;
|
|
557
|
+
}
|
|
558
|
+
token() {
|
|
559
|
+
return this.enrollment().challengeToken;
|
|
560
|
+
}
|
|
561
|
+
focusCode() {
|
|
562
|
+
afterNextRender(() => this.codeInput()?.nativeElement.focus(), { injector: this.injector });
|
|
563
|
+
}
|
|
564
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhTwoFactorEnrollmentComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
565
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.28", type: NhTwoFactorEnrollmentComponent, isStandalone: true, selector: "nh-two-factor-enrollment", inputs: { enrollment: { classPropertyName: "enrollment", publicName: "enrollment", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { completed: "completed", expired: "expired", cancelled: "cancelled" }, viewQueries: [{ propertyName: "codeInput", first: true, predicate: ["codeInput"], descendants: true, isSignal: true }], ngImport: i0, template: "@if (result(); as completedResult) {\n <nh-two-factor-recovery-codes [codes]=\"completedResult.recoveryCodes ?? []\" (acknowledged)=\"finish()\" />\n} @else {\n <form class=\"nh-two-factor\" [attr.aria-labelledby]=\"id + '-title'\" [attr.aria-busy]=\"busy()\" (submit)=\"$event.preventDefault(); confirm()\">\n <h2 class=\"nh-two-factor__title\" [id]=\"id + '-title'\">{{ 'nh-two-factor.ui.enrollment-title' | translate }}</h2>\n <p class=\"nh-two-factor__text\">{{ 'nh-two-factor.ui.enrollment-instructions' | translate }}</p>\n\n @if (methods().length > 1) {\n <fieldset class=\"nh-two-factor__methods\">\n <legend class=\"nh-two-factor__legend\">{{ 'nh-two-factor.ui.method-label' | translate }}</legend>\n @for (option of methods(); track option) {\n <label class=\"nh-two-factor__method\">\n <input\n type=\"radio\"\n [name]=\"id + '-method'\"\n [value]=\"option\"\n [checked]=\"method() === option\"\n [disabled]=\"busy()\"\n (change)=\"selectMethod(option)\" />\n <span>{{ 'nh-two-factor.ui.method.' + option | translate }}</span>\n </label>\n }\n </fieldset>\n }\n\n @switch (method()) {\n @case ('authenticator') {\n <p class=\"nh-two-factor__hint\">{{ 'nh-two-factor.ui.authenticator-instructions' | translate }}</p>\n @if (setup(); as authenticator) {\n <div class=\"nh-two-factor__setup\">\n @if (authenticator.qrCodeDataUri) {\n <img class=\"nh-two-factor__qr\" [src]=\"authenticator.qrCodeDataUri\" [alt]=\"'nh-two-factor.ui.qr-code-alt' | translate\" />\n }\n <p class=\"nh-two-factor__text\">\n {{ 'nh-two-factor.ui.shared-key' | translate }}:\n <span class=\"nh-two-factor__key\">{{ authenticator.sharedKey }}</span>\n </p>\n </div>\n } @else if (busy()) {\n <p class=\"nh-two-factor__hint\" role=\"status\">{{ 'nh-two-factor.ui.loading' | translate }}</p>\n }\n }\n @case ('email') {\n <p class=\"nh-two-factor__hint\">{{ 'nh-two-factor.ui.email-instructions' | translate }}</p>\n <div class=\"nh-two-factor__actions\">\n <button type=\"button\" class=\"nh-two-factor__button\" [disabled]=\"busy()\" (click)=\"sendEmailCode()\">\n {{ (emailSent() ? 'nh-two-factor.ui.resend-email-code' : 'nh-two-factor.ui.send-email-code') | translate }}\n </button>\n </div>\n @if (emailSent(); as sent) {\n <p class=\"nh-two-factor__hint\" role=\"status\">\n {{ 'nh-two-factor.ui.email-code-sent' | translate: { time: (sent.expiresAt | date: 'shortTime') } }}\n </p>\n }\n }\n @case ('passkey') {\n <p class=\"nh-two-factor__hint\">{{ 'nh-two-factor.ui.passkey-instructions' | translate }}</p>\n <div class=\"nh-two-factor__field\">\n <label [for]=\"id + '-passkey-name'\">{{ 'nh-two-factor.ui.passkey-name' | translate }}</label>\n <input\n type=\"text\"\n class=\"nh-two-factor__input\"\n maxlength=\"100\"\n autocomplete=\"off\"\n [id]=\"id + '-passkey-name'\"\n [value]=\"passkeyName()\"\n [disabled]=\"busy()\"\n (input)=\"passkeyName.set(value($event))\" />\n </div>\n }\n }\n\n @if (method() !== 'passkey' && (method() !== 'authenticator' || setup())) {\n <div class=\"nh-two-factor__field\">\n <label [for]=\"id + '-code'\">{{ 'nh-two-factor.ui.code-label.' + method() | translate }}</label>\n <input\n #codeInput\n type=\"text\"\n class=\"nh-two-factor__input nh-two-factor__input--code\"\n inputmode=\"numeric\"\n autocomplete=\"one-time-code\"\n spellcheck=\"false\"\n [id]=\"id + '-code'\"\n [value]=\"code()\"\n [disabled]=\"busy()\"\n [attr.aria-invalid]=\"errorKeys().length > 0 ? true : null\"\n [attr.aria-describedby]=\"errorKeys().length > 0 ? id + '-errors' : null\"\n (input)=\"code.set(value($event))\" />\n </div>\n }\n\n <div [id]=\"id + '-errors'\" aria-live=\"assertive\">\n @if (errorKeys().length > 0) {\n <ul class=\"nh-two-factor__errors\" role=\"alert\">\n @for (key of errorKeys(); track key) {\n <li>{{ key | translate }}</li>\n }\n </ul>\n }\n </div>\n\n <div class=\"nh-two-factor__actions\">\n <button type=\"submit\" class=\"nh-two-factor__button nh-two-factor__button--primary\" [disabled]=\"busy()\">\n {{ (method() === 'passkey' ? 'nh-two-factor.ui.create-passkey' : 'nh-two-factor.ui.confirm') | translate }}\n </button>\n <button type=\"button\" class=\"nh-two-factor__button\" [disabled]=\"busy()\" (click)=\"cancelled.emit()\">\n {{ 'nh-two-factor.ui.cancel' | translate }}\n </button>\n </div>\n </form>\n}\n", styles: [":host{display:block;min-width:0;color:var(--nh-color-text, #1f2933)}.nh-two-factor{display:flex;flex-direction:column;gap:1rem;min-width:0}.nh-two-factor__title{margin:0;font-size:1.125rem;font-weight:650}.nh-two-factor__subtitle{margin:0;font-size:1rem;font-weight:650}.nh-two-factor__text,.nh-two-factor__hint{margin:0;overflow-wrap:anywhere}.nh-two-factor__hint{color:var(--nh-color-muted, #52606d);font-size:.875rem}.nh-two-factor__notice{margin:0;padding:.5rem .75rem;border-radius:.5rem;background:var(--nh-color-success-background, #e7f6ec);color:var(--nh-color-success, #1e6b3a)}.nh-two-factor__methods{display:flex;flex-wrap:wrap;gap:.5rem;min-width:0;margin:0;padding:0;border:0}.nh-two-factor__legend{width:100%;margin-bottom:.25rem;padding:0;font-weight:600}.nh-two-factor__method{position:relative}.nh-two-factor__method input{position:absolute;inset:0;opacity:0;cursor:pointer}.nh-two-factor__method span{display:inline-flex;padding:.5rem .75rem;border:1px solid var(--nh-color-border, #cbd2d9);border-radius:.5rem;background:var(--nh-color-surface, #fff)}.nh-two-factor__method input:checked+span{border-color:var(--nh-color-primary, #2563eb);box-shadow:inset 0 0 0 1px var(--nh-color-primary, #2563eb);font-weight:600}.nh-two-factor__method input:focus-visible+span{outline:2px solid var(--nh-color-focus, #2563eb);outline-offset:2px}.nh-two-factor__method input:disabled+span{opacity:.6}.nh-two-factor__field{display:flex;flex-direction:column;gap:.375rem;min-width:0}.nh-two-factor__field label{font-weight:600}.nh-two-factor__input{width:100%;max-width:20rem;padding:.5rem .75rem;border:1px solid var(--nh-color-border, #cbd2d9);border-radius:.5rem;background:var(--nh-color-input-background, #fff);color:inherit;font:inherit}.nh-two-factor__input:focus-visible{outline:2px solid var(--nh-color-focus, #2563eb);outline-offset:1px}.nh-two-factor__input[aria-invalid=true]{border-color:var(--nh-color-danger, #b91c1c)}.nh-two-factor__input--code{font-variant-numeric:tabular-nums;letter-spacing:.15em}.nh-two-factor__check{display:flex;align-items:center;gap:.5rem}.nh-two-factor__actions{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem}.nh-two-factor__button{padding:.5rem 1rem;border:1px solid var(--nh-color-border, #cbd2d9);border-radius:.5rem;background:var(--nh-color-surface, #fff);color:inherit;font:inherit;cursor:pointer}.nh-two-factor__button:focus-visible{outline:2px solid var(--nh-color-focus, #2563eb);outline-offset:2px}.nh-two-factor__button:disabled{opacity:.6;cursor:not-allowed}.nh-two-factor__button--primary{border-color:var(--nh-color-primary, #2563eb);background:var(--nh-color-primary, #2563eb);color:var(--nh-color-on-primary, #fff)}.nh-two-factor__button--danger{border-color:var(--nh-color-danger, #b91c1c);color:var(--nh-color-danger, #b91c1c)}.nh-two-factor__errors{margin:0;padding:.5rem .75rem;border-radius:.5rem;background:var(--nh-color-danger-background, #fdecec);color:var(--nh-color-danger, #b91c1c);list-style:none}.nh-two-factor__setup{display:flex;flex-wrap:wrap;align-items:flex-start;gap:1rem}.nh-two-factor__qr{width:11rem;height:11rem;padding:.5rem;border-radius:.5rem;background:#fff;image-rendering:pixelated}.nh-two-factor__key{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;overflow-wrap:anywhere;-webkit-user-select:all;user-select:all}.nh-two-factor__codes{display:grid;grid-template-columns:repeat(auto-fill,minmax(9rem,1fr));gap:.5rem;margin:0;padding:0;list-style:none;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}.nh-two-factor__codes li{padding:.375rem .5rem;border:1px dashed var(--nh-color-border, #cbd2d9);border-radius:.375rem;text-align:center}.nh-two-factor__section{display:flex;flex-direction:column;gap:.75rem;padding-top:1rem;border-top:1px solid var(--nh-color-border, #cbd2d9)}.nh-two-factor__status{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem}.nh-two-factor__badge{padding:.125rem .5rem;border-radius:999px;background:var(--nh-color-status-background, #eef2f7);font-size:.8125rem;font-weight:600}.nh-two-factor__badge--on{background:var(--nh-color-success-background, #e7f6ec);color:var(--nh-color-success, #1e6b3a)}.nh-two-factor__list{display:flex;flex-direction:column;gap:.5rem;margin:0;padding:0;list-style:none}.nh-two-factor__item{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:.5rem;min-width:0;padding:.75rem;border:1px solid var(--nh-color-border, #cbd2d9);border-radius:.5rem}.nh-two-factor__item>div{min-width:0}.visually-hidden{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}\n"], dependencies: [{ kind: "component", type: NhTwoFactorRecoveryCodesComponent, selector: "nh-two-factor-recovery-codes", inputs: ["codes", "fileName"], outputs: ["acknowledged"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }, { kind: "pipe", type: DatePipe, name: "date" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
566
|
+
}
|
|
567
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhTwoFactorEnrollmentComponent, decorators: [{
|
|
568
|
+
type: Component,
|
|
569
|
+
args: [{ selector: 'nh-two-factor-enrollment', standalone: true, imports: [TranslatePipe, DatePipe, NhTwoFactorRecoveryCodesComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (result(); as completedResult) {\n <nh-two-factor-recovery-codes [codes]=\"completedResult.recoveryCodes ?? []\" (acknowledged)=\"finish()\" />\n} @else {\n <form class=\"nh-two-factor\" [attr.aria-labelledby]=\"id + '-title'\" [attr.aria-busy]=\"busy()\" (submit)=\"$event.preventDefault(); confirm()\">\n <h2 class=\"nh-two-factor__title\" [id]=\"id + '-title'\">{{ 'nh-two-factor.ui.enrollment-title' | translate }}</h2>\n <p class=\"nh-two-factor__text\">{{ 'nh-two-factor.ui.enrollment-instructions' | translate }}</p>\n\n @if (methods().length > 1) {\n <fieldset class=\"nh-two-factor__methods\">\n <legend class=\"nh-two-factor__legend\">{{ 'nh-two-factor.ui.method-label' | translate }}</legend>\n @for (option of methods(); track option) {\n <label class=\"nh-two-factor__method\">\n <input\n type=\"radio\"\n [name]=\"id + '-method'\"\n [value]=\"option\"\n [checked]=\"method() === option\"\n [disabled]=\"busy()\"\n (change)=\"selectMethod(option)\" />\n <span>{{ 'nh-two-factor.ui.method.' + option | translate }}</span>\n </label>\n }\n </fieldset>\n }\n\n @switch (method()) {\n @case ('authenticator') {\n <p class=\"nh-two-factor__hint\">{{ 'nh-two-factor.ui.authenticator-instructions' | translate }}</p>\n @if (setup(); as authenticator) {\n <div class=\"nh-two-factor__setup\">\n @if (authenticator.qrCodeDataUri) {\n <img class=\"nh-two-factor__qr\" [src]=\"authenticator.qrCodeDataUri\" [alt]=\"'nh-two-factor.ui.qr-code-alt' | translate\" />\n }\n <p class=\"nh-two-factor__text\">\n {{ 'nh-two-factor.ui.shared-key' | translate }}:\n <span class=\"nh-two-factor__key\">{{ authenticator.sharedKey }}</span>\n </p>\n </div>\n } @else if (busy()) {\n <p class=\"nh-two-factor__hint\" role=\"status\">{{ 'nh-two-factor.ui.loading' | translate }}</p>\n }\n }\n @case ('email') {\n <p class=\"nh-two-factor__hint\">{{ 'nh-two-factor.ui.email-instructions' | translate }}</p>\n <div class=\"nh-two-factor__actions\">\n <button type=\"button\" class=\"nh-two-factor__button\" [disabled]=\"busy()\" (click)=\"sendEmailCode()\">\n {{ (emailSent() ? 'nh-two-factor.ui.resend-email-code' : 'nh-two-factor.ui.send-email-code') | translate }}\n </button>\n </div>\n @if (emailSent(); as sent) {\n <p class=\"nh-two-factor__hint\" role=\"status\">\n {{ 'nh-two-factor.ui.email-code-sent' | translate: { time: (sent.expiresAt | date: 'shortTime') } }}\n </p>\n }\n }\n @case ('passkey') {\n <p class=\"nh-two-factor__hint\">{{ 'nh-two-factor.ui.passkey-instructions' | translate }}</p>\n <div class=\"nh-two-factor__field\">\n <label [for]=\"id + '-passkey-name'\">{{ 'nh-two-factor.ui.passkey-name' | translate }}</label>\n <input\n type=\"text\"\n class=\"nh-two-factor__input\"\n maxlength=\"100\"\n autocomplete=\"off\"\n [id]=\"id + '-passkey-name'\"\n [value]=\"passkeyName()\"\n [disabled]=\"busy()\"\n (input)=\"passkeyName.set(value($event))\" />\n </div>\n }\n }\n\n @if (method() !== 'passkey' && (method() !== 'authenticator' || setup())) {\n <div class=\"nh-two-factor__field\">\n <label [for]=\"id + '-code'\">{{ 'nh-two-factor.ui.code-label.' + method() | translate }}</label>\n <input\n #codeInput\n type=\"text\"\n class=\"nh-two-factor__input nh-two-factor__input--code\"\n inputmode=\"numeric\"\n autocomplete=\"one-time-code\"\n spellcheck=\"false\"\n [id]=\"id + '-code'\"\n [value]=\"code()\"\n [disabled]=\"busy()\"\n [attr.aria-invalid]=\"errorKeys().length > 0 ? true : null\"\n [attr.aria-describedby]=\"errorKeys().length > 0 ? id + '-errors' : null\"\n (input)=\"code.set(value($event))\" />\n </div>\n }\n\n <div [id]=\"id + '-errors'\" aria-live=\"assertive\">\n @if (errorKeys().length > 0) {\n <ul class=\"nh-two-factor__errors\" role=\"alert\">\n @for (key of errorKeys(); track key) {\n <li>{{ key | translate }}</li>\n }\n </ul>\n }\n </div>\n\n <div class=\"nh-two-factor__actions\">\n <button type=\"submit\" class=\"nh-two-factor__button nh-two-factor__button--primary\" [disabled]=\"busy()\">\n {{ (method() === 'passkey' ? 'nh-two-factor.ui.create-passkey' : 'nh-two-factor.ui.confirm') | translate }}\n </button>\n <button type=\"button\" class=\"nh-two-factor__button\" [disabled]=\"busy()\" (click)=\"cancelled.emit()\">\n {{ 'nh-two-factor.ui.cancel' | translate }}\n </button>\n </div>\n </form>\n}\n", styles: [":host{display:block;min-width:0;color:var(--nh-color-text, #1f2933)}.nh-two-factor{display:flex;flex-direction:column;gap:1rem;min-width:0}.nh-two-factor__title{margin:0;font-size:1.125rem;font-weight:650}.nh-two-factor__subtitle{margin:0;font-size:1rem;font-weight:650}.nh-two-factor__text,.nh-two-factor__hint{margin:0;overflow-wrap:anywhere}.nh-two-factor__hint{color:var(--nh-color-muted, #52606d);font-size:.875rem}.nh-two-factor__notice{margin:0;padding:.5rem .75rem;border-radius:.5rem;background:var(--nh-color-success-background, #e7f6ec);color:var(--nh-color-success, #1e6b3a)}.nh-two-factor__methods{display:flex;flex-wrap:wrap;gap:.5rem;min-width:0;margin:0;padding:0;border:0}.nh-two-factor__legend{width:100%;margin-bottom:.25rem;padding:0;font-weight:600}.nh-two-factor__method{position:relative}.nh-two-factor__method input{position:absolute;inset:0;opacity:0;cursor:pointer}.nh-two-factor__method span{display:inline-flex;padding:.5rem .75rem;border:1px solid var(--nh-color-border, #cbd2d9);border-radius:.5rem;background:var(--nh-color-surface, #fff)}.nh-two-factor__method input:checked+span{border-color:var(--nh-color-primary, #2563eb);box-shadow:inset 0 0 0 1px var(--nh-color-primary, #2563eb);font-weight:600}.nh-two-factor__method input:focus-visible+span{outline:2px solid var(--nh-color-focus, #2563eb);outline-offset:2px}.nh-two-factor__method input:disabled+span{opacity:.6}.nh-two-factor__field{display:flex;flex-direction:column;gap:.375rem;min-width:0}.nh-two-factor__field label{font-weight:600}.nh-two-factor__input{width:100%;max-width:20rem;padding:.5rem .75rem;border:1px solid var(--nh-color-border, #cbd2d9);border-radius:.5rem;background:var(--nh-color-input-background, #fff);color:inherit;font:inherit}.nh-two-factor__input:focus-visible{outline:2px solid var(--nh-color-focus, #2563eb);outline-offset:1px}.nh-two-factor__input[aria-invalid=true]{border-color:var(--nh-color-danger, #b91c1c)}.nh-two-factor__input--code{font-variant-numeric:tabular-nums;letter-spacing:.15em}.nh-two-factor__check{display:flex;align-items:center;gap:.5rem}.nh-two-factor__actions{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem}.nh-two-factor__button{padding:.5rem 1rem;border:1px solid var(--nh-color-border, #cbd2d9);border-radius:.5rem;background:var(--nh-color-surface, #fff);color:inherit;font:inherit;cursor:pointer}.nh-two-factor__button:focus-visible{outline:2px solid var(--nh-color-focus, #2563eb);outline-offset:2px}.nh-two-factor__button:disabled{opacity:.6;cursor:not-allowed}.nh-two-factor__button--primary{border-color:var(--nh-color-primary, #2563eb);background:var(--nh-color-primary, #2563eb);color:var(--nh-color-on-primary, #fff)}.nh-two-factor__button--danger{border-color:var(--nh-color-danger, #b91c1c);color:var(--nh-color-danger, #b91c1c)}.nh-two-factor__errors{margin:0;padding:.5rem .75rem;border-radius:.5rem;background:var(--nh-color-danger-background, #fdecec);color:var(--nh-color-danger, #b91c1c);list-style:none}.nh-two-factor__setup{display:flex;flex-wrap:wrap;align-items:flex-start;gap:1rem}.nh-two-factor__qr{width:11rem;height:11rem;padding:.5rem;border-radius:.5rem;background:#fff;image-rendering:pixelated}.nh-two-factor__key{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;overflow-wrap:anywhere;-webkit-user-select:all;user-select:all}.nh-two-factor__codes{display:grid;grid-template-columns:repeat(auto-fill,minmax(9rem,1fr));gap:.5rem;margin:0;padding:0;list-style:none;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}.nh-two-factor__codes li{padding:.375rem .5rem;border:1px dashed var(--nh-color-border, #cbd2d9);border-radius:.375rem;text-align:center}.nh-two-factor__section{display:flex;flex-direction:column;gap:.75rem;padding-top:1rem;border-top:1px solid var(--nh-color-border, #cbd2d9)}.nh-two-factor__status{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem}.nh-two-factor__badge{padding:.125rem .5rem;border-radius:999px;background:var(--nh-color-status-background, #eef2f7);font-size:.8125rem;font-weight:600}.nh-two-factor__badge--on{background:var(--nh-color-success-background, #e7f6ec);color:var(--nh-color-success, #1e6b3a)}.nh-two-factor__list{display:flex;flex-direction:column;gap:.5rem;margin:0;padding:0;list-style:none}.nh-two-factor__item{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:.5rem;min-width:0;padding:.75rem;border:1px solid var(--nh-color-border, #cbd2d9);border-radius:.5rem}.nh-two-factor__item>div{min-width:0}.visually-hidden{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}\n"] }]
|
|
570
|
+
}], ctorParameters: () => [], propDecorators: { codeInput: [{ type: i0.ViewChild, args: ['codeInput', { isSignal: true }] }], enrollment: [{ type: i0.Input, args: [{ isSignal: true, alias: "enrollment", required: true }] }], completed: [{ type: i0.Output, args: ["completed"] }], expired: [{ type: i0.Output, args: ["expired"] }], cancelled: [{ type: i0.Output, args: ["cancelled"] }] } });
|
|
571
|
+
|
|
572
|
+
const passwordReauthentication = 'password';
|
|
573
|
+
let nextId$1 = 0;
|
|
574
|
+
/**
|
|
575
|
+
* Two-factor settings of the signed-in user: authenticator app, e-mail codes, passkeys,
|
|
576
|
+
* recovery codes, remembered devices and turning two-factor authentication off. Sensitive
|
|
577
|
+
* changes ask for the password or a code first.
|
|
578
|
+
*/
|
|
579
|
+
class NhTwoFactorSettingsComponent {
|
|
580
|
+
constructor() {
|
|
581
|
+
this.auth = inject(NhAuthService);
|
|
582
|
+
this.injector = inject(Injector);
|
|
583
|
+
this.reauthenticationInput = viewChild('reauthenticationInput', ...(ngDevMode ? [{ debugName: "reauthenticationInput" }] : []));
|
|
584
|
+
this.codeInput = viewChild('codeInput', ...(ngDevMode ? [{ debugName: "codeInput" }] : []));
|
|
585
|
+
/** Whether the title is visible. Hosts with their own heading hide it; screen readers keep it. */
|
|
586
|
+
this.showTitle = input(true, ...(ngDevMode ? [{ debugName: "showTitle" }] : []));
|
|
587
|
+
/** Emits the reloaded status after every saved change. */
|
|
588
|
+
this.changed = output();
|
|
589
|
+
this.id = `nh-two-factor-settings-${nextId$1++}`;
|
|
590
|
+
this.passkeySupported = this.auth.isPasskeySupported();
|
|
591
|
+
this.status = signal(null, ...(ngDevMode ? [{ debugName: "status" }] : []));
|
|
592
|
+
this.passkeys = signal([], ...(ngDevMode ? [{ debugName: "passkeys" }] : []));
|
|
593
|
+
this.loading = signal(true, ...(ngDevMode ? [{ debugName: "loading" }] : []));
|
|
594
|
+
this.busy = signal(false, ...(ngDevMode ? [{ debugName: "busy" }] : []));
|
|
595
|
+
this.errorKeys = signal([], ...(ngDevMode ? [{ debugName: "errorKeys" }] : []));
|
|
596
|
+
this.noticeKey = signal(null, ...(ngDevMode ? [{ debugName: "noticeKey" }] : []));
|
|
597
|
+
this.setupPanel = signal(null, ...(ngDevMode ? [{ debugName: "setupPanel" }] : []));
|
|
598
|
+
this.setup = signal(null, ...(ngDevMode ? [{ debugName: "setup" }] : []));
|
|
599
|
+
this.emailSent = signal(null, ...(ngDevMode ? [{ debugName: "emailSent" }] : []));
|
|
600
|
+
this.code = signal('', ...(ngDevMode ? [{ debugName: "code" }] : []));
|
|
601
|
+
this.recoveryCodes = signal(null, ...(ngDevMode ? [{ debugName: "recoveryCodes" }] : []));
|
|
602
|
+
this.pendingAction = signal(null, ...(ngDevMode ? [{ debugName: "pendingAction" }] : []));
|
|
603
|
+
this.reauthenticationMethod = signal(passwordReauthentication, ...(ngDevMode ? [{ debugName: "reauthenticationMethod" }] : []));
|
|
604
|
+
this.reauthenticationSecret = signal('', ...(ngDevMode ? [{ debugName: "reauthenticationSecret" }] : []));
|
|
605
|
+
this.newPasskeyName = signal('', ...(ngDevMode ? [{ debugName: "newPasskeyName" }] : []));
|
|
606
|
+
this.renamingId = signal(null, ...(ngDevMode ? [{ debugName: "renamingId" }] : []));
|
|
607
|
+
this.renameValue = signal('', ...(ngDevMode ? [{ debugName: "renameValue" }] : []));
|
|
608
|
+
this.enabled = computed(() => this.status()?.enabled === true, ...(ngDevMode ? [{ debugName: "enabled" }] : []));
|
|
609
|
+
this.required = computed(() => this.status()?.required === true, ...(ngDevMode ? [{ debugName: "required" }] : []));
|
|
610
|
+
this.hasAuthenticator = computed(() => this.hasMethod(NhTwoFactorMethods.authenticator), ...(ngDevMode ? [{ debugName: "hasAuthenticator" }] : []));
|
|
611
|
+
this.hasEmail = computed(() => this.hasMethod(NhTwoFactorMethods.email), ...(ngDevMode ? [{ debugName: "hasEmail" }] : []));
|
|
612
|
+
this.authenticatorAvailable = computed(() => this.isAvailable(NhTwoFactorMethods.authenticator), ...(ngDevMode ? [{ debugName: "authenticatorAvailable" }] : []));
|
|
613
|
+
this.emailAvailable = computed(() => this.isAvailable(NhTwoFactorMethods.email), ...(ngDevMode ? [{ debugName: "emailAvailable" }] : []));
|
|
614
|
+
this.passkeysAvailable = computed(() => this.isAvailable(NhTwoFactorMethods.passkey), ...(ngDevMode ? [{ debugName: "passkeysAvailable" }] : []));
|
|
615
|
+
this.recoveryCodesAvailable = computed(() => this.isAvailable(NhTwoFactorMethods.recoveryCode), ...(ngDevMode ? [{ debugName: "recoveryCodesAvailable" }] : []));
|
|
616
|
+
this.reauthenticationMethods = computed(() => [
|
|
617
|
+
passwordReauthentication,
|
|
618
|
+
...(this.status()?.methods ?? []).filter(method => method === NhTwoFactorMethods.authenticator || method === NhTwoFactorMethods.recoveryCode)
|
|
619
|
+
], ...(ngDevMode ? [{ debugName: "reauthenticationMethods" }] : []));
|
|
620
|
+
this.value = nhTwoFactorInputValue;
|
|
621
|
+
inject(NhTwoFactorTranslationMerger).start();
|
|
622
|
+
}
|
|
623
|
+
ngOnInit() {
|
|
624
|
+
void this.load();
|
|
625
|
+
}
|
|
626
|
+
async load() {
|
|
627
|
+
this.loading.set(true);
|
|
628
|
+
const status = await this.auth.getTwoFactorStatus();
|
|
629
|
+
if (!status.isSuccess || !status.data) {
|
|
630
|
+
this.errorKeys.set(nhTwoFactorErrorKeys(status));
|
|
631
|
+
this.loading.set(false);
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
this.status.set(status.data);
|
|
635
|
+
if (status.data.availableMethods.includes(NhTwoFactorMethods.passkey)) {
|
|
636
|
+
const passkeys = await this.auth.getPasskeys();
|
|
637
|
+
this.passkeys.set(passkeys.data ?? []);
|
|
638
|
+
}
|
|
639
|
+
this.loading.set(false);
|
|
640
|
+
}
|
|
641
|
+
/** Starts a change, asking for reauthentication first when the change needs it. */
|
|
642
|
+
request(action) {
|
|
643
|
+
this.errorKeys.set([]);
|
|
644
|
+
this.noticeKey.set(null);
|
|
645
|
+
if (!this.needsReauthentication(action)) {
|
|
646
|
+
void this.perform(action);
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
this.pendingAction.set(action);
|
|
650
|
+
this.reauthenticationMethod.set(passwordReauthentication);
|
|
651
|
+
this.reauthenticationSecret.set('');
|
|
652
|
+
afterNextRender(() => this.reauthenticationInput()?.nativeElement.focus(), { injector: this.injector });
|
|
653
|
+
}
|
|
654
|
+
async confirmReauthentication() {
|
|
655
|
+
const action = this.pendingAction();
|
|
656
|
+
const secret = this.reauthenticationSecret().trim();
|
|
657
|
+
if (!action || this.busy()) {
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
if (!secret) {
|
|
661
|
+
this.errorKeys.set(['nh-two-factor.reauthentication-required']);
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
const method = this.reauthenticationMethod();
|
|
665
|
+
const reauthentication = method === passwordReauthentication
|
|
666
|
+
? new NhTwoFactorReauthentication({ password: secret })
|
|
667
|
+
: new NhTwoFactorReauthentication({ method: method, code: secret });
|
|
668
|
+
this.pendingAction.set(null);
|
|
669
|
+
this.reauthenticationSecret.set('');
|
|
670
|
+
await this.perform(action, reauthentication);
|
|
671
|
+
}
|
|
672
|
+
cancelReauthentication() {
|
|
673
|
+
this.pendingAction.set(null);
|
|
674
|
+
this.reauthenticationSecret.set('');
|
|
675
|
+
this.errorKeys.set([]);
|
|
676
|
+
}
|
|
677
|
+
async confirmSetup() {
|
|
678
|
+
const code = this.code().trim();
|
|
679
|
+
if (!code) {
|
|
680
|
+
this.errorKeys.set(['nh-two-factor.invalid-code']);
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
const change = await this.run(() => this.setupPanel() === 'email'
|
|
684
|
+
? this.auth.confirmEmailSetup(code)
|
|
685
|
+
: this.auth.confirmAuthenticator(code));
|
|
686
|
+
this.code.set('');
|
|
687
|
+
if (change) {
|
|
688
|
+
this.closeSetup();
|
|
689
|
+
await this.applyChange(change);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
closeSetup() {
|
|
693
|
+
this.setupPanel.set(null);
|
|
694
|
+
this.setup.set(null);
|
|
695
|
+
this.emailSent.set(null);
|
|
696
|
+
this.code.set('');
|
|
697
|
+
this.emailReauthentication = undefined;
|
|
698
|
+
}
|
|
699
|
+
async resendEmailCode() {
|
|
700
|
+
const sent = await this.run(() => this.auth.beginEmailSetup(this.emailReauthentication));
|
|
701
|
+
if (sent) {
|
|
702
|
+
this.emailSent.set(sent);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
startRename(passkey) {
|
|
706
|
+
this.renamingId.set(passkey.id);
|
|
707
|
+
this.renameValue.set(passkey.name);
|
|
708
|
+
}
|
|
709
|
+
cancelRename() {
|
|
710
|
+
this.renamingId.set(null);
|
|
711
|
+
this.renameValue.set('');
|
|
712
|
+
}
|
|
713
|
+
async saveRename(passkey) {
|
|
714
|
+
const name = this.renameValue().trim();
|
|
715
|
+
if (!name || this.busy()) {
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
this.busy.set(true);
|
|
719
|
+
this.errorKeys.set([]);
|
|
720
|
+
const result = await this.auth.renamePasskey(passkey.id, name);
|
|
721
|
+
this.busy.set(false);
|
|
722
|
+
if (!result.isSuccess) {
|
|
723
|
+
this.errorKeys.set(nhTwoFactorErrorKeys(result));
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
this.cancelRename();
|
|
727
|
+
this.passkeys.update(passkeys => passkeys.map(item => item.id === passkey.id ? new NhPasskey({ ...item, name: name }) : item));
|
|
728
|
+
this.noticeKey.set('nh-two-factor.ui.change-saved');
|
|
729
|
+
}
|
|
730
|
+
needsReauthentication(action) {
|
|
731
|
+
switch (action.kind) {
|
|
732
|
+
case 'authenticator':
|
|
733
|
+
case 'email':
|
|
734
|
+
case 'add-passkey':
|
|
735
|
+
return this.enabled();
|
|
736
|
+
default:
|
|
737
|
+
return true;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
async perform(action, reauthentication) {
|
|
741
|
+
switch (action.kind) {
|
|
742
|
+
case 'authenticator': {
|
|
743
|
+
const setup = await this.run(() => this.auth.beginAuthenticatorSetup(reauthentication));
|
|
744
|
+
if (setup) {
|
|
745
|
+
this.openSetup('authenticator');
|
|
746
|
+
this.setup.set(setup);
|
|
747
|
+
}
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
case 'email': {
|
|
751
|
+
const sent = await this.run(() => this.auth.beginEmailSetup(reauthentication));
|
|
752
|
+
if (sent) {
|
|
753
|
+
this.openSetup('email');
|
|
754
|
+
this.emailSent.set(sent);
|
|
755
|
+
this.emailReauthentication = reauthentication;
|
|
756
|
+
}
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
case 'recovery-codes':
|
|
760
|
+
await this.applyChange(await this.run(() => this.auth.regenerateRecoveryCodes(reauthentication)));
|
|
761
|
+
return;
|
|
762
|
+
case 'forget-devices':
|
|
763
|
+
await this.applyChange(await this.run(() => this.auth.forgetTwoFactorDevices(reauthentication)), 'nh-two-factor.ui.devices-forgotten');
|
|
764
|
+
return;
|
|
765
|
+
case 'disable':
|
|
766
|
+
await this.applyChange(await this.run(() => this.auth.disableTwoFactor(reauthentication)));
|
|
767
|
+
return;
|
|
768
|
+
case 'add-passkey': {
|
|
769
|
+
const name = this.newPasskeyName().trim() || undefined;
|
|
770
|
+
const change = await this.run(() => this.auth.registerPasskey(name, reauthentication));
|
|
771
|
+
if (change) {
|
|
772
|
+
this.newPasskeyName.set('');
|
|
773
|
+
}
|
|
774
|
+
await this.applyChange(change);
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
case 'remove-passkey':
|
|
778
|
+
await this.applyChange(await this.run(() => this.auth.removePasskey(action.passkeyId, reauthentication)));
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
openSetup(panel) {
|
|
783
|
+
this.closeSetup();
|
|
784
|
+
this.setupPanel.set(panel);
|
|
785
|
+
afterNextRender(() => this.codeInput()?.nativeElement.focus(), { injector: this.injector });
|
|
786
|
+
}
|
|
787
|
+
async applyChange(change, noticeKey) {
|
|
788
|
+
if (!change) {
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
this.recoveryCodes.set(change.recoveryCodes?.length ? change.recoveryCodes : null);
|
|
792
|
+
this.noticeKey.set(noticeKey ?? (change.sessionRenewed ? 'nh-two-factor.ui.session-renewed' : 'nh-two-factor.ui.change-saved'));
|
|
793
|
+
await this.load();
|
|
794
|
+
const status = this.status();
|
|
795
|
+
if (status) {
|
|
796
|
+
this.changed.emit(status);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
async run(action) {
|
|
800
|
+
this.busy.set(true);
|
|
801
|
+
this.errorKeys.set([]);
|
|
802
|
+
const result = await action();
|
|
803
|
+
this.busy.set(false);
|
|
804
|
+
if (!result.isSuccess) {
|
|
805
|
+
this.errorKeys.set(nhTwoFactorErrorKeys(result));
|
|
806
|
+
return undefined;
|
|
807
|
+
}
|
|
808
|
+
return result.data;
|
|
809
|
+
}
|
|
810
|
+
hasMethod(method) {
|
|
811
|
+
return this.status()?.methods.includes(method) === true;
|
|
812
|
+
}
|
|
813
|
+
isAvailable(method) {
|
|
814
|
+
return this.status()?.availableMethods.includes(method) === true;
|
|
815
|
+
}
|
|
816
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhTwoFactorSettingsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
817
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.28", type: NhTwoFactorSettingsComponent, isStandalone: true, selector: "nh-two-factor-settings", inputs: { showTitle: { classPropertyName: "showTitle", publicName: "showTitle", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { changed: "changed" }, viewQueries: [{ propertyName: "reauthenticationInput", first: true, predicate: ["reauthenticationInput"], descendants: true, isSignal: true }, { propertyName: "codeInput", first: true, predicate: ["codeInput"], descendants: true, isSignal: true }], ngImport: i0, template: "<section class=\"nh-two-factor\" [attr.aria-labelledby]=\"id + '-title'\" [attr.aria-busy]=\"loading() || busy()\">\n <div class=\"nh-two-factor__status\">\n <h2 class=\"nh-two-factor__title\" [class.visually-hidden]=\"!showTitle()\" [id]=\"id + '-title'\">{{ 'nh-two-factor.ui.settings-title' | translate }}</h2>\n @if (status(); as current) {\n <span class=\"nh-two-factor__badge\" [class.nh-two-factor__badge--on]=\"current.enabled\">\n {{ (current.enabled ? 'nh-two-factor.ui.status-enabled' : 'nh-two-factor.ui.status-disabled') | translate }}\n </span>\n @if (current.required) {\n <span class=\"nh-two-factor__badge\">{{ 'nh-two-factor.ui.status-required' | translate }}</span>\n }\n }\n </div>\n\n @if (loading() && !status()) {\n <p class=\"nh-two-factor__hint\" role=\"status\">{{ 'nh-two-factor.ui.loading' | translate }}</p>\n }\n\n <div role=\"status\">\n @if (noticeKey(); as notice) {\n <p class=\"nh-two-factor__notice\">{{ notice | translate }}</p>\n }\n </div>\n\n <div [id]=\"id + '-errors'\" aria-live=\"assertive\">\n @if (errorKeys().length > 0) {\n <ul class=\"nh-two-factor__errors\" role=\"alert\">\n @for (key of errorKeys(); track key) {\n <li>{{ key | translate }}</li>\n }\n </ul>\n }\n </div>\n\n @if (pendingAction()) {\n <form class=\"nh-two-factor nh-two-factor__section\" [attr.aria-labelledby]=\"id + '-reauthentication-title'\" (submit)=\"$event.preventDefault(); confirmReauthentication()\">\n <h3 class=\"nh-two-factor__subtitle\" [id]=\"id + '-reauthentication-title'\">{{ 'nh-two-factor.ui.reauthentication-title' | translate }}</h3>\n\n @if (reauthenticationMethods().length > 1) {\n <fieldset class=\"nh-two-factor__methods\">\n <legend class=\"nh-two-factor__legend\">{{ 'nh-two-factor.ui.reauthentication-method' | translate }}</legend>\n @for (option of reauthenticationMethods(); track option) {\n <label class=\"nh-two-factor__method\">\n <input\n type=\"radio\"\n [name]=\"id + '-reauthentication-method'\"\n [value]=\"option\"\n [checked]=\"reauthenticationMethod() === option\"\n (change)=\"reauthenticationMethod.set(option); reauthenticationSecret.set('')\" />\n <span>{{ (option === 'password' ? 'nh-two-factor.ui.password' : 'nh-two-factor.ui.method.' + option) | translate }}</span>\n </label>\n }\n </fieldset>\n }\n\n <div class=\"nh-two-factor__field\">\n <label [for]=\"id + '-reauthentication'\">\n {{ (reauthenticationMethod() === 'password' ? 'nh-two-factor.ui.password' : 'nh-two-factor.ui.code-label.' + reauthenticationMethod()) | translate }}\n </label>\n <input\n #reauthenticationInput\n class=\"nh-two-factor__input\"\n [id]=\"id + '-reauthentication'\"\n [type]=\"reauthenticationMethod() === 'password' ? 'password' : 'text'\"\n [attr.autocomplete]=\"reauthenticationMethod() === 'password' ? 'current-password' : 'one-time-code'\"\n [value]=\"reauthenticationSecret()\"\n [disabled]=\"busy()\"\n (input)=\"reauthenticationSecret.set(value($event))\" />\n </div>\n\n <div class=\"nh-two-factor__actions\">\n <button type=\"submit\" class=\"nh-two-factor__button nh-two-factor__button--primary\" [disabled]=\"busy()\">\n {{ 'nh-two-factor.ui.confirm' | translate }}\n </button>\n <button type=\"button\" class=\"nh-two-factor__button\" [disabled]=\"busy()\" (click)=\"cancelReauthentication()\">\n {{ 'nh-two-factor.ui.cancel' | translate }}\n </button>\n </div>\n </form>\n }\n\n @if (recoveryCodes(); as codes) {\n <div class=\"nh-two-factor__section\">\n <nh-two-factor-recovery-codes [codes]=\"codes\" (acknowledged)=\"recoveryCodes.set(null)\" />\n </div>\n }\n\n @if (setupPanel(); as panel) {\n <form class=\"nh-two-factor nh-two-factor__section\" [attr.aria-labelledby]=\"id + '-setup-title'\" (submit)=\"$event.preventDefault(); confirmSetup()\">\n <h3 class=\"nh-two-factor__subtitle\" [id]=\"id + '-setup-title'\">{{ 'nh-two-factor.ui.method.' + panel | translate }}</h3>\n\n @if (panel === 'authenticator') {\n <p class=\"nh-two-factor__hint\">{{ 'nh-two-factor.ui.authenticator-instructions' | translate }}</p>\n @if (setup(); as authenticator) {\n <div class=\"nh-two-factor__setup\">\n @if (authenticator.qrCodeDataUri) {\n <img class=\"nh-two-factor__qr\" [src]=\"authenticator.qrCodeDataUri\" [alt]=\"'nh-two-factor.ui.qr-code-alt' | translate\" />\n }\n <p class=\"nh-two-factor__text\">\n {{ 'nh-two-factor.ui.shared-key' | translate }}:\n <span class=\"nh-two-factor__key\">{{ authenticator.sharedKey }}</span>\n </p>\n </div>\n }\n } @else {\n <p class=\"nh-two-factor__hint\">{{ 'nh-two-factor.ui.email-instructions' | translate }}</p>\n @if (emailSent(); as sent) {\n <p class=\"nh-two-factor__hint\">\n {{ 'nh-two-factor.ui.email-code-sent' | translate: { time: (sent.expiresAt | date: 'shortTime') } }}\n </p>\n }\n }\n\n <div class=\"nh-two-factor__field\">\n <label [for]=\"id + '-code'\">{{ 'nh-two-factor.ui.code-label.' + panel | translate }}</label>\n <input\n #codeInput\n type=\"text\"\n class=\"nh-two-factor__input nh-two-factor__input--code\"\n inputmode=\"numeric\"\n autocomplete=\"one-time-code\"\n spellcheck=\"false\"\n [id]=\"id + '-code'\"\n [value]=\"code()\"\n [disabled]=\"busy()\"\n (input)=\"code.set(value($event))\" />\n </div>\n\n <div class=\"nh-two-factor__actions\">\n <button type=\"submit\" class=\"nh-two-factor__button nh-two-factor__button--primary\" [disabled]=\"busy()\">\n {{ 'nh-two-factor.ui.confirm' | translate }}\n </button>\n @if (panel === 'email') {\n <button type=\"button\" class=\"nh-two-factor__button\" [disabled]=\"busy()\" (click)=\"resendEmailCode()\">\n {{ 'nh-two-factor.ui.resend-email-code' | translate }}\n </button>\n }\n <button type=\"button\" class=\"nh-two-factor__button\" [disabled]=\"busy()\" (click)=\"closeSetup()\">\n {{ 'nh-two-factor.ui.cancel' | translate }}\n </button>\n </div>\n </form>\n }\n\n @if (status(); as current) {\n @if (!setupPanel() && !pendingAction()) {\n <div class=\"nh-two-factor__actions\">\n @if (authenticatorAvailable()) {\n <button type=\"button\" class=\"nh-two-factor__button\" [disabled]=\"busy()\" (click)=\"request({ kind: 'authenticator' })\">\n {{ (hasAuthenticator() ? 'nh-two-factor.ui.change-authenticator' : 'nh-two-factor.ui.set-up-authenticator') | translate }}\n </button>\n }\n @if (emailAvailable() && !hasEmail()) {\n <button type=\"button\" class=\"nh-two-factor__button\" [disabled]=\"busy()\" (click)=\"request({ kind: 'email' })\">\n {{ 'nh-two-factor.ui.set-up-email' | translate }}\n </button>\n }\n </div>\n }\n\n @if (passkeysAvailable()) {\n <div class=\"nh-two-factor__section\">\n <h3 class=\"nh-two-factor__subtitle\">{{ 'nh-two-factor.ui.passkeys-title' | translate }}</h3>\n\n @if (passkeys().length === 0) {\n <p class=\"nh-two-factor__hint\">{{ 'nh-two-factor.ui.no-passkeys' | translate }}</p>\n } @else {\n <ul class=\"nh-two-factor__list\">\n @for (passkey of passkeys(); track passkey.id) {\n <li class=\"nh-two-factor__item\">\n @if (renamingId() === passkey.id) {\n <form class=\"nh-two-factor__actions\" (submit)=\"$event.preventDefault(); saveRename(passkey)\">\n <label class=\"visually-hidden\" [for]=\"id + '-rename-' + $index\">{{ 'nh-two-factor.ui.passkey-name' | translate }}</label>\n <input\n type=\"text\"\n class=\"nh-two-factor__input\"\n maxlength=\"100\"\n [id]=\"id + '-rename-' + $index\"\n [value]=\"renameValue()\"\n [disabled]=\"busy()\"\n (input)=\"renameValue.set(value($event))\" />\n <button type=\"submit\" class=\"nh-two-factor__button nh-two-factor__button--primary\" [disabled]=\"busy()\">\n {{ 'nh-two-factor.ui.save' | translate }}\n </button>\n <button type=\"button\" class=\"nh-two-factor__button\" [disabled]=\"busy()\" (click)=\"cancelRename()\">\n {{ 'nh-two-factor.ui.cancel' | translate }}\n </button>\n </form>\n } @else {\n <div>\n <p class=\"nh-two-factor__text\"><strong>{{ passkey.name }}</strong></p>\n <p class=\"nh-two-factor__hint\">\n {{ 'nh-two-factor.ui.passkey-created' | translate: { date: (passkey.createdAt | date: 'mediumDate') } }}\n @if (passkey.isBackedUp) {\n \u00B7 {{ 'nh-two-factor.ui.passkey-synced' | translate }}\n }\n </p>\n </div>\n <div class=\"nh-two-factor__actions\">\n <button type=\"button\" class=\"nh-two-factor__button\" [disabled]=\"busy()\" (click)=\"startRename(passkey)\">\n {{ 'nh-two-factor.ui.rename' | translate }}\n </button>\n <button type=\"button\" class=\"nh-two-factor__button nh-two-factor__button--danger\" [disabled]=\"busy()\" (click)=\"request({ kind: 'remove-passkey', passkeyId: passkey.id })\">\n {{ 'nh-two-factor.ui.remove' | translate }}\n </button>\n </div>\n }\n </li>\n }\n </ul>\n }\n\n @if (passkeySupported && !pendingAction()) {\n <form class=\"nh-two-factor__actions\" (submit)=\"$event.preventDefault(); request({ kind: 'add-passkey' })\">\n <label class=\"visually-hidden\" [for]=\"id + '-new-passkey'\">{{ 'nh-two-factor.ui.passkey-name' | translate }}</label>\n <input\n type=\"text\"\n class=\"nh-two-factor__input\"\n maxlength=\"100\"\n autocomplete=\"off\"\n [id]=\"id + '-new-passkey'\"\n [attr.placeholder]=\"'nh-two-factor.ui.passkey-name' | translate\"\n [value]=\"newPasskeyName()\"\n [disabled]=\"busy()\"\n (input)=\"newPasskeyName.set(value($event))\" />\n <button type=\"submit\" class=\"nh-two-factor__button\" [disabled]=\"busy()\">\n {{ 'nh-two-factor.ui.add-passkey' | translate }}\n </button>\n </form>\n }\n </div>\n }\n\n @if (current.enabled && !pendingAction()) {\n <div class=\"nh-two-factor__section\">\n @if (recoveryCodesAvailable()) {\n <p class=\"nh-two-factor__hint\">{{ 'nh-two-factor.ui.recovery-codes-left' | translate: { count: current.recoveryCodesLeft } }}</p>\n }\n <div class=\"nh-two-factor__actions\">\n @if (recoveryCodesAvailable()) {\n <button type=\"button\" class=\"nh-two-factor__button\" [disabled]=\"busy()\" (click)=\"request({ kind: 'recovery-codes' })\">\n {{ 'nh-two-factor.ui.regenerate-recovery-codes' | translate }}\n </button>\n }\n @if (current.rememberDeviceAvailable) {\n <button type=\"button\" class=\"nh-two-factor__button\" [disabled]=\"busy()\" (click)=\"request({ kind: 'forget-devices' })\">\n {{ 'nh-two-factor.ui.forget-devices' | translate }}\n </button>\n }\n @if (!required()) {\n <button type=\"button\" class=\"nh-two-factor__button nh-two-factor__button--danger\" [disabled]=\"busy()\" (click)=\"request({ kind: 'disable' })\">\n {{ 'nh-two-factor.ui.disable' | translate }}\n </button>\n }\n </div>\n </div>\n }\n }\n</section>\n", styles: [":host{display:block;min-width:0;color:var(--nh-color-text, #1f2933)}.nh-two-factor{display:flex;flex-direction:column;gap:1rem;min-width:0}.nh-two-factor__title{margin:0;font-size:1.125rem;font-weight:650}.nh-two-factor__subtitle{margin:0;font-size:1rem;font-weight:650}.nh-two-factor__text,.nh-two-factor__hint{margin:0;overflow-wrap:anywhere}.nh-two-factor__hint{color:var(--nh-color-muted, #52606d);font-size:.875rem}.nh-two-factor__notice{margin:0;padding:.5rem .75rem;border-radius:.5rem;background:var(--nh-color-success-background, #e7f6ec);color:var(--nh-color-success, #1e6b3a)}.nh-two-factor__methods{display:flex;flex-wrap:wrap;gap:.5rem;min-width:0;margin:0;padding:0;border:0}.nh-two-factor__legend{width:100%;margin-bottom:.25rem;padding:0;font-weight:600}.nh-two-factor__method{position:relative}.nh-two-factor__method input{position:absolute;inset:0;opacity:0;cursor:pointer}.nh-two-factor__method span{display:inline-flex;padding:.5rem .75rem;border:1px solid var(--nh-color-border, #cbd2d9);border-radius:.5rem;background:var(--nh-color-surface, #fff)}.nh-two-factor__method input:checked+span{border-color:var(--nh-color-primary, #2563eb);box-shadow:inset 0 0 0 1px var(--nh-color-primary, #2563eb);font-weight:600}.nh-two-factor__method input:focus-visible+span{outline:2px solid var(--nh-color-focus, #2563eb);outline-offset:2px}.nh-two-factor__method input:disabled+span{opacity:.6}.nh-two-factor__field{display:flex;flex-direction:column;gap:.375rem;min-width:0}.nh-two-factor__field label{font-weight:600}.nh-two-factor__input{width:100%;max-width:20rem;padding:.5rem .75rem;border:1px solid var(--nh-color-border, #cbd2d9);border-radius:.5rem;background:var(--nh-color-input-background, #fff);color:inherit;font:inherit}.nh-two-factor__input:focus-visible{outline:2px solid var(--nh-color-focus, #2563eb);outline-offset:1px}.nh-two-factor__input[aria-invalid=true]{border-color:var(--nh-color-danger, #b91c1c)}.nh-two-factor__input--code{font-variant-numeric:tabular-nums;letter-spacing:.15em}.nh-two-factor__check{display:flex;align-items:center;gap:.5rem}.nh-two-factor__actions{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem}.nh-two-factor__button{padding:.5rem 1rem;border:1px solid var(--nh-color-border, #cbd2d9);border-radius:.5rem;background:var(--nh-color-surface, #fff);color:inherit;font:inherit;cursor:pointer}.nh-two-factor__button:focus-visible{outline:2px solid var(--nh-color-focus, #2563eb);outline-offset:2px}.nh-two-factor__button:disabled{opacity:.6;cursor:not-allowed}.nh-two-factor__button--primary{border-color:var(--nh-color-primary, #2563eb);background:var(--nh-color-primary, #2563eb);color:var(--nh-color-on-primary, #fff)}.nh-two-factor__button--danger{border-color:var(--nh-color-danger, #b91c1c);color:var(--nh-color-danger, #b91c1c)}.nh-two-factor__errors{margin:0;padding:.5rem .75rem;border-radius:.5rem;background:var(--nh-color-danger-background, #fdecec);color:var(--nh-color-danger, #b91c1c);list-style:none}.nh-two-factor__setup{display:flex;flex-wrap:wrap;align-items:flex-start;gap:1rem}.nh-two-factor__qr{width:11rem;height:11rem;padding:.5rem;border-radius:.5rem;background:#fff;image-rendering:pixelated}.nh-two-factor__key{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;overflow-wrap:anywhere;-webkit-user-select:all;user-select:all}.nh-two-factor__codes{display:grid;grid-template-columns:repeat(auto-fill,minmax(9rem,1fr));gap:.5rem;margin:0;padding:0;list-style:none;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}.nh-two-factor__codes li{padding:.375rem .5rem;border:1px dashed var(--nh-color-border, #cbd2d9);border-radius:.375rem;text-align:center}.nh-two-factor__section{display:flex;flex-direction:column;gap:.75rem;padding-top:1rem;border-top:1px solid var(--nh-color-border, #cbd2d9)}.nh-two-factor__status{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem}.nh-two-factor__badge{padding:.125rem .5rem;border-radius:999px;background:var(--nh-color-status-background, #eef2f7);font-size:.8125rem;font-weight:600}.nh-two-factor__badge--on{background:var(--nh-color-success-background, #e7f6ec);color:var(--nh-color-success, #1e6b3a)}.nh-two-factor__list{display:flex;flex-direction:column;gap:.5rem;margin:0;padding:0;list-style:none}.nh-two-factor__item{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:.5rem;min-width:0;padding:.75rem;border:1px solid var(--nh-color-border, #cbd2d9);border-radius:.5rem}.nh-two-factor__item>div{min-width:0}.visually-hidden{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}\n"], dependencies: [{ kind: "component", type: NhTwoFactorRecoveryCodesComponent, selector: "nh-two-factor-recovery-codes", inputs: ["codes", "fileName"], outputs: ["acknowledged"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }, { kind: "pipe", type: DatePipe, name: "date" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
818
|
+
}
|
|
819
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhTwoFactorSettingsComponent, decorators: [{
|
|
820
|
+
type: Component,
|
|
821
|
+
args: [{ selector: 'nh-two-factor-settings', standalone: true, imports: [TranslatePipe, DatePipe, NhTwoFactorRecoveryCodesComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<section class=\"nh-two-factor\" [attr.aria-labelledby]=\"id + '-title'\" [attr.aria-busy]=\"loading() || busy()\">\n <div class=\"nh-two-factor__status\">\n <h2 class=\"nh-two-factor__title\" [class.visually-hidden]=\"!showTitle()\" [id]=\"id + '-title'\">{{ 'nh-two-factor.ui.settings-title' | translate }}</h2>\n @if (status(); as current) {\n <span class=\"nh-two-factor__badge\" [class.nh-two-factor__badge--on]=\"current.enabled\">\n {{ (current.enabled ? 'nh-two-factor.ui.status-enabled' : 'nh-two-factor.ui.status-disabled') | translate }}\n </span>\n @if (current.required) {\n <span class=\"nh-two-factor__badge\">{{ 'nh-two-factor.ui.status-required' | translate }}</span>\n }\n }\n </div>\n\n @if (loading() && !status()) {\n <p class=\"nh-two-factor__hint\" role=\"status\">{{ 'nh-two-factor.ui.loading' | translate }}</p>\n }\n\n <div role=\"status\">\n @if (noticeKey(); as notice) {\n <p class=\"nh-two-factor__notice\">{{ notice | translate }}</p>\n }\n </div>\n\n <div [id]=\"id + '-errors'\" aria-live=\"assertive\">\n @if (errorKeys().length > 0) {\n <ul class=\"nh-two-factor__errors\" role=\"alert\">\n @for (key of errorKeys(); track key) {\n <li>{{ key | translate }}</li>\n }\n </ul>\n }\n </div>\n\n @if (pendingAction()) {\n <form class=\"nh-two-factor nh-two-factor__section\" [attr.aria-labelledby]=\"id + '-reauthentication-title'\" (submit)=\"$event.preventDefault(); confirmReauthentication()\">\n <h3 class=\"nh-two-factor__subtitle\" [id]=\"id + '-reauthentication-title'\">{{ 'nh-two-factor.ui.reauthentication-title' | translate }}</h3>\n\n @if (reauthenticationMethods().length > 1) {\n <fieldset class=\"nh-two-factor__methods\">\n <legend class=\"nh-two-factor__legend\">{{ 'nh-two-factor.ui.reauthentication-method' | translate }}</legend>\n @for (option of reauthenticationMethods(); track option) {\n <label class=\"nh-two-factor__method\">\n <input\n type=\"radio\"\n [name]=\"id + '-reauthentication-method'\"\n [value]=\"option\"\n [checked]=\"reauthenticationMethod() === option\"\n (change)=\"reauthenticationMethod.set(option); reauthenticationSecret.set('')\" />\n <span>{{ (option === 'password' ? 'nh-two-factor.ui.password' : 'nh-two-factor.ui.method.' + option) | translate }}</span>\n </label>\n }\n </fieldset>\n }\n\n <div class=\"nh-two-factor__field\">\n <label [for]=\"id + '-reauthentication'\">\n {{ (reauthenticationMethod() === 'password' ? 'nh-two-factor.ui.password' : 'nh-two-factor.ui.code-label.' + reauthenticationMethod()) | translate }}\n </label>\n <input\n #reauthenticationInput\n class=\"nh-two-factor__input\"\n [id]=\"id + '-reauthentication'\"\n [type]=\"reauthenticationMethod() === 'password' ? 'password' : 'text'\"\n [attr.autocomplete]=\"reauthenticationMethod() === 'password' ? 'current-password' : 'one-time-code'\"\n [value]=\"reauthenticationSecret()\"\n [disabled]=\"busy()\"\n (input)=\"reauthenticationSecret.set(value($event))\" />\n </div>\n\n <div class=\"nh-two-factor__actions\">\n <button type=\"submit\" class=\"nh-two-factor__button nh-two-factor__button--primary\" [disabled]=\"busy()\">\n {{ 'nh-two-factor.ui.confirm' | translate }}\n </button>\n <button type=\"button\" class=\"nh-two-factor__button\" [disabled]=\"busy()\" (click)=\"cancelReauthentication()\">\n {{ 'nh-two-factor.ui.cancel' | translate }}\n </button>\n </div>\n </form>\n }\n\n @if (recoveryCodes(); as codes) {\n <div class=\"nh-two-factor__section\">\n <nh-two-factor-recovery-codes [codes]=\"codes\" (acknowledged)=\"recoveryCodes.set(null)\" />\n </div>\n }\n\n @if (setupPanel(); as panel) {\n <form class=\"nh-two-factor nh-two-factor__section\" [attr.aria-labelledby]=\"id + '-setup-title'\" (submit)=\"$event.preventDefault(); confirmSetup()\">\n <h3 class=\"nh-two-factor__subtitle\" [id]=\"id + '-setup-title'\">{{ 'nh-two-factor.ui.method.' + panel | translate }}</h3>\n\n @if (panel === 'authenticator') {\n <p class=\"nh-two-factor__hint\">{{ 'nh-two-factor.ui.authenticator-instructions' | translate }}</p>\n @if (setup(); as authenticator) {\n <div class=\"nh-two-factor__setup\">\n @if (authenticator.qrCodeDataUri) {\n <img class=\"nh-two-factor__qr\" [src]=\"authenticator.qrCodeDataUri\" [alt]=\"'nh-two-factor.ui.qr-code-alt' | translate\" />\n }\n <p class=\"nh-two-factor__text\">\n {{ 'nh-two-factor.ui.shared-key' | translate }}:\n <span class=\"nh-two-factor__key\">{{ authenticator.sharedKey }}</span>\n </p>\n </div>\n }\n } @else {\n <p class=\"nh-two-factor__hint\">{{ 'nh-two-factor.ui.email-instructions' | translate }}</p>\n @if (emailSent(); as sent) {\n <p class=\"nh-two-factor__hint\">\n {{ 'nh-two-factor.ui.email-code-sent' | translate: { time: (sent.expiresAt | date: 'shortTime') } }}\n </p>\n }\n }\n\n <div class=\"nh-two-factor__field\">\n <label [for]=\"id + '-code'\">{{ 'nh-two-factor.ui.code-label.' + panel | translate }}</label>\n <input\n #codeInput\n type=\"text\"\n class=\"nh-two-factor__input nh-two-factor__input--code\"\n inputmode=\"numeric\"\n autocomplete=\"one-time-code\"\n spellcheck=\"false\"\n [id]=\"id + '-code'\"\n [value]=\"code()\"\n [disabled]=\"busy()\"\n (input)=\"code.set(value($event))\" />\n </div>\n\n <div class=\"nh-two-factor__actions\">\n <button type=\"submit\" class=\"nh-two-factor__button nh-two-factor__button--primary\" [disabled]=\"busy()\">\n {{ 'nh-two-factor.ui.confirm' | translate }}\n </button>\n @if (panel === 'email') {\n <button type=\"button\" class=\"nh-two-factor__button\" [disabled]=\"busy()\" (click)=\"resendEmailCode()\">\n {{ 'nh-two-factor.ui.resend-email-code' | translate }}\n </button>\n }\n <button type=\"button\" class=\"nh-two-factor__button\" [disabled]=\"busy()\" (click)=\"closeSetup()\">\n {{ 'nh-two-factor.ui.cancel' | translate }}\n </button>\n </div>\n </form>\n }\n\n @if (status(); as current) {\n @if (!setupPanel() && !pendingAction()) {\n <div class=\"nh-two-factor__actions\">\n @if (authenticatorAvailable()) {\n <button type=\"button\" class=\"nh-two-factor__button\" [disabled]=\"busy()\" (click)=\"request({ kind: 'authenticator' })\">\n {{ (hasAuthenticator() ? 'nh-two-factor.ui.change-authenticator' : 'nh-two-factor.ui.set-up-authenticator') | translate }}\n </button>\n }\n @if (emailAvailable() && !hasEmail()) {\n <button type=\"button\" class=\"nh-two-factor__button\" [disabled]=\"busy()\" (click)=\"request({ kind: 'email' })\">\n {{ 'nh-two-factor.ui.set-up-email' | translate }}\n </button>\n }\n </div>\n }\n\n @if (passkeysAvailable()) {\n <div class=\"nh-two-factor__section\">\n <h3 class=\"nh-two-factor__subtitle\">{{ 'nh-two-factor.ui.passkeys-title' | translate }}</h3>\n\n @if (passkeys().length === 0) {\n <p class=\"nh-two-factor__hint\">{{ 'nh-two-factor.ui.no-passkeys' | translate }}</p>\n } @else {\n <ul class=\"nh-two-factor__list\">\n @for (passkey of passkeys(); track passkey.id) {\n <li class=\"nh-two-factor__item\">\n @if (renamingId() === passkey.id) {\n <form class=\"nh-two-factor__actions\" (submit)=\"$event.preventDefault(); saveRename(passkey)\">\n <label class=\"visually-hidden\" [for]=\"id + '-rename-' + $index\">{{ 'nh-two-factor.ui.passkey-name' | translate }}</label>\n <input\n type=\"text\"\n class=\"nh-two-factor__input\"\n maxlength=\"100\"\n [id]=\"id + '-rename-' + $index\"\n [value]=\"renameValue()\"\n [disabled]=\"busy()\"\n (input)=\"renameValue.set(value($event))\" />\n <button type=\"submit\" class=\"nh-two-factor__button nh-two-factor__button--primary\" [disabled]=\"busy()\">\n {{ 'nh-two-factor.ui.save' | translate }}\n </button>\n <button type=\"button\" class=\"nh-two-factor__button\" [disabled]=\"busy()\" (click)=\"cancelRename()\">\n {{ 'nh-two-factor.ui.cancel' | translate }}\n </button>\n </form>\n } @else {\n <div>\n <p class=\"nh-two-factor__text\"><strong>{{ passkey.name }}</strong></p>\n <p class=\"nh-two-factor__hint\">\n {{ 'nh-two-factor.ui.passkey-created' | translate: { date: (passkey.createdAt | date: 'mediumDate') } }}\n @if (passkey.isBackedUp) {\n \u00B7 {{ 'nh-two-factor.ui.passkey-synced' | translate }}\n }\n </p>\n </div>\n <div class=\"nh-two-factor__actions\">\n <button type=\"button\" class=\"nh-two-factor__button\" [disabled]=\"busy()\" (click)=\"startRename(passkey)\">\n {{ 'nh-two-factor.ui.rename' | translate }}\n </button>\n <button type=\"button\" class=\"nh-two-factor__button nh-two-factor__button--danger\" [disabled]=\"busy()\" (click)=\"request({ kind: 'remove-passkey', passkeyId: passkey.id })\">\n {{ 'nh-two-factor.ui.remove' | translate }}\n </button>\n </div>\n }\n </li>\n }\n </ul>\n }\n\n @if (passkeySupported && !pendingAction()) {\n <form class=\"nh-two-factor__actions\" (submit)=\"$event.preventDefault(); request({ kind: 'add-passkey' })\">\n <label class=\"visually-hidden\" [for]=\"id + '-new-passkey'\">{{ 'nh-two-factor.ui.passkey-name' | translate }}</label>\n <input\n type=\"text\"\n class=\"nh-two-factor__input\"\n maxlength=\"100\"\n autocomplete=\"off\"\n [id]=\"id + '-new-passkey'\"\n [attr.placeholder]=\"'nh-two-factor.ui.passkey-name' | translate\"\n [value]=\"newPasskeyName()\"\n [disabled]=\"busy()\"\n (input)=\"newPasskeyName.set(value($event))\" />\n <button type=\"submit\" class=\"nh-two-factor__button\" [disabled]=\"busy()\">\n {{ 'nh-two-factor.ui.add-passkey' | translate }}\n </button>\n </form>\n }\n </div>\n }\n\n @if (current.enabled && !pendingAction()) {\n <div class=\"nh-two-factor__section\">\n @if (recoveryCodesAvailable()) {\n <p class=\"nh-two-factor__hint\">{{ 'nh-two-factor.ui.recovery-codes-left' | translate: { count: current.recoveryCodesLeft } }}</p>\n }\n <div class=\"nh-two-factor__actions\">\n @if (recoveryCodesAvailable()) {\n <button type=\"button\" class=\"nh-two-factor__button\" [disabled]=\"busy()\" (click)=\"request({ kind: 'recovery-codes' })\">\n {{ 'nh-two-factor.ui.regenerate-recovery-codes' | translate }}\n </button>\n }\n @if (current.rememberDeviceAvailable) {\n <button type=\"button\" class=\"nh-two-factor__button\" [disabled]=\"busy()\" (click)=\"request({ kind: 'forget-devices' })\">\n {{ 'nh-two-factor.ui.forget-devices' | translate }}\n </button>\n }\n @if (!required()) {\n <button type=\"button\" class=\"nh-two-factor__button nh-two-factor__button--danger\" [disabled]=\"busy()\" (click)=\"request({ kind: 'disable' })\">\n {{ 'nh-two-factor.ui.disable' | translate }}\n </button>\n }\n </div>\n </div>\n }\n }\n</section>\n", styles: [":host{display:block;min-width:0;color:var(--nh-color-text, #1f2933)}.nh-two-factor{display:flex;flex-direction:column;gap:1rem;min-width:0}.nh-two-factor__title{margin:0;font-size:1.125rem;font-weight:650}.nh-two-factor__subtitle{margin:0;font-size:1rem;font-weight:650}.nh-two-factor__text,.nh-two-factor__hint{margin:0;overflow-wrap:anywhere}.nh-two-factor__hint{color:var(--nh-color-muted, #52606d);font-size:.875rem}.nh-two-factor__notice{margin:0;padding:.5rem .75rem;border-radius:.5rem;background:var(--nh-color-success-background, #e7f6ec);color:var(--nh-color-success, #1e6b3a)}.nh-two-factor__methods{display:flex;flex-wrap:wrap;gap:.5rem;min-width:0;margin:0;padding:0;border:0}.nh-two-factor__legend{width:100%;margin-bottom:.25rem;padding:0;font-weight:600}.nh-two-factor__method{position:relative}.nh-two-factor__method input{position:absolute;inset:0;opacity:0;cursor:pointer}.nh-two-factor__method span{display:inline-flex;padding:.5rem .75rem;border:1px solid var(--nh-color-border, #cbd2d9);border-radius:.5rem;background:var(--nh-color-surface, #fff)}.nh-two-factor__method input:checked+span{border-color:var(--nh-color-primary, #2563eb);box-shadow:inset 0 0 0 1px var(--nh-color-primary, #2563eb);font-weight:600}.nh-two-factor__method input:focus-visible+span{outline:2px solid var(--nh-color-focus, #2563eb);outline-offset:2px}.nh-two-factor__method input:disabled+span{opacity:.6}.nh-two-factor__field{display:flex;flex-direction:column;gap:.375rem;min-width:0}.nh-two-factor__field label{font-weight:600}.nh-two-factor__input{width:100%;max-width:20rem;padding:.5rem .75rem;border:1px solid var(--nh-color-border, #cbd2d9);border-radius:.5rem;background:var(--nh-color-input-background, #fff);color:inherit;font:inherit}.nh-two-factor__input:focus-visible{outline:2px solid var(--nh-color-focus, #2563eb);outline-offset:1px}.nh-two-factor__input[aria-invalid=true]{border-color:var(--nh-color-danger, #b91c1c)}.nh-two-factor__input--code{font-variant-numeric:tabular-nums;letter-spacing:.15em}.nh-two-factor__check{display:flex;align-items:center;gap:.5rem}.nh-two-factor__actions{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem}.nh-two-factor__button{padding:.5rem 1rem;border:1px solid var(--nh-color-border, #cbd2d9);border-radius:.5rem;background:var(--nh-color-surface, #fff);color:inherit;font:inherit;cursor:pointer}.nh-two-factor__button:focus-visible{outline:2px solid var(--nh-color-focus, #2563eb);outline-offset:2px}.nh-two-factor__button:disabled{opacity:.6;cursor:not-allowed}.nh-two-factor__button--primary{border-color:var(--nh-color-primary, #2563eb);background:var(--nh-color-primary, #2563eb);color:var(--nh-color-on-primary, #fff)}.nh-two-factor__button--danger{border-color:var(--nh-color-danger, #b91c1c);color:var(--nh-color-danger, #b91c1c)}.nh-two-factor__errors{margin:0;padding:.5rem .75rem;border-radius:.5rem;background:var(--nh-color-danger-background, #fdecec);color:var(--nh-color-danger, #b91c1c);list-style:none}.nh-two-factor__setup{display:flex;flex-wrap:wrap;align-items:flex-start;gap:1rem}.nh-two-factor__qr{width:11rem;height:11rem;padding:.5rem;border-radius:.5rem;background:#fff;image-rendering:pixelated}.nh-two-factor__key{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;overflow-wrap:anywhere;-webkit-user-select:all;user-select:all}.nh-two-factor__codes{display:grid;grid-template-columns:repeat(auto-fill,minmax(9rem,1fr));gap:.5rem;margin:0;padding:0;list-style:none;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}.nh-two-factor__codes li{padding:.375rem .5rem;border:1px dashed var(--nh-color-border, #cbd2d9);border-radius:.375rem;text-align:center}.nh-two-factor__section{display:flex;flex-direction:column;gap:.75rem;padding-top:1rem;border-top:1px solid var(--nh-color-border, #cbd2d9)}.nh-two-factor__status{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem}.nh-two-factor__badge{padding:.125rem .5rem;border-radius:999px;background:var(--nh-color-status-background, #eef2f7);font-size:.8125rem;font-weight:600}.nh-two-factor__badge--on{background:var(--nh-color-success-background, #e7f6ec);color:var(--nh-color-success, #1e6b3a)}.nh-two-factor__list{display:flex;flex-direction:column;gap:.5rem;margin:0;padding:0;list-style:none}.nh-two-factor__item{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:.5rem;min-width:0;padding:.75rem;border:1px solid var(--nh-color-border, #cbd2d9);border-radius:.5rem}.nh-two-factor__item>div{min-width:0}.visually-hidden{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}\n"] }]
|
|
822
|
+
}], ctorParameters: () => [], propDecorators: { reauthenticationInput: [{ type: i0.ViewChild, args: ['reauthenticationInput', { isSignal: true }] }], codeInput: [{ type: i0.ViewChild, args: ['codeInput', { isSignal: true }] }], showTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "showTitle", required: false }] }], changed: [{ type: i0.Output, args: ["changed"] }] } });
|
|
823
|
+
|
|
824
|
+
let nextId = 0;
|
|
825
|
+
/**
|
|
826
|
+
* Signs in with a discoverable passkey, without a username or password. Renders nothing in
|
|
827
|
+
* browsers without passkey support.
|
|
828
|
+
*/
|
|
829
|
+
class NhPasskeyLoginButtonComponent {
|
|
830
|
+
constructor() {
|
|
831
|
+
this.auth = inject(NhAuthService);
|
|
832
|
+
/** Whether the button shows failures itself. */
|
|
833
|
+
this.showErrors = input(true, ...(ngDevMode ? [{ debugName: "showErrors" }] : []));
|
|
834
|
+
this.authenticated = output();
|
|
835
|
+
/** The policy asked for more: a second-factor challenge or an enrollment. */
|
|
836
|
+
this.pending = output();
|
|
837
|
+
/** Translation keys of a failed sign-in. */
|
|
838
|
+
this.failed = output();
|
|
839
|
+
this.id = `nh-passkey-login-${nextId++}`;
|
|
840
|
+
this.supported = this.auth.isPasskeySupported();
|
|
841
|
+
this.busy = signal(false, ...(ngDevMode ? [{ debugName: "busy" }] : []));
|
|
842
|
+
this.errorKeys = signal([], ...(ngDevMode ? [{ debugName: "errorKeys" }] : []));
|
|
843
|
+
inject(NhTwoFactorTranslationMerger).start();
|
|
844
|
+
}
|
|
845
|
+
async signIn() {
|
|
846
|
+
if (this.busy()) {
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
this.busy.set(true);
|
|
850
|
+
this.errorKeys.set([]);
|
|
851
|
+
const result = await this.auth.signInWithPasskey();
|
|
852
|
+
this.busy.set(false);
|
|
853
|
+
if (!result.isSuccess || !result.data) {
|
|
854
|
+
const keys = nhTwoFactorErrorKeys(result);
|
|
855
|
+
this.errorKeys.set(keys);
|
|
856
|
+
this.failed.emit(keys);
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
if (result.data.status === 'authenticated' && result.data.authorization) {
|
|
860
|
+
this.authenticated.emit(result.data.authorization);
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
this.pending.emit(result.data);
|
|
864
|
+
}
|
|
865
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhPasskeyLoginButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
866
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.28", type: NhPasskeyLoginButtonComponent, isStandalone: true, selector: "nh-passkey-login-button", inputs: { showErrors: { classPropertyName: "showErrors", publicName: "showErrors", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { authenticated: "authenticated", pending: "pending", failed: "failed" }, ngImport: i0, template: "@if (supported) {\n <div class=\"nh-two-factor\">\n <div class=\"nh-two-factor__actions\">\n <button\n type=\"button\"\n class=\"nh-two-factor__button\"\n [disabled]=\"busy()\"\n [attr.aria-busy]=\"busy()\"\n [attr.aria-describedby]=\"errorKeys().length > 0 ? id + '-errors' : null\"\n (click)=\"signIn()\">\n {{ 'nh-two-factor.ui.sign-in-with-passkey' | translate }}\n </button>\n </div>\n\n @if (showErrors()) {\n <div [id]=\"id + '-errors'\" aria-live=\"assertive\">\n @if (errorKeys().length > 0) {\n <ul class=\"nh-two-factor__errors\" role=\"alert\">\n @for (key of errorKeys(); track key) {\n <li>{{ key | translate }}</li>\n }\n </ul>\n }\n </div>\n }\n </div>\n}\n", styles: [":host{display:block;min-width:0;color:var(--nh-color-text, #1f2933)}.nh-two-factor{display:flex;flex-direction:column;gap:1rem;min-width:0}.nh-two-factor__title{margin:0;font-size:1.125rem;font-weight:650}.nh-two-factor__subtitle{margin:0;font-size:1rem;font-weight:650}.nh-two-factor__text,.nh-two-factor__hint{margin:0;overflow-wrap:anywhere}.nh-two-factor__hint{color:var(--nh-color-muted, #52606d);font-size:.875rem}.nh-two-factor__notice{margin:0;padding:.5rem .75rem;border-radius:.5rem;background:var(--nh-color-success-background, #e7f6ec);color:var(--nh-color-success, #1e6b3a)}.nh-two-factor__methods{display:flex;flex-wrap:wrap;gap:.5rem;min-width:0;margin:0;padding:0;border:0}.nh-two-factor__legend{width:100%;margin-bottom:.25rem;padding:0;font-weight:600}.nh-two-factor__method{position:relative}.nh-two-factor__method input{position:absolute;inset:0;opacity:0;cursor:pointer}.nh-two-factor__method span{display:inline-flex;padding:.5rem .75rem;border:1px solid var(--nh-color-border, #cbd2d9);border-radius:.5rem;background:var(--nh-color-surface, #fff)}.nh-two-factor__method input:checked+span{border-color:var(--nh-color-primary, #2563eb);box-shadow:inset 0 0 0 1px var(--nh-color-primary, #2563eb);font-weight:600}.nh-two-factor__method input:focus-visible+span{outline:2px solid var(--nh-color-focus, #2563eb);outline-offset:2px}.nh-two-factor__method input:disabled+span{opacity:.6}.nh-two-factor__field{display:flex;flex-direction:column;gap:.375rem;min-width:0}.nh-two-factor__field label{font-weight:600}.nh-two-factor__input{width:100%;max-width:20rem;padding:.5rem .75rem;border:1px solid var(--nh-color-border, #cbd2d9);border-radius:.5rem;background:var(--nh-color-input-background, #fff);color:inherit;font:inherit}.nh-two-factor__input:focus-visible{outline:2px solid var(--nh-color-focus, #2563eb);outline-offset:1px}.nh-two-factor__input[aria-invalid=true]{border-color:var(--nh-color-danger, #b91c1c)}.nh-two-factor__input--code{font-variant-numeric:tabular-nums;letter-spacing:.15em}.nh-two-factor__check{display:flex;align-items:center;gap:.5rem}.nh-two-factor__actions{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem}.nh-two-factor__button{padding:.5rem 1rem;border:1px solid var(--nh-color-border, #cbd2d9);border-radius:.5rem;background:var(--nh-color-surface, #fff);color:inherit;font:inherit;cursor:pointer}.nh-two-factor__button:focus-visible{outline:2px solid var(--nh-color-focus, #2563eb);outline-offset:2px}.nh-two-factor__button:disabled{opacity:.6;cursor:not-allowed}.nh-two-factor__button--primary{border-color:var(--nh-color-primary, #2563eb);background:var(--nh-color-primary, #2563eb);color:var(--nh-color-on-primary, #fff)}.nh-two-factor__button--danger{border-color:var(--nh-color-danger, #b91c1c);color:var(--nh-color-danger, #b91c1c)}.nh-two-factor__errors{margin:0;padding:.5rem .75rem;border-radius:.5rem;background:var(--nh-color-danger-background, #fdecec);color:var(--nh-color-danger, #b91c1c);list-style:none}.nh-two-factor__setup{display:flex;flex-wrap:wrap;align-items:flex-start;gap:1rem}.nh-two-factor__qr{width:11rem;height:11rem;padding:.5rem;border-radius:.5rem;background:#fff;image-rendering:pixelated}.nh-two-factor__key{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;overflow-wrap:anywhere;-webkit-user-select:all;user-select:all}.nh-two-factor__codes{display:grid;grid-template-columns:repeat(auto-fill,minmax(9rem,1fr));gap:.5rem;margin:0;padding:0;list-style:none;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}.nh-two-factor__codes li{padding:.375rem .5rem;border:1px dashed var(--nh-color-border, #cbd2d9);border-radius:.375rem;text-align:center}.nh-two-factor__section{display:flex;flex-direction:column;gap:.75rem;padding-top:1rem;border-top:1px solid var(--nh-color-border, #cbd2d9)}.nh-two-factor__status{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem}.nh-two-factor__badge{padding:.125rem .5rem;border-radius:999px;background:var(--nh-color-status-background, #eef2f7);font-size:.8125rem;font-weight:600}.nh-two-factor__badge--on{background:var(--nh-color-success-background, #e7f6ec);color:var(--nh-color-success, #1e6b3a)}.nh-two-factor__list{display:flex;flex-direction:column;gap:.5rem;margin:0;padding:0;list-style:none}.nh-two-factor__item{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:.5rem;min-width:0;padding:.75rem;border:1px solid var(--nh-color-border, #cbd2d9);border-radius:.5rem}.nh-two-factor__item>div{min-width:0}.visually-hidden{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}\n"], dependencies: [{ kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
867
|
+
}
|
|
868
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NhPasskeyLoginButtonComponent, decorators: [{
|
|
869
|
+
type: Component,
|
|
870
|
+
args: [{ selector: 'nh-passkey-login-button', standalone: true, imports: [TranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (supported) {\n <div class=\"nh-two-factor\">\n <div class=\"nh-two-factor__actions\">\n <button\n type=\"button\"\n class=\"nh-two-factor__button\"\n [disabled]=\"busy()\"\n [attr.aria-busy]=\"busy()\"\n [attr.aria-describedby]=\"errorKeys().length > 0 ? id + '-errors' : null\"\n (click)=\"signIn()\">\n {{ 'nh-two-factor.ui.sign-in-with-passkey' | translate }}\n </button>\n </div>\n\n @if (showErrors()) {\n <div [id]=\"id + '-errors'\" aria-live=\"assertive\">\n @if (errorKeys().length > 0) {\n <ul class=\"nh-two-factor__errors\" role=\"alert\">\n @for (key of errorKeys(); track key) {\n <li>{{ key | translate }}</li>\n }\n </ul>\n }\n </div>\n }\n </div>\n}\n", styles: [":host{display:block;min-width:0;color:var(--nh-color-text, #1f2933)}.nh-two-factor{display:flex;flex-direction:column;gap:1rem;min-width:0}.nh-two-factor__title{margin:0;font-size:1.125rem;font-weight:650}.nh-two-factor__subtitle{margin:0;font-size:1rem;font-weight:650}.nh-two-factor__text,.nh-two-factor__hint{margin:0;overflow-wrap:anywhere}.nh-two-factor__hint{color:var(--nh-color-muted, #52606d);font-size:.875rem}.nh-two-factor__notice{margin:0;padding:.5rem .75rem;border-radius:.5rem;background:var(--nh-color-success-background, #e7f6ec);color:var(--nh-color-success, #1e6b3a)}.nh-two-factor__methods{display:flex;flex-wrap:wrap;gap:.5rem;min-width:0;margin:0;padding:0;border:0}.nh-two-factor__legend{width:100%;margin-bottom:.25rem;padding:0;font-weight:600}.nh-two-factor__method{position:relative}.nh-two-factor__method input{position:absolute;inset:0;opacity:0;cursor:pointer}.nh-two-factor__method span{display:inline-flex;padding:.5rem .75rem;border:1px solid var(--nh-color-border, #cbd2d9);border-radius:.5rem;background:var(--nh-color-surface, #fff)}.nh-two-factor__method input:checked+span{border-color:var(--nh-color-primary, #2563eb);box-shadow:inset 0 0 0 1px var(--nh-color-primary, #2563eb);font-weight:600}.nh-two-factor__method input:focus-visible+span{outline:2px solid var(--nh-color-focus, #2563eb);outline-offset:2px}.nh-two-factor__method input:disabled+span{opacity:.6}.nh-two-factor__field{display:flex;flex-direction:column;gap:.375rem;min-width:0}.nh-two-factor__field label{font-weight:600}.nh-two-factor__input{width:100%;max-width:20rem;padding:.5rem .75rem;border:1px solid var(--nh-color-border, #cbd2d9);border-radius:.5rem;background:var(--nh-color-input-background, #fff);color:inherit;font:inherit}.nh-two-factor__input:focus-visible{outline:2px solid var(--nh-color-focus, #2563eb);outline-offset:1px}.nh-two-factor__input[aria-invalid=true]{border-color:var(--nh-color-danger, #b91c1c)}.nh-two-factor__input--code{font-variant-numeric:tabular-nums;letter-spacing:.15em}.nh-two-factor__check{display:flex;align-items:center;gap:.5rem}.nh-two-factor__actions{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem}.nh-two-factor__button{padding:.5rem 1rem;border:1px solid var(--nh-color-border, #cbd2d9);border-radius:.5rem;background:var(--nh-color-surface, #fff);color:inherit;font:inherit;cursor:pointer}.nh-two-factor__button:focus-visible{outline:2px solid var(--nh-color-focus, #2563eb);outline-offset:2px}.nh-two-factor__button:disabled{opacity:.6;cursor:not-allowed}.nh-two-factor__button--primary{border-color:var(--nh-color-primary, #2563eb);background:var(--nh-color-primary, #2563eb);color:var(--nh-color-on-primary, #fff)}.nh-two-factor__button--danger{border-color:var(--nh-color-danger, #b91c1c);color:var(--nh-color-danger, #b91c1c)}.nh-two-factor__errors{margin:0;padding:.5rem .75rem;border-radius:.5rem;background:var(--nh-color-danger-background, #fdecec);color:var(--nh-color-danger, #b91c1c);list-style:none}.nh-two-factor__setup{display:flex;flex-wrap:wrap;align-items:flex-start;gap:1rem}.nh-two-factor__qr{width:11rem;height:11rem;padding:.5rem;border-radius:.5rem;background:#fff;image-rendering:pixelated}.nh-two-factor__key{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;overflow-wrap:anywhere;-webkit-user-select:all;user-select:all}.nh-two-factor__codes{display:grid;grid-template-columns:repeat(auto-fill,minmax(9rem,1fr));gap:.5rem;margin:0;padding:0;list-style:none;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}.nh-two-factor__codes li{padding:.375rem .5rem;border:1px dashed var(--nh-color-border, #cbd2d9);border-radius:.375rem;text-align:center}.nh-two-factor__section{display:flex;flex-direction:column;gap:.75rem;padding-top:1rem;border-top:1px solid var(--nh-color-border, #cbd2d9)}.nh-two-factor__status{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem}.nh-two-factor__badge{padding:.125rem .5rem;border-radius:999px;background:var(--nh-color-status-background, #eef2f7);font-size:.8125rem;font-weight:600}.nh-two-factor__badge--on{background:var(--nh-color-success-background, #e7f6ec);color:var(--nh-color-success, #1e6b3a)}.nh-two-factor__list{display:flex;flex-direction:column;gap:.5rem;margin:0;padding:0;list-style:none}.nh-two-factor__item{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:.5rem;min-width:0;padding:.75rem;border:1px solid var(--nh-color-border, #cbd2d9);border-radius:.5rem}.nh-two-factor__item>div{min-width:0}.visually-hidden{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}\n"] }]
|
|
871
|
+
}], ctorParameters: () => [], propDecorators: { showErrors: [{ type: i0.Input, args: [{ isSignal: true, alias: "showErrors", required: false }] }], authenticated: [{ type: i0.Output, args: ["authenticated"] }], pending: [{ type: i0.Output, args: ["pending"] }], failed: [{ type: i0.Output, args: ["failed"] }] } });
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* Registers the two-factor texts at startup. The components also register them when they are
|
|
875
|
+
* first created, so this provider is only needed to translate `nh-two-factor.` failure keys
|
|
876
|
+
* outside the components, for example on a custom login page.
|
|
877
|
+
*/
|
|
878
|
+
function provideNhTwoFactor() {
|
|
879
|
+
return makeEnvironmentProviders([
|
|
880
|
+
provideEnvironmentInitializer(() => inject(NhTwoFactorTranslationMerger).start())
|
|
881
|
+
]);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/*
|
|
885
|
+
* Public API surface of @newheap/platform-common/two-factor.
|
|
886
|
+
*
|
|
887
|
+
* The two-factor components are a separate entry point, so applications that do not import
|
|
888
|
+
* it ship none of their code. The service and models live in @newheap/platform-common.
|
|
889
|
+
*/
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* Generated bundle index. Do not edit.
|
|
893
|
+
*/
|
|
894
|
+
|
|
895
|
+
export { NH_TWO_FACTOR_TRANSLATIONS, NhPasskeyLoginButtonComponent, NhTwoFactorChallengeComponent, NhTwoFactorEnrollmentComponent, NhTwoFactorRecoveryCodesComponent, NhTwoFactorSettingsComponent, NhTwoFactorTranslationMerger, nhTwoFactorErrorKeys, nhTwoFactorStepEnded, provideNhTwoFactor };
|
|
896
|
+
//# sourceMappingURL=newheap-platform-common-two-factor.mjs.map
|