valtech-components 2.0.834 → 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 +465 -0
- 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/services/i18n/default-content.mjs +109 -1
- package/esm2022/lib/version.mjs +2 -2
- package/esm2022/public-api.mjs +2 -1
- package/fesm2022/valtech-components.mjs +603 -18
- package/fesm2022/valtech-components.mjs.map +1 -1
- package/lib/components/organisms/mfa-modal/mfa-modal.component.d.ts +129 -0
- package/lib/services/auth/types.d.ts +9 -0
- package/lib/version.d.ts +1 -1
- package/package.json +1 -1
- package/public-api.d.ts +1 -0
|
@@ -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.
|
|
@@ -4352,6 +4352,60 @@ const VALTECH_DEFAULT_CONTENT = {
|
|
|
4352
4352
|
setPasswordConfirmCancel: 'Cancelar',
|
|
4353
4353
|
passwordSetSuccess: '¡Contraseña creada!',
|
|
4354
4354
|
errorPasswordAlreadySet: 'Esta cuenta ya tiene una contraseña.',
|
|
4355
|
+
// MFA — autenticación en dos pasos
|
|
4356
|
+
mfaManageTitle: 'Autenticación en dos pasos',
|
|
4357
|
+
mfaEnabledLabel: 'MFA habilitado',
|
|
4358
|
+
mfaDisabledLabel: 'MFA deshabilitado',
|
|
4359
|
+
mfaDisabledHint: 'Habilita MFA para mayor seguridad en tu cuenta.',
|
|
4360
|
+
mfaEnableButton: 'Habilitar MFA',
|
|
4361
|
+
mfaDisableButton: 'Deshabilitar MFA',
|
|
4362
|
+
mfaEnableTitle: 'Habilitar MFA',
|
|
4363
|
+
mfaMethodPrompt: 'Elige el método de verificación:',
|
|
4364
|
+
mfaMethodTotp: 'App de autenticación',
|
|
4365
|
+
mfaMethodTotpHint: 'Usa Google Authenticator u otra app (recomendado).',
|
|
4366
|
+
mfaMethodEmail: 'Correo electrónico',
|
|
4367
|
+
mfaMethodEmailHint: 'Recibe códigos en tu correo.',
|
|
4368
|
+
mfaMethodSms: 'SMS',
|
|
4369
|
+
mfaMethodSmsHint: 'Recibe códigos por mensaje de texto.',
|
|
4370
|
+
mfaPhoneLabel: 'Teléfono',
|
|
4371
|
+
mfaPhoneInvalid: 'Ingresa un número válido en formato E.164 (ej: +56912345678).',
|
|
4372
|
+
mfaPhoneRegistered: 'Teléfono registrado',
|
|
4373
|
+
mfaContinue: 'Continuar',
|
|
4374
|
+
mfaCancel: 'Cancelar',
|
|
4375
|
+
mfaTotpSetupTitle: 'Configurar app de autenticación',
|
|
4376
|
+
mfaTotpStep1: 'Paso 1 — Escanea el código QR con tu app de autenticación.',
|
|
4377
|
+
mfaTotpManualEntry: '¿No puedes escanear? Ingresa este código manualmente:',
|
|
4378
|
+
mfaTotpStep2: 'Paso 2 — Ingresa el código de 6 dígitos de tu app:',
|
|
4379
|
+
mfaTotpVerify: 'Verificar y activar',
|
|
4380
|
+
mfaConfirmTitle: 'Confirmar MFA',
|
|
4381
|
+
mfaConfirmPromptEmail: 'Ingresa el código de 6 dígitos enviado a tu correo.',
|
|
4382
|
+
mfaConfirmPromptSms: 'Ingresa el código de 6 dígitos enviado a tu teléfono.',
|
|
4383
|
+
mfaConfirmButton: 'Confirmar',
|
|
4384
|
+
mfaNoCode: '¿No recibiste el código?',
|
|
4385
|
+
mfaResend: 'Reenviar',
|
|
4386
|
+
mfaResendIn: 'Reenviar en',
|
|
4387
|
+
mfaBackupCodesTitle: 'Códigos de respaldo',
|
|
4388
|
+
mfaBackupCodesAvailable: 'Códigos disponibles',
|
|
4389
|
+
mfaBackupCodesLow: 'Te quedan pocos códigos. Considera regenerarlos.',
|
|
4390
|
+
mfaBackupCodesSaveWarning: 'Importante: guarda estos códigos de respaldo.',
|
|
4391
|
+
mfaBackupCodesExplain: 'Si pierdes acceso a tu app, estos códigos te permiten entrar. Cada uno se usa una sola vez.',
|
|
4392
|
+
mfaCopyCodes: 'Copiar códigos',
|
|
4393
|
+
mfaRegenerateCodes: 'Regenerar códigos de respaldo',
|
|
4394
|
+
mfaDisableTitle: 'Deshabilitar MFA',
|
|
4395
|
+
mfaDisablePrompt: 'Ingresa tu contraseña para deshabilitar MFA.',
|
|
4396
|
+
mfaPasswordLabel: 'Contraseña',
|
|
4397
|
+
mfaCodeInvalid: 'Ingresa un código válido de 6 dígitos.',
|
|
4398
|
+
mfaPasswordRequired: 'Ingresa tu contraseña.',
|
|
4399
|
+
mfaEnabledOk: '¡MFA habilitado correctamente!',
|
|
4400
|
+
mfaDisabledOk: 'MFA deshabilitado correctamente.',
|
|
4401
|
+
mfaCodesCopied: 'Códigos copiados al portapapeles.',
|
|
4402
|
+
mfaErrorInvalidCode: 'Código incorrecto.',
|
|
4403
|
+
mfaErrorExpiredCode: 'El código ha expirado. Solicita uno nuevo.',
|
|
4404
|
+
mfaErrorCodeUsed: 'Este código ya fue utilizado.',
|
|
4405
|
+
mfaErrorAlreadyActive: 'MFA ya está habilitado.',
|
|
4406
|
+
mfaErrorNotEnabled: 'MFA no está habilitado.',
|
|
4407
|
+
mfaErrorPhoneRequired: 'Número de teléfono requerido.',
|
|
4408
|
+
mfaErrorPhoneExists: 'Este teléfono ya está en uso.',
|
|
4355
4409
|
// Legal
|
|
4356
4410
|
legalPrefix: 'Utilizamos los servicios de',
|
|
4357
4411
|
legalSuffix: 'para ofrecerte una experiencia segura. Al iniciar sesión, aceptas nuestros',
|
|
@@ -4452,6 +4506,60 @@ const VALTECH_DEFAULT_CONTENT = {
|
|
|
4452
4506
|
setPasswordConfirmCancel: 'Cancel',
|
|
4453
4507
|
passwordSetSuccess: 'Password created!',
|
|
4454
4508
|
errorPasswordAlreadySet: 'This account already has a password.',
|
|
4509
|
+
// MFA — two-factor authentication
|
|
4510
|
+
mfaManageTitle: 'Two-factor authentication',
|
|
4511
|
+
mfaEnabledLabel: 'MFA enabled',
|
|
4512
|
+
mfaDisabledLabel: 'MFA disabled',
|
|
4513
|
+
mfaDisabledHint: 'Enable MFA for extra account security.',
|
|
4514
|
+
mfaEnableButton: 'Enable MFA',
|
|
4515
|
+
mfaDisableButton: 'Disable MFA',
|
|
4516
|
+
mfaEnableTitle: 'Enable MFA',
|
|
4517
|
+
mfaMethodPrompt: 'Choose your verification method:',
|
|
4518
|
+
mfaMethodTotp: 'Authenticator app',
|
|
4519
|
+
mfaMethodTotpHint: 'Use Google Authenticator or another app (recommended).',
|
|
4520
|
+
mfaMethodEmail: 'Email',
|
|
4521
|
+
mfaMethodEmailHint: 'Receive codes in your email.',
|
|
4522
|
+
mfaMethodSms: 'SMS',
|
|
4523
|
+
mfaMethodSmsHint: 'Receive codes by text message.',
|
|
4524
|
+
mfaPhoneLabel: 'Phone',
|
|
4525
|
+
mfaPhoneInvalid: 'Enter a valid number in E.164 format (e.g. +56912345678).',
|
|
4526
|
+
mfaPhoneRegistered: 'Registered phone',
|
|
4527
|
+
mfaContinue: 'Continue',
|
|
4528
|
+
mfaCancel: 'Cancel',
|
|
4529
|
+
mfaTotpSetupTitle: 'Set up authenticator app',
|
|
4530
|
+
mfaTotpStep1: 'Step 1 — Scan the QR code with your authenticator app.',
|
|
4531
|
+
mfaTotpManualEntry: "Can't scan? Enter this code manually:",
|
|
4532
|
+
mfaTotpStep2: 'Step 2 — Enter the 6-digit code from your app:',
|
|
4533
|
+
mfaTotpVerify: 'Verify and activate',
|
|
4534
|
+
mfaConfirmTitle: 'Confirm MFA',
|
|
4535
|
+
mfaConfirmPromptEmail: 'Enter the 6-digit code sent to your email.',
|
|
4536
|
+
mfaConfirmPromptSms: 'Enter the 6-digit code sent to your phone.',
|
|
4537
|
+
mfaConfirmButton: 'Confirm',
|
|
4538
|
+
mfaNoCode: "Didn't receive the code?",
|
|
4539
|
+
mfaResend: 'Resend',
|
|
4540
|
+
mfaResendIn: 'Resend in',
|
|
4541
|
+
mfaBackupCodesTitle: 'Backup codes',
|
|
4542
|
+
mfaBackupCodesAvailable: 'Available codes',
|
|
4543
|
+
mfaBackupCodesLow: 'You have few codes left. Consider regenerating them.',
|
|
4544
|
+
mfaBackupCodesSaveWarning: 'Important: save these backup codes.',
|
|
4545
|
+
mfaBackupCodesExplain: 'If you lose access to your app, these codes let you sign in. Each can be used once.',
|
|
4546
|
+
mfaCopyCodes: 'Copy codes',
|
|
4547
|
+
mfaRegenerateCodes: 'Regenerate backup codes',
|
|
4548
|
+
mfaDisableTitle: 'Disable MFA',
|
|
4549
|
+
mfaDisablePrompt: 'Enter your password to disable MFA.',
|
|
4550
|
+
mfaPasswordLabel: 'Password',
|
|
4551
|
+
mfaCodeInvalid: 'Enter a valid 6-digit code.',
|
|
4552
|
+
mfaPasswordRequired: 'Enter your password.',
|
|
4553
|
+
mfaEnabledOk: 'MFA enabled successfully!',
|
|
4554
|
+
mfaDisabledOk: 'MFA disabled successfully.',
|
|
4555
|
+
mfaCodesCopied: 'Codes copied to clipboard.',
|
|
4556
|
+
mfaErrorInvalidCode: 'Incorrect code.',
|
|
4557
|
+
mfaErrorExpiredCode: 'The code has expired. Request a new one.',
|
|
4558
|
+
mfaErrorCodeUsed: 'This code was already used.',
|
|
4559
|
+
mfaErrorAlreadyActive: 'MFA is already enabled.',
|
|
4560
|
+
mfaErrorNotEnabled: 'MFA is not enabled.',
|
|
4561
|
+
mfaErrorPhoneRequired: 'Phone number required.',
|
|
4562
|
+
mfaErrorPhoneExists: 'This phone is already in use.',
|
|
4455
4563
|
// Legal
|
|
4456
4564
|
legalPrefix: 'We use the services of',
|
|
4457
4565
|
legalSuffix: 'to offer you a secure experience. By signing in, you accept our',
|
|
@@ -24789,13 +24897,16 @@ class AuthService {
|
|
|
24789
24897
|
signinWithOAuth(provider) {
|
|
24790
24898
|
this.stateService.clearError();
|
|
24791
24899
|
return this.oauthService.startFlow(provider).pipe(tap(result => {
|
|
24792
|
-
//
|
|
24793
|
-
|
|
24794
|
-
|
|
24795
|
-
|
|
24796
|
-
|
|
24797
|
-
|
|
24798
|
-
|
|
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
|
+
}
|
|
24799
24910
|
// Convertir OAuthResult a SigninResponse compatible
|
|
24800
24911
|
const response = {
|
|
24801
24912
|
operationId: 'oauth',
|
|
@@ -24807,15 +24918,22 @@ class AuthService {
|
|
|
24807
24918
|
permissions: result.permissions,
|
|
24808
24919
|
};
|
|
24809
24920
|
this.handleSuccessfulAuth(response);
|
|
24810
|
-
}), map$1(result =>
|
|
24811
|
-
|
|
24812
|
-
|
|
24813
|
-
|
|
24814
|
-
|
|
24815
|
-
|
|
24816
|
-
|
|
24817
|
-
|
|
24818
|
-
|
|
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 => {
|
|
24819
24937
|
const authError = {
|
|
24820
24938
|
code: error.code || 'OAUTH_ERROR',
|
|
24821
24939
|
message: error.message || 'Error de autenticación OAuth',
|
|
@@ -28330,6 +28448,24 @@ class OAuthCallbackComponent {
|
|
|
28330
28448
|
this.closeAfterDelay();
|
|
28331
28449
|
return;
|
|
28332
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
|
+
}
|
|
28333
28469
|
// Extraer tokens
|
|
28334
28470
|
const accessToken = params.get('access_token');
|
|
28335
28471
|
const refreshToken = params.get('refresh_token');
|
|
@@ -30126,6 +30262,455 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
30126
30262
|
type: Output
|
|
30127
30263
|
}] } });
|
|
30128
30264
|
|
|
30265
|
+
/** Segundos de espera antes de poder reenviar el código EMAIL/SMS. */
|
|
30266
|
+
const RESEND_COOLDOWN_SECONDS = 30;
|
|
30267
|
+
/**
|
|
30268
|
+
* `val-mfa-modal` — modal de gestión de autenticación de dos factores (MFA)
|
|
30269
|
+
* para un usuario autenticado. Mismo patrón que `val-change-password-modal`.
|
|
30270
|
+
*
|
|
30271
|
+
* Flujo (máquina de estados interna):
|
|
30272
|
+
* - `loading` → `getProfile()` para conocer el estado MFA.
|
|
30273
|
+
* - `status` → muestra MFA habilitado/deshabilitado. Si está habilitado:
|
|
30274
|
+
* gestión de backup codes (TOTP) + deshabilitar. Si no: botón habilitar.
|
|
30275
|
+
* - `method-select` → elegir TOTP / EMAIL / SMS.
|
|
30276
|
+
* - `totp-setup` → QR + secreto manual + backup codes → verificar código.
|
|
30277
|
+
* - `code-confirm` → (EMAIL/SMS) ingresar código recibido, con reenvío.
|
|
30278
|
+
* - `disable` → contraseña para deshabilitar MFA.
|
|
30279
|
+
*
|
|
30280
|
+
* El QR se genera **client-side** (`QrGeneratorService`) — el secreto TOTP
|
|
30281
|
+
* nunca sale del navegador.
|
|
30282
|
+
*
|
|
30283
|
+
* Self-contained: inyecta `AuthService` y llama los endpoints directo. La app
|
|
30284
|
+
* controla `[isOpen]` y reacciona a `(changed)` / `(dismissed)`.
|
|
30285
|
+
*
|
|
30286
|
+
* i18n: namespace compartido `_auth`.
|
|
30287
|
+
*
|
|
30288
|
+
* @example
|
|
30289
|
+
* ```html
|
|
30290
|
+
* <val-mfa-modal
|
|
30291
|
+
* [isOpen]="isModalOpen()"
|
|
30292
|
+
* (dismissed)="isModalOpen.set(false)"
|
|
30293
|
+
* />
|
|
30294
|
+
* ```
|
|
30295
|
+
*/
|
|
30296
|
+
class MfaModalComponent {
|
|
30297
|
+
/** Controla la visibilidad. Cada apertura re-resuelve el estado MFA. */
|
|
30298
|
+
set isOpen(value) {
|
|
30299
|
+
const opening = value && !this._isOpen;
|
|
30300
|
+
this._isOpen = value;
|
|
30301
|
+
if (opening) {
|
|
30302
|
+
this.open();
|
|
30303
|
+
}
|
|
30304
|
+
}
|
|
30305
|
+
get isOpen() {
|
|
30306
|
+
return this._isOpen;
|
|
30307
|
+
}
|
|
30308
|
+
constructor() {
|
|
30309
|
+
this._isOpen = false;
|
|
30310
|
+
/** Emite cuando el estado MFA cambia (habilitado / deshabilitado). */
|
|
30311
|
+
this.changed = new EventEmitter();
|
|
30312
|
+
/** Emite cuando el user cierra el modal (botón X o backdrop). */
|
|
30313
|
+
this.dismissed = new EventEmitter();
|
|
30314
|
+
this.auth = inject(AuthService);
|
|
30315
|
+
this.toast = inject(ToastService);
|
|
30316
|
+
this.i18n = inject(I18nService);
|
|
30317
|
+
this.i18nHelper = inject(InputI18nHelper);
|
|
30318
|
+
this.qrGen = inject(QrGeneratorService);
|
|
30319
|
+
this._step = signal('loading');
|
|
30320
|
+
/** Paso actual del flujo. */
|
|
30321
|
+
this.step = this._step.asReadonly();
|
|
30322
|
+
/** `true` mientras una llamada al backend está en curso. */
|
|
30323
|
+
this.working = signal(false);
|
|
30324
|
+
// Estado MFA actual.
|
|
30325
|
+
this.mfaEnabled = signal(false);
|
|
30326
|
+
this.mfaMethod = signal(null);
|
|
30327
|
+
this.userPhone = signal(null);
|
|
30328
|
+
this.backupCodesCount = signal(0);
|
|
30329
|
+
// Estado del flujo de habilitación.
|
|
30330
|
+
this.selectedMethod = signal('TOTP');
|
|
30331
|
+
this.totpSetup = signal(null);
|
|
30332
|
+
this.totpQr = signal(null);
|
|
30333
|
+
/** Códigos de respaldo recién regenerados — se muestran una sola vez. */
|
|
30334
|
+
this.regeneratedCodes = signal(null);
|
|
30335
|
+
this.resendCooldown = signal(0);
|
|
30336
|
+
this.pinControl = new FormControl('', [Validators.required, Validators.minLength(6), Validators.maxLength(6)]);
|
|
30337
|
+
this.phoneControl = new FormControl('', [Validators.required, Validators.pattern(/^\+[1-9]\d{6,14}$/)]);
|
|
30338
|
+
this.pinInputProps = {
|
|
30339
|
+
control: this.pinControl,
|
|
30340
|
+
token: 'mfa-code',
|
|
30341
|
+
length: 6,
|
|
30342
|
+
allowNumbersOnly: true,
|
|
30343
|
+
autoFocus: true,
|
|
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
|
+
}));
|
|
30376
|
+
this.resendTimer = null;
|
|
30377
|
+
addIcons({ closeOutline, informationCircleOutline });
|
|
30378
|
+
}
|
|
30379
|
+
ngOnDestroy() {
|
|
30380
|
+
this.stopCooldown();
|
|
30381
|
+
}
|
|
30382
|
+
/** Traduce una clave del namespace `_auth`. */
|
|
30383
|
+
t(key) {
|
|
30384
|
+
return this.i18n.t(key, '_auth');
|
|
30385
|
+
}
|
|
30386
|
+
/** Cierre iniciado por el user (X / backdrop). */
|
|
30387
|
+
close() {
|
|
30388
|
+
this.dismissed.emit();
|
|
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
|
+
}
|
|
30405
|
+
// ===========================================================================
|
|
30406
|
+
// Carga de estado
|
|
30407
|
+
// ===========================================================================
|
|
30408
|
+
/** Consulta el perfil para conocer el estado MFA y posicionar el flujo. */
|
|
30409
|
+
resolveStatus() {
|
|
30410
|
+
this._step.set('loading');
|
|
30411
|
+
this.resetFlow();
|
|
30412
|
+
this.auth.getProfile().subscribe({
|
|
30413
|
+
next: profile => {
|
|
30414
|
+
this.mfaEnabled.set(profile.mfaEnabled);
|
|
30415
|
+
this.mfaMethod.set(profile.mfaMethod ?? null);
|
|
30416
|
+
this.userPhone.set(profile.phone ?? null);
|
|
30417
|
+
if (profile.mfaEnabled && profile.mfaMethod === 'TOTP') {
|
|
30418
|
+
this.loadBackupCount();
|
|
30419
|
+
}
|
|
30420
|
+
this._step.set('status');
|
|
30421
|
+
},
|
|
30422
|
+
error: () => {
|
|
30423
|
+
// Fallback: usar el signal de usuario en sesión.
|
|
30424
|
+
const user = this.auth.user();
|
|
30425
|
+
this.mfaEnabled.set(user?.mfaEnabled ?? false);
|
|
30426
|
+
this.mfaMethod.set(user?.mfaMethod ?? null);
|
|
30427
|
+
this._step.set('status');
|
|
30428
|
+
},
|
|
30429
|
+
});
|
|
30430
|
+
}
|
|
30431
|
+
loadBackupCount() {
|
|
30432
|
+
this.auth.getBackupCodesCount().subscribe({
|
|
30433
|
+
next: res => this.backupCodesCount.set(res.count),
|
|
30434
|
+
error: () => this.backupCodesCount.set(0),
|
|
30435
|
+
});
|
|
30436
|
+
}
|
|
30437
|
+
// ===========================================================================
|
|
30438
|
+
// Navegación entre pasos
|
|
30439
|
+
// ===========================================================================
|
|
30440
|
+
goToMethodSelect() {
|
|
30441
|
+
this.regeneratedCodes.set(null);
|
|
30442
|
+
this._step.set('method-select');
|
|
30443
|
+
}
|
|
30444
|
+
goToDisable() {
|
|
30445
|
+
this._step.set('disable');
|
|
30446
|
+
}
|
|
30447
|
+
backToStatus() {
|
|
30448
|
+
this.stopCooldown();
|
|
30449
|
+
this.resolveStatus();
|
|
30450
|
+
}
|
|
30451
|
+
// ===========================================================================
|
|
30452
|
+
// Habilitar MFA
|
|
30453
|
+
// ===========================================================================
|
|
30454
|
+
/** Continúa desde el selector de método al setup correspondiente. */
|
|
30455
|
+
proceedWithMethod() {
|
|
30456
|
+
const method = this.selectedMethod();
|
|
30457
|
+
if (method === 'TOTP') {
|
|
30458
|
+
this.setupTotp();
|
|
30459
|
+
return;
|
|
30460
|
+
}
|
|
30461
|
+
let phone;
|
|
30462
|
+
if (method === 'SMS' && !this.userPhone()) {
|
|
30463
|
+
if (this.phoneControl.invalid) {
|
|
30464
|
+
this.phoneControl.markAsTouched();
|
|
30465
|
+
this.showToast(this.t('mfaPhoneInvalid'));
|
|
30466
|
+
return;
|
|
30467
|
+
}
|
|
30468
|
+
phone = this.phoneControl.value ?? undefined;
|
|
30469
|
+
}
|
|
30470
|
+
this.working.set(true);
|
|
30471
|
+
this.auth.setupMFA(method, phone).subscribe({
|
|
30472
|
+
next: res => {
|
|
30473
|
+
this.working.set(false);
|
|
30474
|
+
if (res.codeSent) {
|
|
30475
|
+
this.pinControl.reset();
|
|
30476
|
+
this._step.set('code-confirm');
|
|
30477
|
+
this.startCooldown();
|
|
30478
|
+
}
|
|
30479
|
+
},
|
|
30480
|
+
error: err => {
|
|
30481
|
+
this.working.set(false);
|
|
30482
|
+
this.showToast(this.resolveError(err));
|
|
30483
|
+
},
|
|
30484
|
+
});
|
|
30485
|
+
}
|
|
30486
|
+
setupTotp() {
|
|
30487
|
+
this.working.set(true);
|
|
30488
|
+
this.auth.setupTOTP().subscribe({
|
|
30489
|
+
next: res => {
|
|
30490
|
+
this.totpSetup.set(res);
|
|
30491
|
+
this.pinControl.reset();
|
|
30492
|
+
this.qrGen
|
|
30493
|
+
.generate({ data: res.qrCodeUrl, width: 220 })
|
|
30494
|
+
.then(qr => this.totpQr.set(qr))
|
|
30495
|
+
.catch(() => this.totpQr.set(null));
|
|
30496
|
+
this.working.set(false);
|
|
30497
|
+
this._step.set('totp-setup');
|
|
30498
|
+
},
|
|
30499
|
+
error: err => {
|
|
30500
|
+
this.working.set(false);
|
|
30501
|
+
this.showToast(this.resolveError(err));
|
|
30502
|
+
},
|
|
30503
|
+
});
|
|
30504
|
+
}
|
|
30505
|
+
/** Verifica el código TOTP de la app de autenticación y activa MFA. */
|
|
30506
|
+
verifyTotp() {
|
|
30507
|
+
const code = this.pinControl.value;
|
|
30508
|
+
if (!code || code.length !== 6) {
|
|
30509
|
+
this.showToast(this.t('mfaCodeInvalid'));
|
|
30510
|
+
return;
|
|
30511
|
+
}
|
|
30512
|
+
this.working.set(true);
|
|
30513
|
+
this.auth.verifyTOTPSetup(code).subscribe({
|
|
30514
|
+
next: res => {
|
|
30515
|
+
this.working.set(false);
|
|
30516
|
+
if (res.enabled) {
|
|
30517
|
+
this.showToast(this.t('mfaEnabledOk'));
|
|
30518
|
+
this.changed.emit();
|
|
30519
|
+
this.resolveStatus();
|
|
30520
|
+
}
|
|
30521
|
+
},
|
|
30522
|
+
error: err => {
|
|
30523
|
+
this.working.set(false);
|
|
30524
|
+
this.showToast(this.resolveError(err));
|
|
30525
|
+
},
|
|
30526
|
+
});
|
|
30527
|
+
}
|
|
30528
|
+
/** Confirma el código EMAIL/SMS y activa MFA. */
|
|
30529
|
+
confirmCode() {
|
|
30530
|
+
const code = this.pinControl.value;
|
|
30531
|
+
if (!code || code.length !== 6) {
|
|
30532
|
+
this.showToast(this.t('mfaCodeInvalid'));
|
|
30533
|
+
return;
|
|
30534
|
+
}
|
|
30535
|
+
this.working.set(true);
|
|
30536
|
+
this.auth.confirmMFA(code).subscribe({
|
|
30537
|
+
next: res => {
|
|
30538
|
+
this.working.set(false);
|
|
30539
|
+
if (res.mfaEnabled) {
|
|
30540
|
+
this.showToast(this.t('mfaEnabledOk'));
|
|
30541
|
+
this.changed.emit();
|
|
30542
|
+
this.resolveStatus();
|
|
30543
|
+
}
|
|
30544
|
+
},
|
|
30545
|
+
error: err => {
|
|
30546
|
+
this.working.set(false);
|
|
30547
|
+
this.showToast(this.resolveError(err));
|
|
30548
|
+
},
|
|
30549
|
+
});
|
|
30550
|
+
}
|
|
30551
|
+
/** Reenvía el código EMAIL/SMS (re-ejecuta el setup). */
|
|
30552
|
+
resendCode() {
|
|
30553
|
+
if (this.resendCooldown() > 0) {
|
|
30554
|
+
return;
|
|
30555
|
+
}
|
|
30556
|
+
this.proceedWithMethod();
|
|
30557
|
+
}
|
|
30558
|
+
// ===========================================================================
|
|
30559
|
+
// Gestión (MFA habilitado)
|
|
30560
|
+
// ===========================================================================
|
|
30561
|
+
/** Regenera los códigos de respaldo TOTP y los muestra una vez. */
|
|
30562
|
+
regenerateBackupCodes() {
|
|
30563
|
+
this.working.set(true);
|
|
30564
|
+
this.auth.regenerateBackupCodes().subscribe({
|
|
30565
|
+
next: res => {
|
|
30566
|
+
this.working.set(false);
|
|
30567
|
+
this.backupCodesCount.set(res.backupCodes.length);
|
|
30568
|
+
this.regeneratedCodes.set(res.backupCodes);
|
|
30569
|
+
},
|
|
30570
|
+
error: err => {
|
|
30571
|
+
this.working.set(false);
|
|
30572
|
+
this.showToast(this.resolveError(err));
|
|
30573
|
+
},
|
|
30574
|
+
});
|
|
30575
|
+
}
|
|
30576
|
+
/** Deshabilita MFA — requiere la contraseña de la cuenta. */
|
|
30577
|
+
onDisableSubmit(event) {
|
|
30578
|
+
const password = event.fields['password'];
|
|
30579
|
+
if (!password) {
|
|
30580
|
+
this.showToast(this.t('mfaPasswordRequired'));
|
|
30581
|
+
return;
|
|
30582
|
+
}
|
|
30583
|
+
this.working.set(true);
|
|
30584
|
+
this.auth.disableMFA(password).subscribe({
|
|
30585
|
+
next: res => {
|
|
30586
|
+
this.working.set(false);
|
|
30587
|
+
if (res.mfaDisabled) {
|
|
30588
|
+
this.showToast(this.t('mfaDisabledOk'));
|
|
30589
|
+
this.changed.emit();
|
|
30590
|
+
this.resolveStatus();
|
|
30591
|
+
}
|
|
30592
|
+
},
|
|
30593
|
+
error: err => {
|
|
30594
|
+
this.working.set(false);
|
|
30595
|
+
this.showToast(this.resolveError(err));
|
|
30596
|
+
},
|
|
30597
|
+
});
|
|
30598
|
+
}
|
|
30599
|
+
/** Copia una lista de códigos de respaldo al portapapeles. */
|
|
30600
|
+
async copyCodes(codes) {
|
|
30601
|
+
try {
|
|
30602
|
+
await navigator.clipboard.writeText(codes.join('\n'));
|
|
30603
|
+
this.showToast(this.t('mfaCodesCopied'));
|
|
30604
|
+
}
|
|
30605
|
+
catch {
|
|
30606
|
+
/* sin recurso de copia disponible */
|
|
30607
|
+
}
|
|
30608
|
+
}
|
|
30609
|
+
/** Etiqueta i18n legible para un método MFA. */
|
|
30610
|
+
methodLabel(method) {
|
|
30611
|
+
switch (method) {
|
|
30612
|
+
case 'TOTP':
|
|
30613
|
+
return this.t('mfaMethodTotp');
|
|
30614
|
+
case 'EMAIL':
|
|
30615
|
+
return this.t('mfaMethodEmail');
|
|
30616
|
+
case 'SMS':
|
|
30617
|
+
return this.t('mfaMethodSms');
|
|
30618
|
+
default:
|
|
30619
|
+
return '';
|
|
30620
|
+
}
|
|
30621
|
+
}
|
|
30622
|
+
// ===========================================================================
|
|
30623
|
+
// Helpers
|
|
30624
|
+
// ===========================================================================
|
|
30625
|
+
resetFlow() {
|
|
30626
|
+
this.pinControl.reset();
|
|
30627
|
+
this.phoneControl.reset();
|
|
30628
|
+
this.totpSetup.set(null);
|
|
30629
|
+
this.totpQr.set(null);
|
|
30630
|
+
this.regeneratedCodes.set(null);
|
|
30631
|
+
this.selectedMethod.set('TOTP');
|
|
30632
|
+
this.stopCooldown();
|
|
30633
|
+
}
|
|
30634
|
+
startCooldown() {
|
|
30635
|
+
this.stopCooldown();
|
|
30636
|
+
this.resendCooldown.set(RESEND_COOLDOWN_SECONDS);
|
|
30637
|
+
this.resendTimer = setInterval(() => {
|
|
30638
|
+
this.resendCooldown.update(v => v - 1);
|
|
30639
|
+
if (this.resendCooldown() <= 0) {
|
|
30640
|
+
this.stopCooldown();
|
|
30641
|
+
}
|
|
30642
|
+
}, 1000);
|
|
30643
|
+
}
|
|
30644
|
+
stopCooldown() {
|
|
30645
|
+
if (this.resendTimer) {
|
|
30646
|
+
clearInterval(this.resendTimer);
|
|
30647
|
+
this.resendTimer = null;
|
|
30648
|
+
}
|
|
30649
|
+
this.resendCooldown.set(0);
|
|
30650
|
+
}
|
|
30651
|
+
/** Mapea los códigos de error MFA del backend a mensajes del namespace `_auth`. */
|
|
30652
|
+
resolveError(err) {
|
|
30653
|
+
const code = err?.code;
|
|
30654
|
+
switch (code) {
|
|
30655
|
+
case 'AUTHV2_MFA_INVALID_CODE':
|
|
30656
|
+
return this.t('mfaErrorInvalidCode');
|
|
30657
|
+
case 'AUTHV2_EXPIRED_CODE':
|
|
30658
|
+
return this.t('mfaErrorExpiredCode');
|
|
30659
|
+
case 'AUTHV2_CODE_USED':
|
|
30660
|
+
return this.t('mfaErrorCodeUsed');
|
|
30661
|
+
case 'AUTHV2_MFA_ALREADY_ACTIVE':
|
|
30662
|
+
return this.t('mfaErrorAlreadyActive');
|
|
30663
|
+
case 'AUTHV2_MFA_NOT_ENABLED':
|
|
30664
|
+
return this.t('mfaErrorNotEnabled');
|
|
30665
|
+
case 'AUTHV2_PHONE_REQUIRED':
|
|
30666
|
+
return this.t('mfaErrorPhoneRequired');
|
|
30667
|
+
case 'AUTHV2_PHONE_EXISTS':
|
|
30668
|
+
return this.t('mfaErrorPhoneExists');
|
|
30669
|
+
case 'AUTHV2_TOO_MANY_ATTEMPTS':
|
|
30670
|
+
return this.t('errorTooManyAttempts');
|
|
30671
|
+
case 'AUTHV2_INVALID_CURRENT_PASSWORD':
|
|
30672
|
+
return this.t('errorCurrentPasswordWrong');
|
|
30673
|
+
default:
|
|
30674
|
+
return this.t('errorGeneric');
|
|
30675
|
+
}
|
|
30676
|
+
}
|
|
30677
|
+
showToast(message) {
|
|
30678
|
+
this.toast.show({ message, duration: 3500 });
|
|
30679
|
+
}
|
|
30680
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: MfaModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
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"] }] }); }
|
|
30682
|
+
}
|
|
30683
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: MfaModalComponent, decorators: [{
|
|
30684
|
+
type: Component,
|
|
30685
|
+
args: [{ selector: 'val-mfa-modal', standalone: true, imports: [
|
|
30686
|
+
ReactiveFormsModule,
|
|
30687
|
+
IonButton,
|
|
30688
|
+
IonButtons,
|
|
30689
|
+
IonContent,
|
|
30690
|
+
IonHeader,
|
|
30691
|
+
IonIcon,
|
|
30692
|
+
IonInput,
|
|
30693
|
+
IonItem,
|
|
30694
|
+
IonLabel,
|
|
30695
|
+
IonModal,
|
|
30696
|
+
IonRadio,
|
|
30697
|
+
IonRadioGroup,
|
|
30698
|
+
IonSpinner,
|
|
30699
|
+
IonToolbar,
|
|
30700
|
+
FormComponent,
|
|
30701
|
+
QrCodeComponent,
|
|
30702
|
+
PinInputComponent,
|
|
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"] }]
|
|
30704
|
+
}], ctorParameters: () => [], propDecorators: { isOpen: [{
|
|
30705
|
+
type: Input
|
|
30706
|
+
}], prefillCode: [{
|
|
30707
|
+
type: Input
|
|
30708
|
+
}], changed: [{
|
|
30709
|
+
type: Output
|
|
30710
|
+
}], dismissed: [{
|
|
30711
|
+
type: Output
|
|
30712
|
+
}] } });
|
|
30713
|
+
|
|
30129
30714
|
/**
|
|
30130
30715
|
* ItemListComponent
|
|
30131
30716
|
*
|
|
@@ -46562,5 +47147,5 @@ function buildFooterLinks(links, t, resolver) {
|
|
|
46562
47147
|
* Generated bundle index. Do not edit.
|
|
46563
47148
|
*/
|
|
46564
47149
|
|
|
46565
|
-
export { ACTION_CARD_DEFAULTS, AD_SIZE_MAP, API_TABLE_COLUMN_LABELS, ARTICLE_SPACING, AVATAR_UPLOAD_DEFAULTS, AccordionComponent, ActionCardComponent, ActionHeaderComponent, ActionType, AdSlotComponent, AdsLoaderService, AdsService, AlertBoxComponent, AnalyticsErrorHandler, AnalyticsRouterTracker, AnalyticsService, AppConfigService, AppVersionService, ArticleBuilder, ArticleComponent, AuthBackgroundComponent, AuthService, AuthStateService, AuthStorageService, AuthSyncService, AvatarComponent, AvatarUploadComponent, BOTTOM_NAV_DEFAULTS, BannerComponent, BaseDefault, BlogPostBuilder, BottomNavComponent, BoxComponent, BreadcrumbComponent, ButtonComponent, ButtonGroupComponent, CALLOUT_LABELS, CHEV_KEYS, COMMON_COUNTRY_CODES, COMMON_CURRENCIES, CURRENCY_INFO, CardComponent, CardSection, CardType, CardsCarouselComponent, ChangePasswordModalComponent, CheckInputComponent, CheckboxRadioInputComponent, ChipGroupComponent, ClearDefault, ClearDefaultBlock, ClearDefaultFull, ClearDefaultRound, ClearDefaultRoundBlock, ClearDefaultRoundFull, CodeDisplayComponent, CommandDisplayComponent, CommentComponent, CommentInputComponent, CommentSectionComponent, CompanyFooterComponent, ComponentStates, ConfirmationDialogService, ContainerComponent, ContentLoaderComponent, ContentReactionComponent, ContentTransformer, CookieBannerComponent, CountdownComponent, CurrencyInputComponent, DEFAULT_ADS_CONFIG, DEFAULT_APP_CONFIG_SERVICE_CONFIG, DEFAULT_APP_VERSION_SERVICE_CONFIG, DEFAULT_AUTH_CONFIG, DEFAULT_BACK_HEADER, DEFAULT_CANCEL_BUTTON, DEFAULT_CHECK_INTERVAL_MS, DEFAULT_CONFIRM_BUTTON, DEFAULT_COUNTDOWN_LABELS, DEFAULT_COUNTDOWN_LABELS_EN, DEFAULT_DEBUG_CONSOLE_CONFIG, DEFAULT_DONATION_CONFIG, DEFAULT_EMPTY_STATE, DEFAULT_EMULATOR_CONFIG, DEFAULT_FEEDBACK_CONFIG, DEFAULT_FEEDBACK_TYPE_OPTIONS, DEFAULT_HOME_HEADER, DEFAULT_INFINITE_LIST_METADATA, DEFAULT_MODAL_CANCEL_BUTTON, DEFAULT_MODAL_CONFIRM_BUTTON, DEFAULT_PAGE_SIZE_OPTIONS, DEFAULT_PLATFORMS, DEFAULT_REFRESHER_METADATA, DEFAULT_SKELETON_CONFIG, DataTableComponent, DateInputComponent, DateRangeInputComponent, DebugConsoleComponent, DetailSkeletonComponent, DeviceService, DisplayComponent, DividerComponent, DocsApiTableComponent, DocsBreadcrumbComponent, DocsBuilder, DocsCalloutComponent, DocsCodeExampleComponent, DocsLayoutComponent, DocsNavLinksComponent, DocsNavigationService, DocsPageComponent, DocsSearchComponent, DocsSectionComponent, DocsShellComponent, DocsSidebarComponent, DocsTocComponent, DonationService, DownloadService, EmailInputComponent, ExpandableTextComponent, FEATURES_LIST_DEFAULTS, FabComponent, FaqComponent, FeaturesListComponent, FeedbackFormComponent, FeedbackService, FileInputComponent, FirebaseService, FirestoreCollectionFactory, FirestoreService, FooterComponent, FooterLinksComponent, FormComponent, FormFooterComponent, FormSkeletonComponent, FunHeaderComponent, GlassComponent, GlowCardComponent, GlowComponent, GridSkeletonComponent, HANDOFF_ROUTE_PARAM, HANDOFF_TOKEN_PARAM, HandoffService, HeaderComponent, HintComponent, HorizontalScrollComponent, HourInputComponent, HrefComponent, I18nService, IMAGE_DEFAULTS, INITIAL_AUTH_STATE, INITIAL_MFA_STATE, Icon, IconComponent, IconService, ImageComponent, ImageCropComponent, ImageService, InAppBrowserService, InfiniteListComponent, InfoComponent, InputI18nHelper, InputType, ItemListComponent, LANG_STORAGE_KEY$1 as LANG_STORAGE_KEY, LEGAL_CONTENT_CONFIG, LOGIN_DEFAULTS, LanguageSelectorComponent, LayeredCardComponent, LegalContentService, LegalLinkService, LinkComponent, LinkProcessorService, LinkedProvidersComponent, LinksAccordionComponent, LinksCakeComponent, ListSkeletonComponent, LoadingDirective, LocalStorageService, LocaleService, LoginComponent, MODAL_SIZES, MOTIF_KEYS, MOTION, MaintenancePageComponent, MarkdownArticleParserService, MenuComponent, MessagingService, MetaService, ModalService, MultiSelectSearchComponent, NavigationService, NewsBuilder, NoContentComponent, NotesBoxComponent, NotificationActionService, NotificationsService, NumberFromToComponent, NumberInputComponent, NumberStepperComponent, OAUTH_PROVIDERS_INFO, OAuthCallbackComponent, OAuthService, OrgSwitchService, OutlineDefault, OutlineDefaultBlock, OutlineDefaultFull, OutlineDefaultRound, OutlineDefaultRoundBlock, OutlineDefaultRoundFull, PATTERN_MOTIFS, PATTERN_PALETTES, PLATFORM_CONFIGS, PageContentComponent, PageLinksComponent, PageRefreshService, PageTemplateComponent, PageWrapperComponent, PaginationComponent, PaginationService, PasswordInputComponent, PatternComponent, PhoneInputComponent, PillComponent, PinInputComponent, PlainCodeBoxComponent, PopoverSelectorComponent, PreferencesService, PresetService, PriceTagComponent, PrimarySolidBlockButton, PrimarySolidBlockHrefButton, PrimarySolidBlockIconButton, PrimarySolidBlockIconHrefButton, PrimarySolidDefaultRoundButton, PrimarySolidDefaultRoundHrefButton, PrimarySolidDefaultRoundIconButton, PrimarySolidDefaultRoundIconHrefButton, PrimarySolidFullButton, PrimarySolidFullHrefButton, PrimarySolidFullIconButton, PrimarySolidFullIconHrefButton, PrimarySolidLargeRoundButton, PrimarySolidLargeRoundHrefButton, PrimarySolidLargeRoundIconButton, PrimarySolidLargeRoundIconHrefButton, PrimarySolidSmallRoundButton, PrimarySolidSmallRoundHrefButton, PrimarySolidSmallRoundIconButton, PrimarySolidSmallRoundIconHrefButton, ProcessLinksPipe, ProfileSkeletonComponent, ProgressBarComponent, ProgressRingComponent, ProgressStatusComponent, PrompterComponent, QR_PRESETS, QrCodeComponent, QrGeneratorService, QueryBuilder, QuoteBoxComponent, RadioInputComponent, RangeInputComponent, RatingComponent, RefresherComponent, RightsFooterComponent, RotatingTextComponent, SHAPE_KEYS, SKELETON_LAYOUT_DEFAULT_ROWS, SKELETON_PRESETS, SOLID_KEYS, SearchSelectorComponent, SearchbarComponent, SecondarySolidBlockButton, SecondarySolidBlockHrefButton, SecondarySolidBlockIconButton, SecondarySolidBlockIconHrefButton, SecondarySolidDefaultRoundButton, SecondarySolidDefaultRoundHrefButton, SecondarySolidDefaultRoundIconButton, SecondarySolidDefaultRoundIconHrefButton, SecondarySolidFullButton, SecondarySolidFullHrefButton, SecondarySolidFullIconButton, SecondarySolidFullIconHrefButton, SecondarySolidLargeRoundButton, SecondarySolidLargeRoundHrefButton, SecondarySolidLargeRoundIconButton, SecondarySolidLargeRoundIconHrefButton, SecondarySolidSmallRoundButton, SecondarySolidSmallRoundHrefButton, SecondarySolidSmallRoundIconButton, SecondarySolidSmallRoundIconHrefButton, SegmentControlComponent, SelectSearchComponent, SessionService, ShareButtonsComponent, SimpleComponent, SkeletonComponent, SkeletonLayoutComponent, SkeletonService, SolidBlockButton, SolidDefault, SolidDefaultBlock, SolidDefaultButton, SolidDefaultFull, SolidDefaultRound, SolidDefaultRoundBlock, SolidDefaultRoundButton, SolidDefaultRoundFull, SolidFullButton, SolidLargeButton, SolidLargeRoundButton, SolidSmallButton, SolidSmallRoundButton, StatsCardComponent, StepperComponent, StorageService, SwipeCarouselComponent, TRI_KEYS, TabbedContentComponent, TableSkeletonComponent, TabsComponent, Terminal404Component, TestimonialCardComponent, TestimonialCarouselComponent, TextComponent, TextInputComponent, TextareaInputComponent, ThemeOption, ThemeService, TimelineComponent, TitleBlockComponent, TitleComponent, ToastService, ToggleInputComponent, TokenService, ToolbarActionType, ToolbarComponent, TranslatePipe, TypedCollection, UPDATE_BANNER_DEFAULT_CONTENT, UPDATE_BANNER_I18N_NAMESPACE, UpdateBannerComponent, UserAvatarComponent, UsernameInputComponent, VALTECH_ADS_CONFIG, VALTECH_APP_CONFIG, VALTECH_APP_VERSION, VALTECH_AUTH_CONFIG, VALTECH_COMPANY_LINKS, VALTECH_DEBUG_CONSOLE, VALTECH_DEFAULT_CONTENT, VALTECH_DONATION_CONFIG, VALTECH_FEEDBACK_CONFIG, VALTECH_FIREBASE_CONFIG, VALTECH_FOOTER_I18N, VALTECH_FOOTER_LOGO, VALTECH_LANGUAGE_SELECTOR, VALTECH_LEGAL_CONFIG, VALTECH_SOCIAL_LINKS, VERSION, WizardComponent, WizardFooterComponent, applyDefaultValueToControl, authGuard, authInterceptor, blogPost, buildFooterLinks, buildPath, collections, connectPageRefresh, createFirebaseConfig, createGlowCardProps, createInitialPaginationState, createNumberFromToField, createRefreshableStream, createTitleProps, docs, extractPathParams, generatePatternTiles, generateRandomTile, getAppInfo, getAppVersion, getCollectionPath, getDocumentId, getTimeOfDayKey, goToTop, guestGuard, hasEmulators, isAtEnd, isCollectionPath, isDocumentPath, isEmulatorMode, isValidPath, joinPath, maxLength, mulberry32, news, parseMarkdownArticle, permissionGuard, permissionGuardFromRoute, provideLegalContent, provideValtechAds, provideValtechAppConfig, provideValtechAppVersion, provideValtechAuth, provideValtechAuthInterceptor, provideValtechDebugConsole, provideValtechDonations, provideValtechFeedback, provideValtechFirebase, provideValtechI18n, provideValtechLegal, provideValtechPresets, provideValtechSkeleton, query, renderPatternSvgInner, replaceSpecialChars, resolveColor, resolveInputDefaultValue, roleGuard, storagePaths, superAdminGuard, toArticle };
|
|
47150
|
+
export { ACTION_CARD_DEFAULTS, AD_SIZE_MAP, API_TABLE_COLUMN_LABELS, ARTICLE_SPACING, AVATAR_UPLOAD_DEFAULTS, AccordionComponent, ActionCardComponent, ActionHeaderComponent, ActionType, AdSlotComponent, AdsLoaderService, AdsService, AlertBoxComponent, AnalyticsErrorHandler, AnalyticsRouterTracker, AnalyticsService, AppConfigService, AppVersionService, ArticleBuilder, ArticleComponent, AuthBackgroundComponent, AuthService, AuthStateService, AuthStorageService, AuthSyncService, AvatarComponent, AvatarUploadComponent, BOTTOM_NAV_DEFAULTS, BannerComponent, BaseDefault, BlogPostBuilder, BottomNavComponent, BoxComponent, BreadcrumbComponent, ButtonComponent, ButtonGroupComponent, CALLOUT_LABELS, CHEV_KEYS, COMMON_COUNTRY_CODES, COMMON_CURRENCIES, CURRENCY_INFO, CardComponent, CardSection, CardType, CardsCarouselComponent, ChangePasswordModalComponent, CheckInputComponent, CheckboxRadioInputComponent, ChipGroupComponent, ClearDefault, ClearDefaultBlock, ClearDefaultFull, ClearDefaultRound, ClearDefaultRoundBlock, ClearDefaultRoundFull, CodeDisplayComponent, CommandDisplayComponent, CommentComponent, CommentInputComponent, CommentSectionComponent, CompanyFooterComponent, ComponentStates, ConfirmationDialogService, ContainerComponent, ContentLoaderComponent, ContentReactionComponent, ContentTransformer, CookieBannerComponent, CountdownComponent, CurrencyInputComponent, DEFAULT_ADS_CONFIG, DEFAULT_APP_CONFIG_SERVICE_CONFIG, DEFAULT_APP_VERSION_SERVICE_CONFIG, DEFAULT_AUTH_CONFIG, DEFAULT_BACK_HEADER, DEFAULT_CANCEL_BUTTON, DEFAULT_CHECK_INTERVAL_MS, DEFAULT_CONFIRM_BUTTON, DEFAULT_COUNTDOWN_LABELS, DEFAULT_COUNTDOWN_LABELS_EN, DEFAULT_DEBUG_CONSOLE_CONFIG, DEFAULT_DONATION_CONFIG, DEFAULT_EMPTY_STATE, DEFAULT_EMULATOR_CONFIG, DEFAULT_FEEDBACK_CONFIG, DEFAULT_FEEDBACK_TYPE_OPTIONS, DEFAULT_HOME_HEADER, DEFAULT_INFINITE_LIST_METADATA, DEFAULT_MODAL_CANCEL_BUTTON, DEFAULT_MODAL_CONFIRM_BUTTON, DEFAULT_PAGE_SIZE_OPTIONS, DEFAULT_PLATFORMS, DEFAULT_REFRESHER_METADATA, DEFAULT_SKELETON_CONFIG, DataTableComponent, DateInputComponent, DateRangeInputComponent, DebugConsoleComponent, DetailSkeletonComponent, DeviceService, DisplayComponent, DividerComponent, DocsApiTableComponent, DocsBreadcrumbComponent, DocsBuilder, DocsCalloutComponent, DocsCodeExampleComponent, DocsLayoutComponent, DocsNavLinksComponent, DocsNavigationService, DocsPageComponent, DocsSearchComponent, DocsSectionComponent, DocsShellComponent, DocsSidebarComponent, DocsTocComponent, DonationService, DownloadService, EmailInputComponent, ExpandableTextComponent, FEATURES_LIST_DEFAULTS, FabComponent, FaqComponent, FeaturesListComponent, FeedbackFormComponent, FeedbackService, FileInputComponent, FirebaseService, FirestoreCollectionFactory, FirestoreService, FooterComponent, FooterLinksComponent, FormComponent, FormFooterComponent, FormSkeletonComponent, FunHeaderComponent, GlassComponent, GlowCardComponent, GlowComponent, GridSkeletonComponent, HANDOFF_ROUTE_PARAM, HANDOFF_TOKEN_PARAM, HandoffService, HeaderComponent, HintComponent, HorizontalScrollComponent, HourInputComponent, HrefComponent, I18nService, IMAGE_DEFAULTS, INITIAL_AUTH_STATE, INITIAL_MFA_STATE, Icon, IconComponent, IconService, ImageComponent, ImageCropComponent, ImageService, InAppBrowserService, InfiniteListComponent, InfoComponent, InputI18nHelper, InputType, ItemListComponent, LANG_STORAGE_KEY$1 as LANG_STORAGE_KEY, LEGAL_CONTENT_CONFIG, LOGIN_DEFAULTS, LanguageSelectorComponent, LayeredCardComponent, LegalContentService, LegalLinkService, LinkComponent, LinkProcessorService, LinkedProvidersComponent, LinksAccordionComponent, LinksCakeComponent, ListSkeletonComponent, LoadingDirective, LocalStorageService, LocaleService, LoginComponent, MODAL_SIZES, MOTIF_KEYS, MOTION, MaintenancePageComponent, MarkdownArticleParserService, MenuComponent, MessagingService, MetaService, MfaModalComponent, ModalService, MultiSelectSearchComponent, NavigationService, NewsBuilder, NoContentComponent, NotesBoxComponent, NotificationActionService, NotificationsService, NumberFromToComponent, NumberInputComponent, NumberStepperComponent, OAUTH_PROVIDERS_INFO, OAuthCallbackComponent, OAuthService, OrgSwitchService, OutlineDefault, OutlineDefaultBlock, OutlineDefaultFull, OutlineDefaultRound, OutlineDefaultRoundBlock, OutlineDefaultRoundFull, PATTERN_MOTIFS, PATTERN_PALETTES, PLATFORM_CONFIGS, PageContentComponent, PageLinksComponent, PageRefreshService, PageTemplateComponent, PageWrapperComponent, PaginationComponent, PaginationService, PasswordInputComponent, PatternComponent, PhoneInputComponent, PillComponent, PinInputComponent, PlainCodeBoxComponent, PopoverSelectorComponent, PreferencesService, PresetService, PriceTagComponent, PrimarySolidBlockButton, PrimarySolidBlockHrefButton, PrimarySolidBlockIconButton, PrimarySolidBlockIconHrefButton, PrimarySolidDefaultRoundButton, PrimarySolidDefaultRoundHrefButton, PrimarySolidDefaultRoundIconButton, PrimarySolidDefaultRoundIconHrefButton, PrimarySolidFullButton, PrimarySolidFullHrefButton, PrimarySolidFullIconButton, PrimarySolidFullIconHrefButton, PrimarySolidLargeRoundButton, PrimarySolidLargeRoundHrefButton, PrimarySolidLargeRoundIconButton, PrimarySolidLargeRoundIconHrefButton, PrimarySolidSmallRoundButton, PrimarySolidSmallRoundHrefButton, PrimarySolidSmallRoundIconButton, PrimarySolidSmallRoundIconHrefButton, ProcessLinksPipe, ProfileSkeletonComponent, ProgressBarComponent, ProgressRingComponent, ProgressStatusComponent, PrompterComponent, QR_PRESETS, QrCodeComponent, QrGeneratorService, QueryBuilder, QuoteBoxComponent, RadioInputComponent, RangeInputComponent, RatingComponent, RefresherComponent, RightsFooterComponent, RotatingTextComponent, SHAPE_KEYS, SKELETON_LAYOUT_DEFAULT_ROWS, SKELETON_PRESETS, SOLID_KEYS, SearchSelectorComponent, SearchbarComponent, SecondarySolidBlockButton, SecondarySolidBlockHrefButton, SecondarySolidBlockIconButton, SecondarySolidBlockIconHrefButton, SecondarySolidDefaultRoundButton, SecondarySolidDefaultRoundHrefButton, SecondarySolidDefaultRoundIconButton, SecondarySolidDefaultRoundIconHrefButton, SecondarySolidFullButton, SecondarySolidFullHrefButton, SecondarySolidFullIconButton, SecondarySolidFullIconHrefButton, SecondarySolidLargeRoundButton, SecondarySolidLargeRoundHrefButton, SecondarySolidLargeRoundIconButton, SecondarySolidLargeRoundIconHrefButton, SecondarySolidSmallRoundButton, SecondarySolidSmallRoundHrefButton, SecondarySolidSmallRoundIconButton, SecondarySolidSmallRoundIconHrefButton, SegmentControlComponent, SelectSearchComponent, SessionService, ShareButtonsComponent, SimpleComponent, SkeletonComponent, SkeletonLayoutComponent, SkeletonService, SolidBlockButton, SolidDefault, SolidDefaultBlock, SolidDefaultButton, SolidDefaultFull, SolidDefaultRound, SolidDefaultRoundBlock, SolidDefaultRoundButton, SolidDefaultRoundFull, SolidFullButton, SolidLargeButton, SolidLargeRoundButton, SolidSmallButton, SolidSmallRoundButton, StatsCardComponent, StepperComponent, StorageService, SwipeCarouselComponent, TRI_KEYS, TabbedContentComponent, TableSkeletonComponent, TabsComponent, Terminal404Component, TestimonialCardComponent, TestimonialCarouselComponent, TextComponent, TextInputComponent, TextareaInputComponent, ThemeOption, ThemeService, TimelineComponent, TitleBlockComponent, TitleComponent, ToastService, ToggleInputComponent, TokenService, ToolbarActionType, ToolbarComponent, TranslatePipe, TypedCollection, UPDATE_BANNER_DEFAULT_CONTENT, UPDATE_BANNER_I18N_NAMESPACE, UpdateBannerComponent, UserAvatarComponent, UsernameInputComponent, VALTECH_ADS_CONFIG, VALTECH_APP_CONFIG, VALTECH_APP_VERSION, VALTECH_AUTH_CONFIG, VALTECH_COMPANY_LINKS, VALTECH_DEBUG_CONSOLE, VALTECH_DEFAULT_CONTENT, VALTECH_DONATION_CONFIG, VALTECH_FEEDBACK_CONFIG, VALTECH_FIREBASE_CONFIG, VALTECH_FOOTER_I18N, VALTECH_FOOTER_LOGO, VALTECH_LANGUAGE_SELECTOR, VALTECH_LEGAL_CONFIG, VALTECH_SOCIAL_LINKS, VERSION, WizardComponent, WizardFooterComponent, applyDefaultValueToControl, authGuard, authInterceptor, blogPost, buildFooterLinks, buildPath, collections, connectPageRefresh, createFirebaseConfig, createGlowCardProps, createInitialPaginationState, createNumberFromToField, createRefreshableStream, createTitleProps, docs, extractPathParams, generatePatternTiles, generateRandomTile, getAppInfo, getAppVersion, getCollectionPath, getDocumentId, getTimeOfDayKey, goToTop, guestGuard, hasEmulators, isAtEnd, isCollectionPath, isDocumentPath, isEmulatorMode, isValidPath, joinPath, maxLength, mulberry32, news, parseMarkdownArticle, permissionGuard, permissionGuardFromRoute, provideLegalContent, provideValtechAds, provideValtechAppConfig, provideValtechAppVersion, provideValtechAuth, provideValtechAuthInterceptor, provideValtechDebugConsole, provideValtechDonations, provideValtechFeedback, provideValtechFirebase, provideValtechI18n, provideValtechLegal, provideValtechPresets, provideValtechSkeleton, query, renderPatternSvgInner, replaceSpecialChars, resolveColor, resolveInputDefaultValue, roleGuard, storagePaths, superAdminGuard, toArticle };
|
|
46566
47151
|
//# sourceMappingURL=valtech-components.mjs.map
|