valtech-components 2.0.835 → 2.0.836
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/esm2022/lib/components/organisms/mfa-modal/mfa-modal.component.mjs +63 -13
- package/esm2022/lib/services/auth/auth.service.mjs +27 -17
- package/esm2022/lib/services/auth/oauth-callback.component.mjs +19 -1
- package/esm2022/lib/services/auth/types.mjs +1 -1
- package/esm2022/lib/version.mjs +2 -2
- package/fesm2022/valtech-components.mjs +101 -26
- package/fesm2022/valtech-components.mjs.map +1 -1
- package/lib/components/organisms/mfa-modal/mfa-modal.component.d.ts +18 -3
- package/lib/services/auth/types.d.ts +9 -0
- package/lib/version.d.ts +1 -1
- package/package.json +1 -1
|
@@ -53,7 +53,7 @@ import 'prismjs/components/prism-json';
|
|
|
53
53
|
* Current version of valtech-components.
|
|
54
54
|
* This is automatically updated during the publish process.
|
|
55
55
|
*/
|
|
56
|
-
const VERSION = '2.0.
|
|
56
|
+
const VERSION = '2.0.836';
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
59
|
* Servicio para gestionar presets de componentes.
|
|
@@ -24897,13 +24897,16 @@ class AuthService {
|
|
|
24897
24897
|
signinWithOAuth(provider) {
|
|
24898
24898
|
this.stateService.clearError();
|
|
24899
24899
|
return this.oauthService.startFlow(provider).pipe(tap(result => {
|
|
24900
|
-
//
|
|
24901
|
-
|
|
24902
|
-
|
|
24903
|
-
|
|
24904
|
-
|
|
24905
|
-
|
|
24906
|
-
|
|
24900
|
+
// MFA requerido tras OAuth — guardar estado temporal, NO autenticar.
|
|
24901
|
+
// El login component reacciona a `mfaPending()` y abre el modal verify.
|
|
24902
|
+
if (result.mfaRequired) {
|
|
24903
|
+
this.stateService.setMFAPending({
|
|
24904
|
+
required: true,
|
|
24905
|
+
mfaToken: result.mfaToken,
|
|
24906
|
+
method: result.mfaMethod,
|
|
24907
|
+
});
|
|
24908
|
+
return;
|
|
24909
|
+
}
|
|
24907
24910
|
// Convertir OAuthResult a SigninResponse compatible
|
|
24908
24911
|
const response = {
|
|
24909
24912
|
operationId: 'oauth',
|
|
@@ -24915,15 +24918,22 @@ class AuthService {
|
|
|
24915
24918
|
permissions: result.permissions,
|
|
24916
24919
|
};
|
|
24917
24920
|
this.handleSuccessfulAuth(response);
|
|
24918
|
-
}), map$1(result =>
|
|
24919
|
-
|
|
24920
|
-
|
|
24921
|
-
|
|
24922
|
-
|
|
24923
|
-
|
|
24924
|
-
|
|
24925
|
-
|
|
24926
|
-
|
|
24921
|
+
}), map$1(result => result.mfaRequired
|
|
24922
|
+
? {
|
|
24923
|
+
operationId: 'oauth',
|
|
24924
|
+
mfaRequired: true,
|
|
24925
|
+
mfaToken: result.mfaToken,
|
|
24926
|
+
mfaMethod: result.mfaMethod,
|
|
24927
|
+
}
|
|
24928
|
+
: {
|
|
24929
|
+
operationId: 'oauth',
|
|
24930
|
+
accessToken: result.accessToken,
|
|
24931
|
+
refreshToken: result.refreshToken,
|
|
24932
|
+
firebaseToken: result.firebaseToken,
|
|
24933
|
+
expiresIn: result.expiresIn,
|
|
24934
|
+
roles: result.roles,
|
|
24935
|
+
permissions: result.permissions,
|
|
24936
|
+
}), catchError(error => {
|
|
24927
24937
|
const authError = {
|
|
24928
24938
|
code: error.code || 'OAUTH_ERROR',
|
|
24929
24939
|
message: error.message || 'Error de autenticación OAuth',
|
|
@@ -28438,6 +28448,24 @@ class OAuthCallbackComponent {
|
|
|
28438
28448
|
this.closeAfterDelay();
|
|
28439
28449
|
return;
|
|
28440
28450
|
}
|
|
28451
|
+
// MFA requerido — el backend redirige sin tokens, con mfa_token. El flujo
|
|
28452
|
+
// continúa con el challenge MFA (AuthService.setMFAPending → modal verify).
|
|
28453
|
+
if (params.get('mfa_required') === 'true') {
|
|
28454
|
+
this.sendToParent({
|
|
28455
|
+
type: 'oauth-callback',
|
|
28456
|
+
tokens: {
|
|
28457
|
+
accessToken: '',
|
|
28458
|
+
refreshToken: '',
|
|
28459
|
+
expiresIn: 0,
|
|
28460
|
+
mfaRequired: true,
|
|
28461
|
+
mfaToken: params.get('mfa_token') || undefined,
|
|
28462
|
+
mfaMethod: params.get('mfa_method') || undefined,
|
|
28463
|
+
},
|
|
28464
|
+
});
|
|
28465
|
+
this.message = 'Verificación adicional requerida';
|
|
28466
|
+
this.closeAfterDelay();
|
|
28467
|
+
return;
|
|
28468
|
+
}
|
|
28441
28469
|
// Extraer tokens
|
|
28442
28470
|
const accessToken = params.get('access_token');
|
|
28443
28471
|
const refreshToken = params.get('refresh_token');
|
|
@@ -30271,7 +30299,7 @@ class MfaModalComponent {
|
|
|
30271
30299
|
const opening = value && !this._isOpen;
|
|
30272
30300
|
this._isOpen = value;
|
|
30273
30301
|
if (opening) {
|
|
30274
|
-
this.
|
|
30302
|
+
this.open();
|
|
30275
30303
|
}
|
|
30276
30304
|
}
|
|
30277
30305
|
get isOpen() {
|
|
@@ -30286,6 +30314,7 @@ class MfaModalComponent {
|
|
|
30286
30314
|
this.auth = inject(AuthService);
|
|
30287
30315
|
this.toast = inject(ToastService);
|
|
30288
30316
|
this.i18n = inject(I18nService);
|
|
30317
|
+
this.i18nHelper = inject(InputI18nHelper);
|
|
30289
30318
|
this.qrGen = inject(QrGeneratorService);
|
|
30290
30319
|
this._step = signal('loading');
|
|
30291
30320
|
/** Paso actual del flujo. */
|
|
@@ -30305,7 +30334,6 @@ class MfaModalComponent {
|
|
|
30305
30334
|
this.regeneratedCodes = signal(null);
|
|
30306
30335
|
this.resendCooldown = signal(0);
|
|
30307
30336
|
this.pinControl = new FormControl('', [Validators.required, Validators.minLength(6), Validators.maxLength(6)]);
|
|
30308
|
-
this.passwordControl = new FormControl('', [Validators.required]);
|
|
30309
30337
|
this.phoneControl = new FormControl('', [Validators.required, Validators.pattern(/^\+[1-9]\d{6,14}$/)]);
|
|
30310
30338
|
this.pinInputProps = {
|
|
30311
30339
|
control: this.pinControl,
|
|
@@ -30314,8 +30342,39 @@ class MfaModalComponent {
|
|
|
30314
30342
|
allowNumbersOnly: true,
|
|
30315
30343
|
autoFocus: true,
|
|
30316
30344
|
};
|
|
30345
|
+
/** Form de deshabilitación — `val-form` con un campo de contraseña. */
|
|
30346
|
+
this.disableFormProps = computed(() => this.i18nHelper.resolveForm({
|
|
30347
|
+
nameKey: 'mfaDisableTitle',
|
|
30348
|
+
i18nNamespace: '_auth',
|
|
30349
|
+
sections: [
|
|
30350
|
+
{
|
|
30351
|
+
name: this.t('mfaDisablePrompt'),
|
|
30352
|
+
order: 0,
|
|
30353
|
+
fields: [
|
|
30354
|
+
{
|
|
30355
|
+
type: InputType.PASSWORD,
|
|
30356
|
+
name: 'password',
|
|
30357
|
+
token: 'mfa-disable-password',
|
|
30358
|
+
labelKey: 'mfaPasswordLabel',
|
|
30359
|
+
hint: '',
|
|
30360
|
+
placeholderKey: 'passwordPlaceholder',
|
|
30361
|
+
errorKeys: { required: 'mfaPasswordRequired' },
|
|
30362
|
+
validators: [Validators.required],
|
|
30363
|
+
order: 0,
|
|
30364
|
+
state: ComponentStates.ENABLED,
|
|
30365
|
+
},
|
|
30366
|
+
],
|
|
30367
|
+
},
|
|
30368
|
+
],
|
|
30369
|
+
actions: {
|
|
30370
|
+
...SolidDefaultBlock('', 'submit'),
|
|
30371
|
+
token: 'mfa-disable-submit',
|
|
30372
|
+
textKey: 'mfaDisableButton',
|
|
30373
|
+
},
|
|
30374
|
+
state: this.working() ? ComponentStates.WORKING : ComponentStates.ENABLED,
|
|
30375
|
+
}));
|
|
30317
30376
|
this.resendTimer = null;
|
|
30318
|
-
addIcons({ closeOutline });
|
|
30377
|
+
addIcons({ closeOutline, informationCircleOutline });
|
|
30319
30378
|
}
|
|
30320
30379
|
ngOnDestroy() {
|
|
30321
30380
|
this.stopCooldown();
|
|
@@ -30328,6 +30387,21 @@ class MfaModalComponent {
|
|
|
30328
30387
|
close() {
|
|
30329
30388
|
this.dismissed.emit();
|
|
30330
30389
|
}
|
|
30390
|
+
/**
|
|
30391
|
+
* Punto de entrada al abrir el modal. Con `prefillCode` (deep-link del email
|
|
30392
|
+
* de setup MFA-email) salta directo a la confirmación; si no, resuelve el
|
|
30393
|
+
* estado MFA actual.
|
|
30394
|
+
*/
|
|
30395
|
+
open() {
|
|
30396
|
+
if (this.prefillCode) {
|
|
30397
|
+
this.resetFlow();
|
|
30398
|
+
this.selectedMethod.set('EMAIL');
|
|
30399
|
+
this.pinControl.setValue(this.prefillCode);
|
|
30400
|
+
this._step.set('code-confirm');
|
|
30401
|
+
return;
|
|
30402
|
+
}
|
|
30403
|
+
this.resolveStatus();
|
|
30404
|
+
}
|
|
30331
30405
|
// ===========================================================================
|
|
30332
30406
|
// Carga de estado
|
|
30333
30407
|
// ===========================================================================
|
|
@@ -30368,7 +30442,6 @@ class MfaModalComponent {
|
|
|
30368
30442
|
this._step.set('method-select');
|
|
30369
30443
|
}
|
|
30370
30444
|
goToDisable() {
|
|
30371
|
-
this.passwordControl.reset();
|
|
30372
30445
|
this._step.set('disable');
|
|
30373
30446
|
}
|
|
30374
30447
|
backToStatus() {
|
|
@@ -30501,8 +30574,8 @@ class MfaModalComponent {
|
|
|
30501
30574
|
});
|
|
30502
30575
|
}
|
|
30503
30576
|
/** Deshabilita MFA — requiere la contraseña de la cuenta. */
|
|
30504
|
-
|
|
30505
|
-
const password =
|
|
30577
|
+
onDisableSubmit(event) {
|
|
30578
|
+
const password = event.fields['password'];
|
|
30506
30579
|
if (!password) {
|
|
30507
30580
|
this.showToast(this.t('mfaPasswordRequired'));
|
|
30508
30581
|
return;
|
|
@@ -30551,7 +30624,6 @@ class MfaModalComponent {
|
|
|
30551
30624
|
// ===========================================================================
|
|
30552
30625
|
resetFlow() {
|
|
30553
30626
|
this.pinControl.reset();
|
|
30554
|
-
this.passwordControl.reset();
|
|
30555
30627
|
this.phoneControl.reset();
|
|
30556
30628
|
this.totpSetup.set(null);
|
|
30557
30629
|
this.totpQr.set(null);
|
|
@@ -30606,7 +30678,7 @@ class MfaModalComponent {
|
|
|
30606
30678
|
this.toast.show({ message, duration: 3500 });
|
|
30607
30679
|
}
|
|
30608
30680
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: MfaModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
30609
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: MfaModalComponent, isStandalone: true, selector: "val-mfa-modal", inputs: { isOpen: "isOpen" }, outputs: { changed: "changed", dismissed: "dismissed" }, ngImport: i0, template: "<ion-modal [isOpen]=\"isOpen\" (didDismiss)=\"close()\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" (click)=\"close()\">\n <ion-icon name=\"close-outline\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n\n <ion-content class=\"ion-padding\">\n <section class=\"mfa-modal\">\n @switch (step()) { @case ('loading') {\n <div class=\"mfa-loading\">\n <ion-spinner name=\"crescent\"></ion-spinner>\n </div>\n } @case ('status') {\n <h2 class=\"mfa-title\">{{ t('mfaManageTitle') }}</h2>\n\n @if (mfaEnabled()) {\n <p class=\"mfa-status mfa-status--on\">{{ t('mfaEnabledLabel') }} \u00B7 {{ methodLabel(mfaMethod()) }}</p>\n\n @if (mfaMethod() === 'TOTP') {\n <div class=\"mfa-block\">\n <h3>{{ t('mfaBackupCodesTitle') }}</h3>\n <p class=\"mfa-muted\">{{ t('mfaBackupCodesAvailable') }}: {{ backupCodesCount() }}</p>\n @if (backupCodesCount() < 3) {\n <p class=\"mfa-warn\">{{ t('mfaBackupCodesLow') }}</p>\n } @if (regeneratedCodes(); as codes) {\n <p class=\"mfa-warn\">{{ t('mfaBackupCodesSaveWarning') }}</p>\n <div class=\"mfa-codes\">\n @for (code of codes; track code) {\n <code class=\"mfa-code\">{{ code }}</code>\n }\n </div>\n <ion-button expand=\"block\" fill=\"outline\" (click)=\"copyCodes(codes)\"> {{ t('mfaCopyCodes') }} </ion-button>\n } @else {\n <ion-button expand=\"block\" fill=\"outline\" [disabled]=\"working()\" (click)=\"regenerateBackupCodes()\">\n @if (working()) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else { {{ t('mfaRegenerateCodes') }} }\n </ion-button>\n }\n </div>\n }\n\n <ion-button expand=\"block\" color=\"danger\" fill=\"outline\" (click)=\"goToDisable()\">\n {{ t('mfaDisableButton') }}\n </ion-button>\n } @else {\n <p class=\"mfa-status mfa-status--off\">{{ t('mfaDisabledLabel') }}</p>\n <p class=\"mfa-muted\">{{ t('mfaDisabledHint') }}</p>\n <ion-button expand=\"block\" (click)=\"goToMethodSelect()\"> {{ t('mfaEnableButton') }} </ion-button>\n } } @case ('method-select') {\n <h2 class=\"mfa-title\">{{ t('mfaEnableTitle') }}</h2>\n <p class=\"mfa-muted\">{{ t('mfaMethodPrompt') }}</p>\n\n <ion-radio-group [value]=\"selectedMethod()\" (ionChange)=\"selectedMethod.set($event.detail.value)\">\n <ion-item>\n <ion-radio slot=\"start\" value=\"TOTP\"></ion-radio>\n <ion-label>\n <strong>{{ t('mfaMethodTotp') }}</strong>\n <p>{{ t('mfaMethodTotpHint') }}</p>\n </ion-label>\n </ion-item>\n <ion-item>\n <ion-radio slot=\"start\" value=\"EMAIL\"></ion-radio>\n <ion-label>\n <strong>{{ t('mfaMethodEmail') }}</strong>\n <p>{{ t('mfaMethodEmailHint') }}</p>\n </ion-label>\n </ion-item>\n <ion-item>\n <ion-radio slot=\"start\" value=\"SMS\"></ion-radio>\n <ion-label>\n <strong>{{ t('mfaMethodSms') }}</strong>\n <p>{{ t('mfaMethodSmsHint') }}</p>\n </ion-label>\n </ion-item>\n </ion-radio-group>\n\n @if (selectedMethod() === 'SMS' && !userPhone()) {\n <ion-input\n label=\"{{ t('mfaPhoneLabel') }}\"\n labelPlacement=\"floating\"\n type=\"tel\"\n placeholder=\"+56912345678\"\n [formControl]=\"phoneControl\"\n ></ion-input>\n @if (phoneControl.invalid && phoneControl.touched) {\n <p class=\"mfa-error\">{{ t('mfaPhoneInvalid') }}</p>\n } } @if (selectedMethod() === 'SMS' && userPhone(); as phone) {\n <p class=\"mfa-muted\">{{ t('mfaPhoneRegistered') }}: {{ phone }}</p>\n }\n\n <ion-button expand=\"block\" [disabled]=\"working()\" (click)=\"proceedWithMethod()\">\n @if (working()) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else { {{ t('mfaContinue') }} }\n </ion-button>\n <ion-button expand=\"block\" fill=\"clear\" color=\"medium\" (click)=\"backToStatus()\">\n {{ t('mfaCancel') }}\n </ion-button>\n } @case ('totp-setup') {\n <h2 class=\"mfa-title\">{{ t('mfaTotpSetupTitle') }}</h2>\n <p class=\"mfa-muted\">{{ t('mfaTotpStep1') }}</p>\n\n @if (totpQr(); as qr) {\n <div class=\"mfa-qr\">\n <val-qr-code [props]=\"{ qr: qr }\" />\n </div>\n } @if (totpSetup(); as setup) {\n <p class=\"mfa-muted\">{{ t('mfaTotpManualEntry') }}</p>\n <code class=\"mfa-secret\">{{ setup.secret }}</code>\n\n <p class=\"mfa-muted\">{{ t('mfaTotpStep2') }}</p>\n <div class=\"mfa-pin\">\n <val-pin-input [props]=\"pinInputProps\" />\n </div>\n\n <ion-button expand=\"block\" [disabled]=\"working()\" (click)=\"verifyTotp()\">\n @if (working()) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else { {{ t('mfaTotpVerify') }} }\n </ion-button>\n\n <div class=\"mfa-block mfa-block--warn\">\n <p class=\"mfa-warn\">{{ t('mfaBackupCodesSaveWarning') }}</p>\n <p class=\"mfa-muted\">{{ t('mfaBackupCodesExplain') }}</p>\n <div class=\"mfa-codes\">\n @for (code of setup.backupCodes; track code) {\n <code class=\"mfa-code\">{{ code }}</code>\n }\n </div>\n <ion-button expand=\"block\" fill=\"outline\" (click)=\"copyCodes(setup.backupCodes)\">\n {{ t('mfaCopyCodes') }}\n </ion-button>\n </div>\n }\n\n <ion-button expand=\"block\" fill=\"clear\" color=\"medium\" (click)=\"backToStatus()\">\n {{ t('mfaCancel') }}\n </ion-button>\n } @case ('code-confirm') {\n <h2 class=\"mfa-title\">{{ t('mfaConfirmTitle') }}</h2>\n <p class=\"mfa-muted\">\n {{ selectedMethod() === 'EMAIL' ? t('mfaConfirmPromptEmail') : t('mfaConfirmPromptSms') }}\n </p>\n\n <div class=\"mfa-pin\">\n <val-pin-input [props]=\"pinInputProps\" />\n </div>\n\n <ion-button expand=\"block\" [disabled]=\"working()\" (click)=\"confirmCode()\">\n @if (working()) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else { {{ t('mfaConfirmButton') }} }\n </ion-button>\n\n <p class=\"mfa-resend\">\n {{ t('mfaNoCode') }} @if (resendCooldown() > 0) {\n <span class=\"mfa-muted\">{{ t('mfaResendIn') }} {{ resendCooldown() }}s</span>\n } @else {\n <a (click)=\"resendCode()\">{{ t('mfaResend') }}</a>\n }\n </p>\n\n <ion-button expand=\"block\" fill=\"clear\" color=\"medium\" (click)=\"backToStatus()\">\n {{ t('mfaCancel') }}\n </ion-button>\n } @case ('disable') {\n <h2 class=\"mfa-title\">{{ t('mfaDisableTitle') }}</h2>\n <p class=\"mfa-muted\">{{ t('mfaDisablePrompt') }}</p>\n\n <ion-input\n label=\"{{ t('mfaPasswordLabel') }}\"\n labelPlacement=\"floating\"\n type=\"password\"\n [formControl]=\"passwordControl\"\n ></ion-input>\n\n <ion-button\n expand=\"block\"\n color=\"danger\"\n [disabled]=\"working() || passwordControl.invalid\"\n (click)=\"confirmDisable()\"\n >\n @if (working()) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else { {{ t('mfaDisableButton') }} }\n </ion-button>\n <ion-button expand=\"block\" fill=\"clear\" color=\"medium\" (click)=\"backToStatus()\">\n {{ t('mfaCancel') }}\n </ion-button>\n } }\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n", styles: [".mfa-modal{max-width:460px;margin:0 auto;display:flex;flex-direction:column;gap:12px;padding-top:4px}.mfa-loading{display:flex;justify-content:center;padding:40px 0}.mfa-title{font-size:18px;font-weight:700;margin:0;color:var(--ion-color-dark)}.mfa-status{font-weight:600;margin:0}.mfa-status--on{color:var(--ion-color-success-shade)}.mfa-status--off{color:var(--ion-color-dark)}.mfa-muted{color:var(--ion-color-medium);font-size:14px;margin:0}.mfa-warn{color:var(--ion-color-warning-shade);font-size:14px;font-weight:600;margin:0}.mfa-error{color:var(--ion-color-danger);font-size:13px;margin:4px 0 0}.mfa-block{display:flex;flex-direction:column;gap:10px;padding:14px;border-radius:12px;background:var(--ion-color-light)}.mfa-block--warn{background:var(--ion-color-warning-tint, rgba(255, 196, 9, .12));border:1px solid var(--ion-color-warning)}.mfa-block h3{font-size:15px;font-weight:600;margin:0;color:var(--ion-color-dark)}.mfa-qr{display:flex;justify-content:center;padding:12px 0}.mfa-secret{display:block;padding:10px 12px;border-radius:8px;background:var(--ion-color-light);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:15px;letter-spacing:.04em;word-break:break-all;text-align:center}.mfa-pin{display:flex;justify-content:center;padding:8px 0}.mfa-codes{display:grid;grid-template-columns:repeat(2,1fr);gap:8px}.mfa-code{padding:8px;border-radius:6px;background:var(--ion-background-color, #fff);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:13px;text-align:center}.mfa-resend{text-align:center;font-size:14px;color:var(--ion-color-medium);margin:4px 0}.mfa-resend a{color:var(--ion-color-primary);cursor:pointer;font-weight:600}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$3.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "component", type: IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: IonInput, selector: "ion-input", inputs: ["accept", "autocapitalize", "autocomplete", "autocorrect", "autofocus", "clearInput", "clearOnEdit", "color", "counter", "counterFormatter", "debounce", "disabled", "enterkeyhint", "errorText", "fill", "helperText", "inputmode", "label", "labelPlacement", "max", "maxlength", "min", "minlength", "mode", "multiple", "name", "pattern", "placeholder", "readonly", "required", "shape", "size", "spellcheck", "step", "type", "value"] }, { kind: "component", type: IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: IonModal, selector: "ion-modal" }, { kind: "component", type: IonRadio, selector: "ion-radio", inputs: ["alignment", "color", "disabled", "justify", "labelPlacement", "mode", "name", "value"] }, { kind: "component", type: IonRadioGroup, selector: "ion-radio-group", inputs: ["allowEmptySelection", "compareWith", "errorText", "helperText", "name", "value"] }, { kind: "component", type: IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { kind: "component", type: IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: QrCodeComponent, selector: "val-qr-code", inputs: ["props"], outputs: ["actionComplete", "imageLoad", "imageError"] }, { kind: "component", type: PinInputComponent, selector: "val-pin-input", inputs: ["props"] }] }); }
|
|
30681
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: MfaModalComponent, isStandalone: true, selector: "val-mfa-modal", inputs: { isOpen: "isOpen", prefillCode: "prefillCode" }, outputs: { changed: "changed", dismissed: "dismissed" }, ngImport: i0, template: "<ion-modal [isOpen]=\"isOpen\" (didDismiss)=\"close()\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" (click)=\"close()\">\n <ion-icon name=\"close-outline\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n\n <ion-content class=\"ion-padding\">\n <section class=\"mfa-modal\">\n @switch (step()) { @case ('loading') {\n <div class=\"mfa-loading\">\n <ion-spinner name=\"crescent\"></ion-spinner>\n </div>\n } @case ('status') {\n <h2 class=\"mfa-title\">{{ t('mfaManageTitle') }}</h2>\n\n @if (mfaEnabled()) {\n <p class=\"mfa-status mfa-status--on\">{{ t('mfaEnabledLabel') }} \u00B7 {{ methodLabel(mfaMethod()) }}</p>\n\n @if (mfaMethod() === 'TOTP') {\n <div class=\"mfa-block\">\n <h3>{{ t('mfaBackupCodesTitle') }}</h3>\n <p class=\"mfa-text\">{{ t('mfaBackupCodesAvailable') }}: {{ backupCodesCount() }}</p>\n\n @if (regeneratedCodes(); as codes) {\n <div class=\"mfa-alert\">\n <ion-icon name=\"information-circle-outline\"></ion-icon>\n <p>{{ t('mfaBackupCodesSaveWarning') }} {{ t('mfaBackupCodesExplain') }}</p>\n </div>\n <div class=\"mfa-codes\">\n @for (code of codes; track code) {\n <code class=\"mfa-code\">{{ code }}</code>\n }\n </div>\n <ion-button expand=\"block\" color=\"dark\" fill=\"outline\" (click)=\"copyCodes(codes)\">\n {{ t('mfaCopyCodes') }}\n </ion-button>\n } @else { @if (backupCodesCount() < 3) {\n <div class=\"mfa-alert\">\n <ion-icon name=\"information-circle-outline\"></ion-icon>\n <p>{{ t('mfaBackupCodesLow') }}</p>\n </div>\n }\n <ion-button\n expand=\"block\"\n color=\"dark\"\n fill=\"outline\"\n [disabled]=\"working()\"\n (click)=\"regenerateBackupCodes()\"\n >\n @if (working()) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else { {{ t('mfaRegenerateCodes') }} }\n </ion-button>\n }\n </div>\n }\n\n <ion-button expand=\"block\" color=\"dark\" fill=\"outline\" (click)=\"goToDisable()\">\n {{ t('mfaDisableButton') }}\n </ion-button>\n } @else {\n <p class=\"mfa-status mfa-status--off\">{{ t('mfaDisabledLabel') }}</p>\n <p class=\"mfa-text\">{{ t('mfaDisabledHint') }}</p>\n <ion-button expand=\"block\" (click)=\"goToMethodSelect()\"> {{ t('mfaEnableButton') }} </ion-button>\n } } @case ('method-select') {\n <h2 class=\"mfa-title\">{{ t('mfaEnableTitle') }}</h2>\n <p class=\"mfa-text\">{{ t('mfaMethodPrompt') }}</p>\n\n <ion-radio-group [value]=\"selectedMethod()\" (ionChange)=\"selectedMethod.set($event.detail.value)\">\n <ion-item>\n <ion-radio slot=\"start\" value=\"TOTP\"></ion-radio>\n <ion-label>\n <strong>{{ t('mfaMethodTotp') }}</strong>\n <p>{{ t('mfaMethodTotpHint') }}</p>\n </ion-label>\n </ion-item>\n <ion-item>\n <ion-radio slot=\"start\" value=\"EMAIL\"></ion-radio>\n <ion-label>\n <strong>{{ t('mfaMethodEmail') }}</strong>\n <p>{{ t('mfaMethodEmailHint') }}</p>\n </ion-label>\n </ion-item>\n <ion-item>\n <ion-radio slot=\"start\" value=\"SMS\"></ion-radio>\n <ion-label>\n <strong>{{ t('mfaMethodSms') }}</strong>\n <p>{{ t('mfaMethodSmsHint') }}</p>\n </ion-label>\n </ion-item>\n </ion-radio-group>\n\n @if (selectedMethod() === 'SMS' && !userPhone()) {\n <ion-input\n label=\"{{ t('mfaPhoneLabel') }}\"\n labelPlacement=\"floating\"\n fill=\"outline\"\n type=\"tel\"\n placeholder=\"+56912345678\"\n [formControl]=\"phoneControl\"\n ></ion-input>\n @if (phoneControl.invalid && phoneControl.touched) {\n <p class=\"mfa-error\">{{ t('mfaPhoneInvalid') }}</p>\n } } @if (selectedMethod() === 'SMS' && userPhone(); as phone) {\n <p class=\"mfa-text\">{{ t('mfaPhoneRegistered') }}: {{ phone }}</p>\n }\n\n <ion-button expand=\"block\" [disabled]=\"working()\" (click)=\"proceedWithMethod()\">\n @if (working()) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else { {{ t('mfaContinue') }} }\n </ion-button>\n <ion-button expand=\"block\" fill=\"clear\" color=\"dark\" (click)=\"backToStatus()\">\n {{ t('mfaCancel') }}\n </ion-button>\n } @case ('totp-setup') {\n <h2 class=\"mfa-title\">{{ t('mfaTotpSetupTitle') }}</h2>\n <p class=\"mfa-text\">{{ t('mfaTotpStep1') }}</p>\n\n @if (totpQr(); as qr) {\n <div class=\"mfa-qr\">\n <val-qr-code [props]=\"{ qr: qr }\" />\n </div>\n } @if (totpSetup(); as setup) {\n <p class=\"mfa-text\">{{ t('mfaTotpManualEntry') }}</p>\n <code class=\"mfa-secret\">{{ setup.secret }}</code>\n\n <p class=\"mfa-text\">{{ t('mfaTotpStep2') }}</p>\n <div class=\"mfa-pin\">\n <val-pin-input [props]=\"pinInputProps\" />\n </div>\n\n <ion-button expand=\"block\" [disabled]=\"working()\" (click)=\"verifyTotp()\">\n @if (working()) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else { {{ t('mfaTotpVerify') }} }\n </ion-button>\n\n <div class=\"mfa-backup\">\n <h3>{{ t('mfaBackupCodesTitle') }}</h3>\n <div class=\"mfa-alert\">\n <ion-icon name=\"information-circle-outline\"></ion-icon>\n <p>{{ t('mfaBackupCodesSaveWarning') }} {{ t('mfaBackupCodesExplain') }}</p>\n </div>\n <div class=\"mfa-codes\">\n @for (code of setup.backupCodes; track code) {\n <code class=\"mfa-code\">{{ code }}</code>\n }\n </div>\n <ion-button expand=\"block\" color=\"dark\" fill=\"outline\" (click)=\"copyCodes(setup.backupCodes)\">\n {{ t('mfaCopyCodes') }}\n </ion-button>\n </div>\n }\n\n <ion-button expand=\"block\" fill=\"clear\" color=\"dark\" (click)=\"backToStatus()\">\n {{ t('mfaCancel') }}\n </ion-button>\n } @case ('code-confirm') {\n <h2 class=\"mfa-title\">{{ t('mfaConfirmTitle') }}</h2>\n <p class=\"mfa-text\">\n {{ selectedMethod() === 'EMAIL' ? t('mfaConfirmPromptEmail') : t('mfaConfirmPromptSms') }}\n </p>\n\n <div class=\"mfa-pin\">\n <val-pin-input [props]=\"pinInputProps\" />\n </div>\n\n <ion-button expand=\"block\" [disabled]=\"working()\" (click)=\"confirmCode()\">\n @if (working()) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else { {{ t('mfaConfirmButton') }} }\n </ion-button>\n\n <p class=\"mfa-resend\">\n {{ t('mfaNoCode') }} @if (resendCooldown() > 0) {\n <span class=\"mfa-text\">{{ t('mfaResendIn') }} {{ resendCooldown() }}s</span>\n } @else {\n <a (click)=\"resendCode()\">{{ t('mfaResend') }}</a>\n }\n </p>\n\n <ion-button expand=\"block\" fill=\"clear\" color=\"dark\" (click)=\"backToStatus()\">\n {{ t('mfaCancel') }}\n </ion-button>\n } @case ('disable') {\n <val-form [props]=\"disableFormProps()\" (onSubmit)=\"onDisableSubmit($event)\" />\n <ion-button expand=\"block\" fill=\"clear\" color=\"dark\" (click)=\"backToStatus()\">\n {{ t('mfaCancel') }}\n </ion-button>\n } }\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n", styles: [".mfa-modal{display:flex;flex-direction:column;gap:14px;padding-top:4px}.mfa-loading{display:flex;justify-content:center;padding:40px 0}.mfa-title{font-size:18px;font-weight:700;margin:0;color:var(--ion-color-dark)}.mfa-status{font-weight:600;margin:0}.mfa-status--on{color:var(--ion-color-success-shade)}.mfa-status--off{color:var(--ion-color-dark)}.mfa-text{color:var(--ion-color-dark);font-size:14px;margin:0}.mfa-error{color:var(--ion-color-danger);font-size:13px;margin:4px 0 0}.mfa-block,.mfa-backup{display:flex;flex-direction:column;gap:12px}.mfa-block{padding:16px;border-radius:14px;background:var(--ion-color-light)}.mfa-block h3,.mfa-backup h3{font-size:15px;font-weight:600;margin:0;color:var(--ion-color-dark)}.mfa-alert{display:flex;align-items:flex-start;gap:10px;padding:12px 14px;border-radius:10px;background:var(--ion-color-light);border-left:3px solid var(--ion-color-primary)}.mfa-alert ion-icon{font-size:20px;color:var(--ion-color-primary);flex-shrink:0;margin-top:1px}.mfa-alert p{margin:0;font-size:13px;line-height:1.45;color:var(--ion-color-dark)}.mfa-qr{display:flex;justify-content:center;padding:8px 0}.mfa-secret{display:block;padding:10px 12px;border-radius:8px;background:var(--ion-color-light);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:15px;letter-spacing:.04em;word-break:break-all;text-align:center}.mfa-pin{display:flex;justify-content:center;padding:8px 0}.mfa-codes{display:grid;grid-template-columns:repeat(2,1fr);gap:8px;padding:12px;border-radius:12px;background:var(--ion-color-light)}.mfa-code{padding:8px;border-radius:6px;background:var(--ion-background-color, #fff);color:var(--ion-color-dark);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:13px;text-align:center}.mfa-resend{text-align:center;font-size:14px;color:var(--ion-color-dark);margin:4px 0}.mfa-resend a{color:var(--ion-color-primary);cursor:pointer;font-weight:600}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$3.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "component", type: IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: IonInput, selector: "ion-input", inputs: ["accept", "autocapitalize", "autocomplete", "autocorrect", "autofocus", "clearInput", "clearOnEdit", "color", "counter", "counterFormatter", "debounce", "disabled", "enterkeyhint", "errorText", "fill", "helperText", "inputmode", "label", "labelPlacement", "max", "maxlength", "min", "minlength", "mode", "multiple", "name", "pattern", "placeholder", "readonly", "required", "shape", "size", "spellcheck", "step", "type", "value"] }, { kind: "component", type: IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: IonModal, selector: "ion-modal" }, { kind: "component", type: IonRadio, selector: "ion-radio", inputs: ["alignment", "color", "disabled", "justify", "labelPlacement", "mode", "name", "value"] }, { kind: "component", type: IonRadioGroup, selector: "ion-radio-group", inputs: ["allowEmptySelection", "compareWith", "errorText", "helperText", "name", "value"] }, { kind: "component", type: IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { kind: "component", type: IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: FormComponent, selector: "val-form", inputs: ["props"], outputs: ["onSubmit", "onInvalid", "onSelectChange"] }, { kind: "component", type: QrCodeComponent, selector: "val-qr-code", inputs: ["props"], outputs: ["actionComplete", "imageLoad", "imageError"] }, { kind: "component", type: PinInputComponent, selector: "val-pin-input", inputs: ["props"] }] }); }
|
|
30610
30682
|
}
|
|
30611
30683
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: MfaModalComponent, decorators: [{
|
|
30612
30684
|
type: Component,
|
|
@@ -30625,11 +30697,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
30625
30697
|
IonRadioGroup,
|
|
30626
30698
|
IonSpinner,
|
|
30627
30699
|
IonToolbar,
|
|
30700
|
+
FormComponent,
|
|
30628
30701
|
QrCodeComponent,
|
|
30629
30702
|
PinInputComponent,
|
|
30630
|
-
], template: "<ion-modal [isOpen]=\"isOpen\" (didDismiss)=\"close()\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" (click)=\"close()\">\n <ion-icon name=\"close-outline\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n\n <ion-content class=\"ion-padding\">\n <section class=\"mfa-modal\">\n @switch (step()) { @case ('loading') {\n <div class=\"mfa-loading\">\n <ion-spinner name=\"crescent\"></ion-spinner>\n </div>\n } @case ('status') {\n <h2 class=\"mfa-title\">{{ t('mfaManageTitle') }}</h2>\n\n @if (mfaEnabled()) {\n <p class=\"mfa-status mfa-status--on\">{{ t('mfaEnabledLabel') }} \u00B7 {{ methodLabel(mfaMethod()) }}</p>\n\n @if (mfaMethod() === 'TOTP') {\n <div class=\"mfa-block\">\n <h3>{{ t('mfaBackupCodesTitle') }}</h3>\n <p class=\"mfa-muted\">{{ t('mfaBackupCodesAvailable') }}: {{ backupCodesCount() }}</p>\n @if (backupCodesCount() < 3) {\n <p class=\"mfa-warn\">{{ t('mfaBackupCodesLow') }}</p>\n } @if (regeneratedCodes(); as codes) {\n <p class=\"mfa-warn\">{{ t('mfaBackupCodesSaveWarning') }}</p>\n <div class=\"mfa-codes\">\n @for (code of codes; track code) {\n <code class=\"mfa-code\">{{ code }}</code>\n }\n </div>\n <ion-button expand=\"block\" fill=\"outline\" (click)=\"copyCodes(codes)\"> {{ t('mfaCopyCodes') }} </ion-button>\n } @else {\n <ion-button expand=\"block\" fill=\"outline\" [disabled]=\"working()\" (click)=\"regenerateBackupCodes()\">\n @if (working()) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else { {{ t('mfaRegenerateCodes') }} }\n </ion-button>\n }\n </div>\n }\n\n <ion-button expand=\"block\" color=\"danger\" fill=\"outline\" (click)=\"goToDisable()\">\n {{ t('mfaDisableButton') }}\n </ion-button>\n } @else {\n <p class=\"mfa-status mfa-status--off\">{{ t('mfaDisabledLabel') }}</p>\n <p class=\"mfa-muted\">{{ t('mfaDisabledHint') }}</p>\n <ion-button expand=\"block\" (click)=\"goToMethodSelect()\"> {{ t('mfaEnableButton') }} </ion-button>\n } } @case ('method-select') {\n <h2 class=\"mfa-title\">{{ t('mfaEnableTitle') }}</h2>\n <p class=\"mfa-muted\">{{ t('mfaMethodPrompt') }}</p>\n\n <ion-radio-group [value]=\"selectedMethod()\" (ionChange)=\"selectedMethod.set($event.detail.value)\">\n <ion-item>\n <ion-radio slot=\"start\" value=\"TOTP\"></ion-radio>\n <ion-label>\n <strong>{{ t('mfaMethodTotp') }}</strong>\n <p>{{ t('mfaMethodTotpHint') }}</p>\n </ion-label>\n </ion-item>\n <ion-item>\n <ion-radio slot=\"start\" value=\"EMAIL\"></ion-radio>\n <ion-label>\n <strong>{{ t('mfaMethodEmail') }}</strong>\n <p>{{ t('mfaMethodEmailHint') }}</p>\n </ion-label>\n </ion-item>\n <ion-item>\n <ion-radio slot=\"start\" value=\"SMS\"></ion-radio>\n <ion-label>\n <strong>{{ t('mfaMethodSms') }}</strong>\n <p>{{ t('mfaMethodSmsHint') }}</p>\n </ion-label>\n </ion-item>\n </ion-radio-group>\n\n @if (selectedMethod() === 'SMS' && !userPhone()) {\n <ion-input\n label=\"{{ t('mfaPhoneLabel') }}\"\n labelPlacement=\"floating\"\n type=\"tel\"\n placeholder=\"+56912345678\"\n [formControl]=\"phoneControl\"\n ></ion-input>\n @if (phoneControl.invalid && phoneControl.touched) {\n <p class=\"mfa-error\">{{ t('mfaPhoneInvalid') }}</p>\n } } @if (selectedMethod() === 'SMS' && userPhone(); as phone) {\n <p class=\"mfa-muted\">{{ t('mfaPhoneRegistered') }}: {{ phone }}</p>\n }\n\n <ion-button expand=\"block\" [disabled]=\"working()\" (click)=\"proceedWithMethod()\">\n @if (working()) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else { {{ t('mfaContinue') }} }\n </ion-button>\n <ion-button expand=\"block\" fill=\"clear\" color=\"medium\" (click)=\"backToStatus()\">\n {{ t('mfaCancel') }}\n </ion-button>\n } @case ('totp-setup') {\n <h2 class=\"mfa-title\">{{ t('mfaTotpSetupTitle') }}</h2>\n <p class=\"mfa-muted\">{{ t('mfaTotpStep1') }}</p>\n\n @if (totpQr(); as qr) {\n <div class=\"mfa-qr\">\n <val-qr-code [props]=\"{ qr: qr }\" />\n </div>\n } @if (totpSetup(); as setup) {\n <p class=\"mfa-muted\">{{ t('mfaTotpManualEntry') }}</p>\n <code class=\"mfa-secret\">{{ setup.secret }}</code>\n\n <p class=\"mfa-muted\">{{ t('mfaTotpStep2') }}</p>\n <div class=\"mfa-pin\">\n <val-pin-input [props]=\"pinInputProps\" />\n </div>\n\n <ion-button expand=\"block\" [disabled]=\"working()\" (click)=\"verifyTotp()\">\n @if (working()) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else { {{ t('mfaTotpVerify') }} }\n </ion-button>\n\n <div class=\"mfa-block mfa-block--warn\">\n <p class=\"mfa-warn\">{{ t('mfaBackupCodesSaveWarning') }}</p>\n <p class=\"mfa-muted\">{{ t('mfaBackupCodesExplain') }}</p>\n <div class=\"mfa-codes\">\n @for (code of setup.backupCodes; track code) {\n <code class=\"mfa-code\">{{ code }}</code>\n }\n </div>\n <ion-button expand=\"block\" fill=\"outline\" (click)=\"copyCodes(setup.backupCodes)\">\n {{ t('mfaCopyCodes') }}\n </ion-button>\n </div>\n }\n\n <ion-button expand=\"block\" fill=\"clear\" color=\"medium\" (click)=\"backToStatus()\">\n {{ t('mfaCancel') }}\n </ion-button>\n } @case ('code-confirm') {\n <h2 class=\"mfa-title\">{{ t('mfaConfirmTitle') }}</h2>\n <p class=\"mfa-muted\">\n {{ selectedMethod() === 'EMAIL' ? t('mfaConfirmPromptEmail') : t('mfaConfirmPromptSms') }}\n </p>\n\n <div class=\"mfa-pin\">\n <val-pin-input [props]=\"pinInputProps\" />\n </div>\n\n <ion-button expand=\"block\" [disabled]=\"working()\" (click)=\"confirmCode()\">\n @if (working()) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else { {{ t('mfaConfirmButton') }} }\n </ion-button>\n\n <p class=\"mfa-resend\">\n {{ t('mfaNoCode') }} @if (resendCooldown() > 0) {\n <span class=\"mfa-muted\">{{ t('mfaResendIn') }} {{ resendCooldown() }}s</span>\n } @else {\n <a (click)=\"resendCode()\">{{ t('mfaResend') }}</a>\n }\n </p>\n\n <ion-button expand=\"block\" fill=\"clear\" color=\"medium\" (click)=\"backToStatus()\">\n {{ t('mfaCancel') }}\n </ion-button>\n } @case ('disable') {\n <h2 class=\"mfa-title\">{{ t('mfaDisableTitle') }}</h2>\n <p class=\"mfa-muted\">{{ t('mfaDisablePrompt') }}</p>\n\n <ion-input\n label=\"{{ t('mfaPasswordLabel') }}\"\n labelPlacement=\"floating\"\n type=\"password\"\n [formControl]=\"passwordControl\"\n ></ion-input>\n\n <ion-button\n expand=\"block\"\n color=\"danger\"\n [disabled]=\"working() || passwordControl.invalid\"\n (click)=\"confirmDisable()\"\n >\n @if (working()) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else { {{ t('mfaDisableButton') }} }\n </ion-button>\n <ion-button expand=\"block\" fill=\"clear\" color=\"medium\" (click)=\"backToStatus()\">\n {{ t('mfaCancel') }}\n </ion-button>\n } }\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n", styles: [".mfa-modal{max-width:460px;margin:0 auto;display:flex;flex-direction:column;gap:12px;padding-top:4px}.mfa-loading{display:flex;justify-content:center;padding:40px 0}.mfa-title{font-size:18px;font-weight:700;margin:0;color:var(--ion-color-dark)}.mfa-status{font-weight:600;margin:0}.mfa-status--on{color:var(--ion-color-success-shade)}.mfa-status--off{color:var(--ion-color-dark)}.mfa-muted{color:var(--ion-color-medium);font-size:14px;margin:0}.mfa-warn{color:var(--ion-color-warning-shade);font-size:14px;font-weight:600;margin:0}.mfa-error{color:var(--ion-color-danger);font-size:13px;margin:4px 0 0}.mfa-block{display:flex;flex-direction:column;gap:10px;padding:14px;border-radius:12px;background:var(--ion-color-light)}.mfa-block--warn{background:var(--ion-color-warning-tint, rgba(255, 196, 9, .12));border:1px solid var(--ion-color-warning)}.mfa-block h3{font-size:15px;font-weight:600;margin:0;color:var(--ion-color-dark)}.mfa-qr{display:flex;justify-content:center;padding:12px 0}.mfa-secret{display:block;padding:10px 12px;border-radius:8px;background:var(--ion-color-light);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:15px;letter-spacing:.04em;word-break:break-all;text-align:center}.mfa-pin{display:flex;justify-content:center;padding:8px 0}.mfa-codes{display:grid;grid-template-columns:repeat(2,1fr);gap:8px}.mfa-code{padding:8px;border-radius:6px;background:var(--ion-background-color, #fff);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:13px;text-align:center}.mfa-resend{text-align:center;font-size:14px;color:var(--ion-color-medium);margin:4px 0}.mfa-resend a{color:var(--ion-color-primary);cursor:pointer;font-weight:600}\n"] }]
|
|
30703
|
+
], template: "<ion-modal [isOpen]=\"isOpen\" (didDismiss)=\"close()\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-buttons slot=\"end\">\n <ion-button fill=\"clear\" (click)=\"close()\">\n <ion-icon name=\"close-outline\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n\n <ion-content class=\"ion-padding\">\n <section class=\"mfa-modal\">\n @switch (step()) { @case ('loading') {\n <div class=\"mfa-loading\">\n <ion-spinner name=\"crescent\"></ion-spinner>\n </div>\n } @case ('status') {\n <h2 class=\"mfa-title\">{{ t('mfaManageTitle') }}</h2>\n\n @if (mfaEnabled()) {\n <p class=\"mfa-status mfa-status--on\">{{ t('mfaEnabledLabel') }} \u00B7 {{ methodLabel(mfaMethod()) }}</p>\n\n @if (mfaMethod() === 'TOTP') {\n <div class=\"mfa-block\">\n <h3>{{ t('mfaBackupCodesTitle') }}</h3>\n <p class=\"mfa-text\">{{ t('mfaBackupCodesAvailable') }}: {{ backupCodesCount() }}</p>\n\n @if (regeneratedCodes(); as codes) {\n <div class=\"mfa-alert\">\n <ion-icon name=\"information-circle-outline\"></ion-icon>\n <p>{{ t('mfaBackupCodesSaveWarning') }} {{ t('mfaBackupCodesExplain') }}</p>\n </div>\n <div class=\"mfa-codes\">\n @for (code of codes; track code) {\n <code class=\"mfa-code\">{{ code }}</code>\n }\n </div>\n <ion-button expand=\"block\" color=\"dark\" fill=\"outline\" (click)=\"copyCodes(codes)\">\n {{ t('mfaCopyCodes') }}\n </ion-button>\n } @else { @if (backupCodesCount() < 3) {\n <div class=\"mfa-alert\">\n <ion-icon name=\"information-circle-outline\"></ion-icon>\n <p>{{ t('mfaBackupCodesLow') }}</p>\n </div>\n }\n <ion-button\n expand=\"block\"\n color=\"dark\"\n fill=\"outline\"\n [disabled]=\"working()\"\n (click)=\"regenerateBackupCodes()\"\n >\n @if (working()) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else { {{ t('mfaRegenerateCodes') }} }\n </ion-button>\n }\n </div>\n }\n\n <ion-button expand=\"block\" color=\"dark\" fill=\"outline\" (click)=\"goToDisable()\">\n {{ t('mfaDisableButton') }}\n </ion-button>\n } @else {\n <p class=\"mfa-status mfa-status--off\">{{ t('mfaDisabledLabel') }}</p>\n <p class=\"mfa-text\">{{ t('mfaDisabledHint') }}</p>\n <ion-button expand=\"block\" (click)=\"goToMethodSelect()\"> {{ t('mfaEnableButton') }} </ion-button>\n } } @case ('method-select') {\n <h2 class=\"mfa-title\">{{ t('mfaEnableTitle') }}</h2>\n <p class=\"mfa-text\">{{ t('mfaMethodPrompt') }}</p>\n\n <ion-radio-group [value]=\"selectedMethod()\" (ionChange)=\"selectedMethod.set($event.detail.value)\">\n <ion-item>\n <ion-radio slot=\"start\" value=\"TOTP\"></ion-radio>\n <ion-label>\n <strong>{{ t('mfaMethodTotp') }}</strong>\n <p>{{ t('mfaMethodTotpHint') }}</p>\n </ion-label>\n </ion-item>\n <ion-item>\n <ion-radio slot=\"start\" value=\"EMAIL\"></ion-radio>\n <ion-label>\n <strong>{{ t('mfaMethodEmail') }}</strong>\n <p>{{ t('mfaMethodEmailHint') }}</p>\n </ion-label>\n </ion-item>\n <ion-item>\n <ion-radio slot=\"start\" value=\"SMS\"></ion-radio>\n <ion-label>\n <strong>{{ t('mfaMethodSms') }}</strong>\n <p>{{ t('mfaMethodSmsHint') }}</p>\n </ion-label>\n </ion-item>\n </ion-radio-group>\n\n @if (selectedMethod() === 'SMS' && !userPhone()) {\n <ion-input\n label=\"{{ t('mfaPhoneLabel') }}\"\n labelPlacement=\"floating\"\n fill=\"outline\"\n type=\"tel\"\n placeholder=\"+56912345678\"\n [formControl]=\"phoneControl\"\n ></ion-input>\n @if (phoneControl.invalid && phoneControl.touched) {\n <p class=\"mfa-error\">{{ t('mfaPhoneInvalid') }}</p>\n } } @if (selectedMethod() === 'SMS' && userPhone(); as phone) {\n <p class=\"mfa-text\">{{ t('mfaPhoneRegistered') }}: {{ phone }}</p>\n }\n\n <ion-button expand=\"block\" [disabled]=\"working()\" (click)=\"proceedWithMethod()\">\n @if (working()) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else { {{ t('mfaContinue') }} }\n </ion-button>\n <ion-button expand=\"block\" fill=\"clear\" color=\"dark\" (click)=\"backToStatus()\">\n {{ t('mfaCancel') }}\n </ion-button>\n } @case ('totp-setup') {\n <h2 class=\"mfa-title\">{{ t('mfaTotpSetupTitle') }}</h2>\n <p class=\"mfa-text\">{{ t('mfaTotpStep1') }}</p>\n\n @if (totpQr(); as qr) {\n <div class=\"mfa-qr\">\n <val-qr-code [props]=\"{ qr: qr }\" />\n </div>\n } @if (totpSetup(); as setup) {\n <p class=\"mfa-text\">{{ t('mfaTotpManualEntry') }}</p>\n <code class=\"mfa-secret\">{{ setup.secret }}</code>\n\n <p class=\"mfa-text\">{{ t('mfaTotpStep2') }}</p>\n <div class=\"mfa-pin\">\n <val-pin-input [props]=\"pinInputProps\" />\n </div>\n\n <ion-button expand=\"block\" [disabled]=\"working()\" (click)=\"verifyTotp()\">\n @if (working()) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else { {{ t('mfaTotpVerify') }} }\n </ion-button>\n\n <div class=\"mfa-backup\">\n <h3>{{ t('mfaBackupCodesTitle') }}</h3>\n <div class=\"mfa-alert\">\n <ion-icon name=\"information-circle-outline\"></ion-icon>\n <p>{{ t('mfaBackupCodesSaveWarning') }} {{ t('mfaBackupCodesExplain') }}</p>\n </div>\n <div class=\"mfa-codes\">\n @for (code of setup.backupCodes; track code) {\n <code class=\"mfa-code\">{{ code }}</code>\n }\n </div>\n <ion-button expand=\"block\" color=\"dark\" fill=\"outline\" (click)=\"copyCodes(setup.backupCodes)\">\n {{ t('mfaCopyCodes') }}\n </ion-button>\n </div>\n }\n\n <ion-button expand=\"block\" fill=\"clear\" color=\"dark\" (click)=\"backToStatus()\">\n {{ t('mfaCancel') }}\n </ion-button>\n } @case ('code-confirm') {\n <h2 class=\"mfa-title\">{{ t('mfaConfirmTitle') }}</h2>\n <p class=\"mfa-text\">\n {{ selectedMethod() === 'EMAIL' ? t('mfaConfirmPromptEmail') : t('mfaConfirmPromptSms') }}\n </p>\n\n <div class=\"mfa-pin\">\n <val-pin-input [props]=\"pinInputProps\" />\n </div>\n\n <ion-button expand=\"block\" [disabled]=\"working()\" (click)=\"confirmCode()\">\n @if (working()) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else { {{ t('mfaConfirmButton') }} }\n </ion-button>\n\n <p class=\"mfa-resend\">\n {{ t('mfaNoCode') }} @if (resendCooldown() > 0) {\n <span class=\"mfa-text\">{{ t('mfaResendIn') }} {{ resendCooldown() }}s</span>\n } @else {\n <a (click)=\"resendCode()\">{{ t('mfaResend') }}</a>\n }\n </p>\n\n <ion-button expand=\"block\" fill=\"clear\" color=\"dark\" (click)=\"backToStatus()\">\n {{ t('mfaCancel') }}\n </ion-button>\n } @case ('disable') {\n <val-form [props]=\"disableFormProps()\" (onSubmit)=\"onDisableSubmit($event)\" />\n <ion-button expand=\"block\" fill=\"clear\" color=\"dark\" (click)=\"backToStatus()\">\n {{ t('mfaCancel') }}\n </ion-button>\n } }\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n", styles: [".mfa-modal{display:flex;flex-direction:column;gap:14px;padding-top:4px}.mfa-loading{display:flex;justify-content:center;padding:40px 0}.mfa-title{font-size:18px;font-weight:700;margin:0;color:var(--ion-color-dark)}.mfa-status{font-weight:600;margin:0}.mfa-status--on{color:var(--ion-color-success-shade)}.mfa-status--off{color:var(--ion-color-dark)}.mfa-text{color:var(--ion-color-dark);font-size:14px;margin:0}.mfa-error{color:var(--ion-color-danger);font-size:13px;margin:4px 0 0}.mfa-block,.mfa-backup{display:flex;flex-direction:column;gap:12px}.mfa-block{padding:16px;border-radius:14px;background:var(--ion-color-light)}.mfa-block h3,.mfa-backup h3{font-size:15px;font-weight:600;margin:0;color:var(--ion-color-dark)}.mfa-alert{display:flex;align-items:flex-start;gap:10px;padding:12px 14px;border-radius:10px;background:var(--ion-color-light);border-left:3px solid var(--ion-color-primary)}.mfa-alert ion-icon{font-size:20px;color:var(--ion-color-primary);flex-shrink:0;margin-top:1px}.mfa-alert p{margin:0;font-size:13px;line-height:1.45;color:var(--ion-color-dark)}.mfa-qr{display:flex;justify-content:center;padding:8px 0}.mfa-secret{display:block;padding:10px 12px;border-radius:8px;background:var(--ion-color-light);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:15px;letter-spacing:.04em;word-break:break-all;text-align:center}.mfa-pin{display:flex;justify-content:center;padding:8px 0}.mfa-codes{display:grid;grid-template-columns:repeat(2,1fr);gap:8px;padding:12px;border-radius:12px;background:var(--ion-color-light)}.mfa-code{padding:8px;border-radius:6px;background:var(--ion-background-color, #fff);color:var(--ion-color-dark);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:13px;text-align:center}.mfa-resend{text-align:center;font-size:14px;color:var(--ion-color-dark);margin:4px 0}.mfa-resend a{color:var(--ion-color-primary);cursor:pointer;font-weight:600}\n"] }]
|
|
30631
30704
|
}], ctorParameters: () => [], propDecorators: { isOpen: [{
|
|
30632
30705
|
type: Input
|
|
30706
|
+
}], prefillCode: [{
|
|
30707
|
+
type: Input
|
|
30633
30708
|
}], changed: [{
|
|
30634
30709
|
type: Output
|
|
30635
30710
|
}], dismissed: [{
|