valtech-components 2.0.833 → 2.0.835
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/change-password-modal/change-password-modal.component.mjs +156 -58
- package/esm2022/lib/components/organisms/mfa-modal/mfa-modal.component.mjs +415 -0
- package/esm2022/lib/services/auth/interceptor.mjs +10 -6
- package/esm2022/lib/services/i18n/default-content.mjs +129 -1
- package/esm2022/lib/version.mjs +2 -2
- package/esm2022/public-api.mjs +2 -1
- package/fesm2022/valtech-components.mjs +694 -63
- package/fesm2022/valtech-components.mjs.map +1 -1
- package/lib/components/organisms/change-password-modal/change-password-modal.component.d.ts +38 -12
- package/lib/components/organisms/mfa-modal/mfa-modal.component.d.ts +114 -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.835';
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
59
|
* Servicio para gestionar presets de componentes.
|
|
@@ -4342,6 +4342,70 @@ const VALTECH_DEFAULT_CONTENT = {
|
|
|
4342
4342
|
passwordChangedSuccess: '¡Contraseña actualizada!',
|
|
4343
4343
|
errorCurrentPasswordWrong: 'La contraseña actual es incorrecta',
|
|
4344
4344
|
errorSamePassword: 'La nueva contraseña debe ser diferente a la actual',
|
|
4345
|
+
// Set password (cuenta OAuth-only)
|
|
4346
|
+
setPasswordTitle: 'Crear contraseña',
|
|
4347
|
+
setPasswordDescription: 'Tu cuenta usa inicio de sesión social. Crea una contraseña para entrar también con tu correo.',
|
|
4348
|
+
setPasswordSubmit: 'Crear contraseña',
|
|
4349
|
+
setPasswordConfirmTitle: '¿Crear contraseña?',
|
|
4350
|
+
setPasswordConfirmMessage: 'Podrás iniciar sesión con tu correo y contraseña, además de tu cuenta social. No perderás el acceso social.',
|
|
4351
|
+
setPasswordConfirmOk: 'Crear contraseña',
|
|
4352
|
+
setPasswordConfirmCancel: 'Cancelar',
|
|
4353
|
+
passwordSetSuccess: '¡Contraseña creada!',
|
|
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.',
|
|
4345
4409
|
// Legal
|
|
4346
4410
|
legalPrefix: 'Utilizamos los servicios de',
|
|
4347
4411
|
legalSuffix: 'para ofrecerte una experiencia segura. Al iniciar sesión, aceptas nuestros',
|
|
@@ -4432,6 +4496,70 @@ const VALTECH_DEFAULT_CONTENT = {
|
|
|
4432
4496
|
passwordChangedSuccess: 'Password updated!',
|
|
4433
4497
|
errorCurrentPasswordWrong: 'Current password is incorrect',
|
|
4434
4498
|
errorSamePassword: 'New password must be different from the current one',
|
|
4499
|
+
// Set password (OAuth-only account)
|
|
4500
|
+
setPasswordTitle: 'Create password',
|
|
4501
|
+
setPasswordDescription: 'Your account uses social sign-in. Create a password to also sign in with your email.',
|
|
4502
|
+
setPasswordSubmit: 'Create password',
|
|
4503
|
+
setPasswordConfirmTitle: 'Create password?',
|
|
4504
|
+
setPasswordConfirmMessage: 'You will be able to sign in with your email and password, in addition to your social account. You will not lose social access.',
|
|
4505
|
+
setPasswordConfirmOk: 'Create password',
|
|
4506
|
+
setPasswordConfirmCancel: 'Cancel',
|
|
4507
|
+
passwordSetSuccess: 'Password created!',
|
|
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.',
|
|
4435
4563
|
// Legal
|
|
4436
4564
|
legalPrefix: 'We use the services of',
|
|
4437
4565
|
legalSuffix: 'to offer you a secure experience. By signing in, you accept our',
|
|
@@ -20518,7 +20646,7 @@ function isPublicEndpoint(request, authPrefix) {
|
|
|
20518
20646
|
'/logout',
|
|
20519
20647
|
'/mfa/verify', // Solo durante login
|
|
20520
20648
|
];
|
|
20521
|
-
return publicEndpoints.some(
|
|
20649
|
+
return publicEndpoints.some(endpoint => request.url.includes(`${authPrefix}${endpoint}`));
|
|
20522
20650
|
}
|
|
20523
20651
|
/**
|
|
20524
20652
|
* Verifica si la request es a un endpoint que NO debe reintentar refresh en 401.
|
|
@@ -20527,6 +20655,9 @@ function isNoRefreshEndpoint(request, authPrefix) {
|
|
|
20527
20655
|
// Endpoints que no deben intentar refresh en 401:
|
|
20528
20656
|
// - Públicos: signin, signup, refresh, logout
|
|
20529
20657
|
// - MFA: verify (durante login), confirm (401 = código incorrecto), disable (401 = contraseña incorrecta)
|
|
20658
|
+
// - change-password: 401 = contraseña actual incorrecta, NO token expirado.
|
|
20659
|
+
// Sin esta exclusión, el retry tras refresh vuelve a dar 401 y el
|
|
20660
|
+
// catchError de handle401Error dispara logout.
|
|
20530
20661
|
const noRefreshEndpoints = [
|
|
20531
20662
|
'/signin',
|
|
20532
20663
|
'/signup',
|
|
@@ -20535,8 +20666,9 @@ function isNoRefreshEndpoint(request, authPrefix) {
|
|
|
20535
20666
|
'/mfa/verify',
|
|
20536
20667
|
'/mfa/confirm',
|
|
20537
20668
|
'/mfa/disable',
|
|
20669
|
+
'/change-password',
|
|
20538
20670
|
];
|
|
20539
|
-
return noRefreshEndpoints.some(
|
|
20671
|
+
return noRefreshEndpoints.some(endpoint => request.url.includes(`${authPrefix}${endpoint}`));
|
|
20540
20672
|
}
|
|
20541
20673
|
/**
|
|
20542
20674
|
* Maneja errores 401 refrescando el token.
|
|
@@ -20545,10 +20677,10 @@ function handle401Error(request, next, authService) {
|
|
|
20545
20677
|
if (!isRefreshing) {
|
|
20546
20678
|
isRefreshing = true;
|
|
20547
20679
|
refreshTokenSubject.next(null);
|
|
20548
|
-
return authService.refreshAccessToken().pipe(switchMap(
|
|
20680
|
+
return authService.refreshAccessToken().pipe(switchMap(response => {
|
|
20549
20681
|
refreshTokenSubject.next(response.accessToken);
|
|
20550
20682
|
return next(addAuthHeader(request, response.accessToken));
|
|
20551
|
-
}), catchError(
|
|
20683
|
+
}), catchError(error => {
|
|
20552
20684
|
authService.logout();
|
|
20553
20685
|
return throwError(() => error);
|
|
20554
20686
|
}), finalize(() => {
|
|
@@ -20556,7 +20688,7 @@ function handle401Error(request, next, authService) {
|
|
|
20556
20688
|
}));
|
|
20557
20689
|
}
|
|
20558
20690
|
// Esperar a que termine el refresco en curso
|
|
20559
|
-
return refreshTokenSubject.pipe(filter((token) => token !== null), take(1), switchMap(
|
|
20691
|
+
return refreshTokenSubject.pipe(filter((token) => token !== null), take(1), switchMap(token => next(addAuthHeader(request, token))));
|
|
20560
20692
|
}
|
|
20561
20693
|
|
|
20562
20694
|
/**
|
|
@@ -28924,16 +29056,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
28924
29056
|
}], ctorParameters: () => [{ type: i2.ToastController }] });
|
|
28925
29057
|
|
|
28926
29058
|
/**
|
|
28927
|
-
* `val-change-password-modal` — modal para
|
|
28928
|
-
*
|
|
28929
|
-
* pero para una sesión activa (vista de seguridad / settings).
|
|
29059
|
+
* `val-change-password-modal` — modal de gestión de contraseña para un usuario
|
|
29060
|
+
* autenticado. Análogo al modal de "recuperar contraseña" del `val-login`.
|
|
28930
29061
|
*
|
|
28931
|
-
*
|
|
28932
|
-
*
|
|
29062
|
+
* Es dual-mode: al abrirse consulta `AuthService.checkHasPassword()` y se adapta:
|
|
29063
|
+
* - **change** — el user ya tiene contraseña → pide actual + nueva →
|
|
29064
|
+
* `changePassword()`.
|
|
29065
|
+
* - **set** — el user es OAuth-only (sin contraseña) → pide solo la nueva,
|
|
29066
|
+
* pide confirmación explícita (no pierde el acceso social, es aditivo) →
|
|
29067
|
+
* `setPasswordForOAuthUser()`.
|
|
28933
29068
|
*
|
|
28934
|
-
*
|
|
28935
|
-
* `
|
|
28936
|
-
* pone `isOpen` en `false` en ambos casos.
|
|
29069
|
+
* Self-contained: inyecta `AuthService` y llama el endpoint directo — la app
|
|
29070
|
+
* solo controla `[isOpen]` y reacciona a `(changed)` / `(dismissed)`.
|
|
28937
29071
|
*
|
|
28938
29072
|
* i18n: usa el namespace compartido `_auth` (mismas claves que `val-login`).
|
|
28939
29073
|
*
|
|
@@ -28947,10 +29081,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
28947
29081
|
* ```
|
|
28948
29082
|
*/
|
|
28949
29083
|
class ChangePasswordModalComponent {
|
|
29084
|
+
/**
|
|
29085
|
+
* Controla la visibilidad del modal. Lo decide el componente padre. Cada vez
|
|
29086
|
+
* que pasa de cerrado a abierto se resuelve el modo (change vs set).
|
|
29087
|
+
*/
|
|
29088
|
+
set isOpen(value) {
|
|
29089
|
+
const opening = value && !this._isOpen;
|
|
29090
|
+
this._isOpen = value;
|
|
29091
|
+
if (opening) {
|
|
29092
|
+
this.resolveMode();
|
|
29093
|
+
}
|
|
29094
|
+
}
|
|
29095
|
+
get isOpen() {
|
|
29096
|
+
return this._isOpen;
|
|
29097
|
+
}
|
|
28950
29098
|
constructor() {
|
|
28951
|
-
|
|
28952
|
-
|
|
28953
|
-
/** Emite al cambiar la contraseña con éxito. El padre debe cerrar el modal. */
|
|
29099
|
+
this._isOpen = false;
|
|
29100
|
+
/** Emite al cambiar/crear la contraseña con éxito. El padre cierra el modal. */
|
|
28954
29101
|
this.changed = new EventEmitter();
|
|
28955
29102
|
/** Emite cuando el user cierra el modal (botón X o backdrop). */
|
|
28956
29103
|
this.dismissed = new EventEmitter();
|
|
@@ -28958,65 +29105,112 @@ class ChangePasswordModalComponent {
|
|
|
28958
29105
|
this.toast = inject(ToastService);
|
|
28959
29106
|
this.i18n = inject(I18nService);
|
|
28960
29107
|
this.i18nHelper = inject(InputI18nHelper);
|
|
29108
|
+
this.confirmDialog = inject(ConfirmationDialogService);
|
|
29109
|
+
this._mode = signal('loading');
|
|
29110
|
+
/** Modo actual — `loading` mientras se consulta `checkHasPassword()`. */
|
|
29111
|
+
this.mode = this._mode.asReadonly();
|
|
28961
29112
|
this._formState = signal(ComponentStates.ENABLED);
|
|
28962
|
-
this.formProps = computed(() =>
|
|
28963
|
-
|
|
28964
|
-
|
|
28965
|
-
|
|
28966
|
-
|
|
28967
|
-
|
|
28968
|
-
order: 0,
|
|
28969
|
-
fields: [
|
|
29113
|
+
this.formProps = computed(() => {
|
|
29114
|
+
if (this._mode() === 'set') {
|
|
29115
|
+
return this.i18nHelper.resolveForm({
|
|
29116
|
+
nameKey: 'setPasswordTitle',
|
|
29117
|
+
i18nNamespace: '_auth',
|
|
29118
|
+
sections: [
|
|
28970
29119
|
{
|
|
28971
|
-
|
|
28972
|
-
name: 'currentPassword',
|
|
28973
|
-
token: 'change-current-password',
|
|
28974
|
-
labelKey: 'currentPassword',
|
|
28975
|
-
hint: '',
|
|
28976
|
-
placeholderKey: 'passwordPlaceholder',
|
|
28977
|
-
errorKeys: {
|
|
28978
|
-
required: 'currentPasswordRequired',
|
|
28979
|
-
},
|
|
28980
|
-
validators: [Validators.required],
|
|
29120
|
+
name: this.t('setPasswordDescription'),
|
|
28981
29121
|
order: 0,
|
|
28982
|
-
|
|
28983
|
-
},
|
|
28984
|
-
{
|
|
28985
|
-
type: InputType.PASSWORD,
|
|
28986
|
-
name: 'newPassword',
|
|
28987
|
-
token: 'change-new-password',
|
|
28988
|
-
labelKey: 'newPassword',
|
|
28989
|
-
hintKey: 'newPasswordHint',
|
|
28990
|
-
placeholderKey: 'passwordPlaceholder',
|
|
28991
|
-
errorKeys: {
|
|
28992
|
-
required: 'passwordRequired',
|
|
28993
|
-
minlength: 'passwordMinLength',
|
|
28994
|
-
},
|
|
28995
|
-
validators: [Validators.required, Validators.minLength(8)],
|
|
28996
|
-
order: 1,
|
|
28997
|
-
state: ComponentStates.ENABLED,
|
|
29122
|
+
fields: [this.newPasswordField(0)],
|
|
28998
29123
|
},
|
|
28999
29124
|
],
|
|
29125
|
+
actions: {
|
|
29126
|
+
...SolidDefaultBlock('', 'submit'),
|
|
29127
|
+
token: 'set-submit',
|
|
29128
|
+
textKey: 'setPasswordSubmit',
|
|
29129
|
+
},
|
|
29130
|
+
state: this._formState(),
|
|
29131
|
+
});
|
|
29132
|
+
}
|
|
29133
|
+
return this.i18nHelper.resolveForm({
|
|
29134
|
+
nameKey: 'changePasswordTitle',
|
|
29135
|
+
i18nNamespace: '_auth',
|
|
29136
|
+
sections: [
|
|
29137
|
+
{
|
|
29138
|
+
name: this.t('changePasswordDescription'),
|
|
29139
|
+
order: 0,
|
|
29140
|
+
fields: [
|
|
29141
|
+
{
|
|
29142
|
+
type: InputType.PASSWORD,
|
|
29143
|
+
name: 'currentPassword',
|
|
29144
|
+
token: 'change-current-password',
|
|
29145
|
+
labelKey: 'currentPassword',
|
|
29146
|
+
hint: '',
|
|
29147
|
+
placeholderKey: 'passwordPlaceholder',
|
|
29148
|
+
errorKeys: {
|
|
29149
|
+
required: 'currentPasswordRequired',
|
|
29150
|
+
},
|
|
29151
|
+
validators: [Validators.required],
|
|
29152
|
+
order: 0,
|
|
29153
|
+
state: ComponentStates.ENABLED,
|
|
29154
|
+
},
|
|
29155
|
+
this.newPasswordField(1),
|
|
29156
|
+
],
|
|
29157
|
+
},
|
|
29158
|
+
],
|
|
29159
|
+
actions: {
|
|
29160
|
+
...SolidDefaultBlock('', 'submit'),
|
|
29161
|
+
token: 'change-submit',
|
|
29162
|
+
textKey: 'changePasswordSubmit',
|
|
29000
29163
|
},
|
|
29001
|
-
|
|
29002
|
-
|
|
29003
|
-
|
|
29004
|
-
token: 'change-submit',
|
|
29005
|
-
textKey: 'changePasswordSubmit',
|
|
29006
|
-
},
|
|
29007
|
-
state: this._formState(),
|
|
29008
|
-
}));
|
|
29164
|
+
state: this._formState(),
|
|
29165
|
+
});
|
|
29166
|
+
});
|
|
29009
29167
|
addIcons({ closeOutline });
|
|
29010
29168
|
}
|
|
29011
29169
|
/** Traduce una clave del namespace `_auth`. */
|
|
29012
29170
|
t(key) {
|
|
29013
29171
|
return this.i18n.t(key, '_auth');
|
|
29014
29172
|
}
|
|
29173
|
+
/** Campo "nueva contraseña" — compartido por ambos modos. */
|
|
29174
|
+
newPasswordField(order) {
|
|
29175
|
+
return {
|
|
29176
|
+
type: InputType.PASSWORD,
|
|
29177
|
+
name: 'newPassword',
|
|
29178
|
+
token: 'change-new-password',
|
|
29179
|
+
labelKey: 'newPassword',
|
|
29180
|
+
hintKey: 'newPasswordHint',
|
|
29181
|
+
placeholderKey: 'passwordPlaceholder',
|
|
29182
|
+
errorKeys: {
|
|
29183
|
+
required: 'passwordRequired',
|
|
29184
|
+
minlength: 'passwordMinLength',
|
|
29185
|
+
},
|
|
29186
|
+
validators: [Validators.required, Validators.minLength(8)],
|
|
29187
|
+
order,
|
|
29188
|
+
state: ComponentStates.ENABLED,
|
|
29189
|
+
};
|
|
29190
|
+
}
|
|
29015
29191
|
/** Cierre iniciado por el user (X / backdrop). */
|
|
29016
29192
|
close() {
|
|
29017
29193
|
this.dismissed.emit();
|
|
29018
29194
|
}
|
|
29019
|
-
|
|
29195
|
+
/** Consulta si el user ya tiene contraseña para elegir el modo del modal. */
|
|
29196
|
+
resolveMode() {
|
|
29197
|
+
this._mode.set('loading');
|
|
29198
|
+
this._formState.set(ComponentStates.ENABLED);
|
|
29199
|
+
this.auth.checkHasPassword().subscribe({
|
|
29200
|
+
next: res => this._mode.set(res.hasPassword ? 'change' : 'set'),
|
|
29201
|
+
// Fallback conservador: ante un fallo, asumir flujo de cambio normal.
|
|
29202
|
+
error: () => this._mode.set('change'),
|
|
29203
|
+
});
|
|
29204
|
+
}
|
|
29205
|
+
async submitHandler(event) {
|
|
29206
|
+
if (this._mode() === 'set') {
|
|
29207
|
+
await this.handleSetPassword(event);
|
|
29208
|
+
return;
|
|
29209
|
+
}
|
|
29210
|
+
this.handleChangePassword(event);
|
|
29211
|
+
}
|
|
29212
|
+
/** Flujo normal: el user tiene contraseña y la cambia. */
|
|
29213
|
+
handleChangePassword(event) {
|
|
29020
29214
|
const currentPassword = event.fields['currentPassword'];
|
|
29021
29215
|
const newPassword = event.fields['newPassword'];
|
|
29022
29216
|
if (!currentPassword || !newPassword) {
|
|
@@ -29036,6 +29230,39 @@ class ChangePasswordModalComponent {
|
|
|
29036
29230
|
},
|
|
29037
29231
|
});
|
|
29038
29232
|
}
|
|
29233
|
+
/**
|
|
29234
|
+
* Flujo OAuth-only: el user no tiene contraseña. Antes de crearla pide
|
|
29235
|
+
* confirmación explícita — es un cambio de cuenta, aunque aditivo (conserva
|
|
29236
|
+
* el acceso social).
|
|
29237
|
+
*/
|
|
29238
|
+
async handleSetPassword(event) {
|
|
29239
|
+
const newPassword = event.fields['newPassword'];
|
|
29240
|
+
if (!newPassword) {
|
|
29241
|
+
this.showToast(this.t('completeAllFields'));
|
|
29242
|
+
return;
|
|
29243
|
+
}
|
|
29244
|
+
const result = await this.confirmDialog.confirm({
|
|
29245
|
+
title: this.t('setPasswordConfirmTitle'),
|
|
29246
|
+
message: this.t('setPasswordConfirmMessage'),
|
|
29247
|
+
confirmButton: { text: this.t('setPasswordConfirmOk'), role: 'confirm' },
|
|
29248
|
+
cancelButton: { text: this.t('setPasswordConfirmCancel'), role: 'cancel' },
|
|
29249
|
+
});
|
|
29250
|
+
if (!result.confirmed) {
|
|
29251
|
+
return;
|
|
29252
|
+
}
|
|
29253
|
+
this._formState.set(ComponentStates.WORKING);
|
|
29254
|
+
this.auth.setPasswordForOAuthUser(newPassword).subscribe({
|
|
29255
|
+
next: () => {
|
|
29256
|
+
this._formState.set(ComponentStates.ENABLED);
|
|
29257
|
+
this.showToast(this.t('passwordSetSuccess'));
|
|
29258
|
+
this.changed.emit();
|
|
29259
|
+
},
|
|
29260
|
+
error: err => {
|
|
29261
|
+
this._formState.set(ComponentStates.ENABLED);
|
|
29262
|
+
this.showToast(this.resolveError(err));
|
|
29263
|
+
},
|
|
29264
|
+
});
|
|
29265
|
+
}
|
|
29039
29266
|
/** Mapea los códigos de error del backend a mensajes del namespace `_auth`. */
|
|
29040
29267
|
resolveError(err) {
|
|
29041
29268
|
const code = err?.code;
|
|
@@ -29044,6 +29271,8 @@ class ChangePasswordModalComponent {
|
|
|
29044
29271
|
return this.t('errorCurrentPasswordWrong');
|
|
29045
29272
|
case 'AUTHV2_SAME_PASSWORD':
|
|
29046
29273
|
return this.t('errorSamePassword');
|
|
29274
|
+
case 'AUTHV2_PASSWORD_ALREADY_SET':
|
|
29275
|
+
return this.t('errorPasswordAlreadySet');
|
|
29047
29276
|
default:
|
|
29048
29277
|
return this.t('errorGeneric');
|
|
29049
29278
|
}
|
|
@@ -29052,11 +29281,11 @@ class ChangePasswordModalComponent {
|
|
|
29052
29281
|
this.toast.show({ message, duration: 3500 });
|
|
29053
29282
|
}
|
|
29054
29283
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ChangePasswordModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
29055
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "
|
|
29284
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ChangePasswordModalComponent, isStandalone: true, selector: "val-change-password-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 <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n @if (mode() === 'loading') {\n <div class=\"modal-loading\">\n <ion-spinner name=\"crescent\"></ion-spinner>\n </div>\n } @else {\n <val-form [props]=\"formProps()\" (onSubmit)=\"submitHandler($event)\" />\n }\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n", styles: [".modal-form-section{display:flex;flex-direction:column;gap:16px;max-width:420px;margin:0 auto;padding-top:8px}.modal-loading{display:flex;justify-content:center;padding:40px 0}\n"], dependencies: [{ 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: IonModal, selector: "ion-modal" }, { 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"] }] }); }
|
|
29056
29285
|
}
|
|
29057
29286
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ChangePasswordModalComponent, decorators: [{
|
|
29058
29287
|
type: Component,
|
|
29059
|
-
args: [{ selector: 'val-change-password-modal', standalone: true, imports: [IonButton, IonButtons, IonContent, IonHeader, IonIcon, IonModal, IonToolbar, FormComponent], 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 <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n <val-form [props]=\"formProps()\" (onSubmit)=\"submitHandler($event)\" />\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n", styles: [".modal-form-section{display:flex;flex-direction:column;gap:16px;max-width:420px;margin:0 auto;padding-top:8px}\n"] }]
|
|
29288
|
+
args: [{ selector: 'val-change-password-modal', standalone: true, imports: [IonButton, IonButtons, IonContent, IonHeader, IonIcon, IonModal, IonSpinner, IonToolbar, FormComponent], 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 <ion-content class=\"ion-padding\">\n <section class=\"modal-form-section\">\n @if (mode() === 'loading') {\n <div class=\"modal-loading\">\n <ion-spinner name=\"crescent\"></ion-spinner>\n </div>\n } @else {\n <val-form [props]=\"formProps()\" (onSubmit)=\"submitHandler($event)\" />\n }\n </section>\n </ion-content>\n </ng-template>\n</ion-modal>\n", styles: [".modal-form-section{display:flex;flex-direction:column;gap:16px;max-width:420px;margin:0 auto;padding-top:8px}.modal-loading{display:flex;justify-content:center;padding:40px 0}\n"] }]
|
|
29060
29289
|
}], ctorParameters: () => [], propDecorators: { isOpen: [{
|
|
29061
29290
|
type: Input
|
|
29062
29291
|
}], changed: [{
|
|
@@ -30005,6 +30234,408 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
30005
30234
|
type: Output
|
|
30006
30235
|
}] } });
|
|
30007
30236
|
|
|
30237
|
+
/** Segundos de espera antes de poder reenviar el código EMAIL/SMS. */
|
|
30238
|
+
const RESEND_COOLDOWN_SECONDS = 30;
|
|
30239
|
+
/**
|
|
30240
|
+
* `val-mfa-modal` — modal de gestión de autenticación de dos factores (MFA)
|
|
30241
|
+
* para un usuario autenticado. Mismo patrón que `val-change-password-modal`.
|
|
30242
|
+
*
|
|
30243
|
+
* Flujo (máquina de estados interna):
|
|
30244
|
+
* - `loading` → `getProfile()` para conocer el estado MFA.
|
|
30245
|
+
* - `status` → muestra MFA habilitado/deshabilitado. Si está habilitado:
|
|
30246
|
+
* gestión de backup codes (TOTP) + deshabilitar. Si no: botón habilitar.
|
|
30247
|
+
* - `method-select` → elegir TOTP / EMAIL / SMS.
|
|
30248
|
+
* - `totp-setup` → QR + secreto manual + backup codes → verificar código.
|
|
30249
|
+
* - `code-confirm` → (EMAIL/SMS) ingresar código recibido, con reenvío.
|
|
30250
|
+
* - `disable` → contraseña para deshabilitar MFA.
|
|
30251
|
+
*
|
|
30252
|
+
* El QR se genera **client-side** (`QrGeneratorService`) — el secreto TOTP
|
|
30253
|
+
* nunca sale del navegador.
|
|
30254
|
+
*
|
|
30255
|
+
* Self-contained: inyecta `AuthService` y llama los endpoints directo. La app
|
|
30256
|
+
* controla `[isOpen]` y reacciona a `(changed)` / `(dismissed)`.
|
|
30257
|
+
*
|
|
30258
|
+
* i18n: namespace compartido `_auth`.
|
|
30259
|
+
*
|
|
30260
|
+
* @example
|
|
30261
|
+
* ```html
|
|
30262
|
+
* <val-mfa-modal
|
|
30263
|
+
* [isOpen]="isModalOpen()"
|
|
30264
|
+
* (dismissed)="isModalOpen.set(false)"
|
|
30265
|
+
* />
|
|
30266
|
+
* ```
|
|
30267
|
+
*/
|
|
30268
|
+
class MfaModalComponent {
|
|
30269
|
+
/** Controla la visibilidad. Cada apertura re-resuelve el estado MFA. */
|
|
30270
|
+
set isOpen(value) {
|
|
30271
|
+
const opening = value && !this._isOpen;
|
|
30272
|
+
this._isOpen = value;
|
|
30273
|
+
if (opening) {
|
|
30274
|
+
this.resolveStatus();
|
|
30275
|
+
}
|
|
30276
|
+
}
|
|
30277
|
+
get isOpen() {
|
|
30278
|
+
return this._isOpen;
|
|
30279
|
+
}
|
|
30280
|
+
constructor() {
|
|
30281
|
+
this._isOpen = false;
|
|
30282
|
+
/** Emite cuando el estado MFA cambia (habilitado / deshabilitado). */
|
|
30283
|
+
this.changed = new EventEmitter();
|
|
30284
|
+
/** Emite cuando el user cierra el modal (botón X o backdrop). */
|
|
30285
|
+
this.dismissed = new EventEmitter();
|
|
30286
|
+
this.auth = inject(AuthService);
|
|
30287
|
+
this.toast = inject(ToastService);
|
|
30288
|
+
this.i18n = inject(I18nService);
|
|
30289
|
+
this.qrGen = inject(QrGeneratorService);
|
|
30290
|
+
this._step = signal('loading');
|
|
30291
|
+
/** Paso actual del flujo. */
|
|
30292
|
+
this.step = this._step.asReadonly();
|
|
30293
|
+
/** `true` mientras una llamada al backend está en curso. */
|
|
30294
|
+
this.working = signal(false);
|
|
30295
|
+
// Estado MFA actual.
|
|
30296
|
+
this.mfaEnabled = signal(false);
|
|
30297
|
+
this.mfaMethod = signal(null);
|
|
30298
|
+
this.userPhone = signal(null);
|
|
30299
|
+
this.backupCodesCount = signal(0);
|
|
30300
|
+
// Estado del flujo de habilitación.
|
|
30301
|
+
this.selectedMethod = signal('TOTP');
|
|
30302
|
+
this.totpSetup = signal(null);
|
|
30303
|
+
this.totpQr = signal(null);
|
|
30304
|
+
/** Códigos de respaldo recién regenerados — se muestran una sola vez. */
|
|
30305
|
+
this.regeneratedCodes = signal(null);
|
|
30306
|
+
this.resendCooldown = signal(0);
|
|
30307
|
+
this.pinControl = new FormControl('', [Validators.required, Validators.minLength(6), Validators.maxLength(6)]);
|
|
30308
|
+
this.passwordControl = new FormControl('', [Validators.required]);
|
|
30309
|
+
this.phoneControl = new FormControl('', [Validators.required, Validators.pattern(/^\+[1-9]\d{6,14}$/)]);
|
|
30310
|
+
this.pinInputProps = {
|
|
30311
|
+
control: this.pinControl,
|
|
30312
|
+
token: 'mfa-code',
|
|
30313
|
+
length: 6,
|
|
30314
|
+
allowNumbersOnly: true,
|
|
30315
|
+
autoFocus: true,
|
|
30316
|
+
};
|
|
30317
|
+
this.resendTimer = null;
|
|
30318
|
+
addIcons({ closeOutline });
|
|
30319
|
+
}
|
|
30320
|
+
ngOnDestroy() {
|
|
30321
|
+
this.stopCooldown();
|
|
30322
|
+
}
|
|
30323
|
+
/** Traduce una clave del namespace `_auth`. */
|
|
30324
|
+
t(key) {
|
|
30325
|
+
return this.i18n.t(key, '_auth');
|
|
30326
|
+
}
|
|
30327
|
+
/** Cierre iniciado por el user (X / backdrop). */
|
|
30328
|
+
close() {
|
|
30329
|
+
this.dismissed.emit();
|
|
30330
|
+
}
|
|
30331
|
+
// ===========================================================================
|
|
30332
|
+
// Carga de estado
|
|
30333
|
+
// ===========================================================================
|
|
30334
|
+
/** Consulta el perfil para conocer el estado MFA y posicionar el flujo. */
|
|
30335
|
+
resolveStatus() {
|
|
30336
|
+
this._step.set('loading');
|
|
30337
|
+
this.resetFlow();
|
|
30338
|
+
this.auth.getProfile().subscribe({
|
|
30339
|
+
next: profile => {
|
|
30340
|
+
this.mfaEnabled.set(profile.mfaEnabled);
|
|
30341
|
+
this.mfaMethod.set(profile.mfaMethod ?? null);
|
|
30342
|
+
this.userPhone.set(profile.phone ?? null);
|
|
30343
|
+
if (profile.mfaEnabled && profile.mfaMethod === 'TOTP') {
|
|
30344
|
+
this.loadBackupCount();
|
|
30345
|
+
}
|
|
30346
|
+
this._step.set('status');
|
|
30347
|
+
},
|
|
30348
|
+
error: () => {
|
|
30349
|
+
// Fallback: usar el signal de usuario en sesión.
|
|
30350
|
+
const user = this.auth.user();
|
|
30351
|
+
this.mfaEnabled.set(user?.mfaEnabled ?? false);
|
|
30352
|
+
this.mfaMethod.set(user?.mfaMethod ?? null);
|
|
30353
|
+
this._step.set('status');
|
|
30354
|
+
},
|
|
30355
|
+
});
|
|
30356
|
+
}
|
|
30357
|
+
loadBackupCount() {
|
|
30358
|
+
this.auth.getBackupCodesCount().subscribe({
|
|
30359
|
+
next: res => this.backupCodesCount.set(res.count),
|
|
30360
|
+
error: () => this.backupCodesCount.set(0),
|
|
30361
|
+
});
|
|
30362
|
+
}
|
|
30363
|
+
// ===========================================================================
|
|
30364
|
+
// Navegación entre pasos
|
|
30365
|
+
// ===========================================================================
|
|
30366
|
+
goToMethodSelect() {
|
|
30367
|
+
this.regeneratedCodes.set(null);
|
|
30368
|
+
this._step.set('method-select');
|
|
30369
|
+
}
|
|
30370
|
+
goToDisable() {
|
|
30371
|
+
this.passwordControl.reset();
|
|
30372
|
+
this._step.set('disable');
|
|
30373
|
+
}
|
|
30374
|
+
backToStatus() {
|
|
30375
|
+
this.stopCooldown();
|
|
30376
|
+
this.resolveStatus();
|
|
30377
|
+
}
|
|
30378
|
+
// ===========================================================================
|
|
30379
|
+
// Habilitar MFA
|
|
30380
|
+
// ===========================================================================
|
|
30381
|
+
/** Continúa desde el selector de método al setup correspondiente. */
|
|
30382
|
+
proceedWithMethod() {
|
|
30383
|
+
const method = this.selectedMethod();
|
|
30384
|
+
if (method === 'TOTP') {
|
|
30385
|
+
this.setupTotp();
|
|
30386
|
+
return;
|
|
30387
|
+
}
|
|
30388
|
+
let phone;
|
|
30389
|
+
if (method === 'SMS' && !this.userPhone()) {
|
|
30390
|
+
if (this.phoneControl.invalid) {
|
|
30391
|
+
this.phoneControl.markAsTouched();
|
|
30392
|
+
this.showToast(this.t('mfaPhoneInvalid'));
|
|
30393
|
+
return;
|
|
30394
|
+
}
|
|
30395
|
+
phone = this.phoneControl.value ?? undefined;
|
|
30396
|
+
}
|
|
30397
|
+
this.working.set(true);
|
|
30398
|
+
this.auth.setupMFA(method, phone).subscribe({
|
|
30399
|
+
next: res => {
|
|
30400
|
+
this.working.set(false);
|
|
30401
|
+
if (res.codeSent) {
|
|
30402
|
+
this.pinControl.reset();
|
|
30403
|
+
this._step.set('code-confirm');
|
|
30404
|
+
this.startCooldown();
|
|
30405
|
+
}
|
|
30406
|
+
},
|
|
30407
|
+
error: err => {
|
|
30408
|
+
this.working.set(false);
|
|
30409
|
+
this.showToast(this.resolveError(err));
|
|
30410
|
+
},
|
|
30411
|
+
});
|
|
30412
|
+
}
|
|
30413
|
+
setupTotp() {
|
|
30414
|
+
this.working.set(true);
|
|
30415
|
+
this.auth.setupTOTP().subscribe({
|
|
30416
|
+
next: res => {
|
|
30417
|
+
this.totpSetup.set(res);
|
|
30418
|
+
this.pinControl.reset();
|
|
30419
|
+
this.qrGen
|
|
30420
|
+
.generate({ data: res.qrCodeUrl, width: 220 })
|
|
30421
|
+
.then(qr => this.totpQr.set(qr))
|
|
30422
|
+
.catch(() => this.totpQr.set(null));
|
|
30423
|
+
this.working.set(false);
|
|
30424
|
+
this._step.set('totp-setup');
|
|
30425
|
+
},
|
|
30426
|
+
error: err => {
|
|
30427
|
+
this.working.set(false);
|
|
30428
|
+
this.showToast(this.resolveError(err));
|
|
30429
|
+
},
|
|
30430
|
+
});
|
|
30431
|
+
}
|
|
30432
|
+
/** Verifica el código TOTP de la app de autenticación y activa MFA. */
|
|
30433
|
+
verifyTotp() {
|
|
30434
|
+
const code = this.pinControl.value;
|
|
30435
|
+
if (!code || code.length !== 6) {
|
|
30436
|
+
this.showToast(this.t('mfaCodeInvalid'));
|
|
30437
|
+
return;
|
|
30438
|
+
}
|
|
30439
|
+
this.working.set(true);
|
|
30440
|
+
this.auth.verifyTOTPSetup(code).subscribe({
|
|
30441
|
+
next: res => {
|
|
30442
|
+
this.working.set(false);
|
|
30443
|
+
if (res.enabled) {
|
|
30444
|
+
this.showToast(this.t('mfaEnabledOk'));
|
|
30445
|
+
this.changed.emit();
|
|
30446
|
+
this.resolveStatus();
|
|
30447
|
+
}
|
|
30448
|
+
},
|
|
30449
|
+
error: err => {
|
|
30450
|
+
this.working.set(false);
|
|
30451
|
+
this.showToast(this.resolveError(err));
|
|
30452
|
+
},
|
|
30453
|
+
});
|
|
30454
|
+
}
|
|
30455
|
+
/** Confirma el código EMAIL/SMS y activa MFA. */
|
|
30456
|
+
confirmCode() {
|
|
30457
|
+
const code = this.pinControl.value;
|
|
30458
|
+
if (!code || code.length !== 6) {
|
|
30459
|
+
this.showToast(this.t('mfaCodeInvalid'));
|
|
30460
|
+
return;
|
|
30461
|
+
}
|
|
30462
|
+
this.working.set(true);
|
|
30463
|
+
this.auth.confirmMFA(code).subscribe({
|
|
30464
|
+
next: res => {
|
|
30465
|
+
this.working.set(false);
|
|
30466
|
+
if (res.mfaEnabled) {
|
|
30467
|
+
this.showToast(this.t('mfaEnabledOk'));
|
|
30468
|
+
this.changed.emit();
|
|
30469
|
+
this.resolveStatus();
|
|
30470
|
+
}
|
|
30471
|
+
},
|
|
30472
|
+
error: err => {
|
|
30473
|
+
this.working.set(false);
|
|
30474
|
+
this.showToast(this.resolveError(err));
|
|
30475
|
+
},
|
|
30476
|
+
});
|
|
30477
|
+
}
|
|
30478
|
+
/** Reenvía el código EMAIL/SMS (re-ejecuta el setup). */
|
|
30479
|
+
resendCode() {
|
|
30480
|
+
if (this.resendCooldown() > 0) {
|
|
30481
|
+
return;
|
|
30482
|
+
}
|
|
30483
|
+
this.proceedWithMethod();
|
|
30484
|
+
}
|
|
30485
|
+
// ===========================================================================
|
|
30486
|
+
// Gestión (MFA habilitado)
|
|
30487
|
+
// ===========================================================================
|
|
30488
|
+
/** Regenera los códigos de respaldo TOTP y los muestra una vez. */
|
|
30489
|
+
regenerateBackupCodes() {
|
|
30490
|
+
this.working.set(true);
|
|
30491
|
+
this.auth.regenerateBackupCodes().subscribe({
|
|
30492
|
+
next: res => {
|
|
30493
|
+
this.working.set(false);
|
|
30494
|
+
this.backupCodesCount.set(res.backupCodes.length);
|
|
30495
|
+
this.regeneratedCodes.set(res.backupCodes);
|
|
30496
|
+
},
|
|
30497
|
+
error: err => {
|
|
30498
|
+
this.working.set(false);
|
|
30499
|
+
this.showToast(this.resolveError(err));
|
|
30500
|
+
},
|
|
30501
|
+
});
|
|
30502
|
+
}
|
|
30503
|
+
/** Deshabilita MFA — requiere la contraseña de la cuenta. */
|
|
30504
|
+
confirmDisable() {
|
|
30505
|
+
const password = this.passwordControl.value;
|
|
30506
|
+
if (!password) {
|
|
30507
|
+
this.showToast(this.t('mfaPasswordRequired'));
|
|
30508
|
+
return;
|
|
30509
|
+
}
|
|
30510
|
+
this.working.set(true);
|
|
30511
|
+
this.auth.disableMFA(password).subscribe({
|
|
30512
|
+
next: res => {
|
|
30513
|
+
this.working.set(false);
|
|
30514
|
+
if (res.mfaDisabled) {
|
|
30515
|
+
this.showToast(this.t('mfaDisabledOk'));
|
|
30516
|
+
this.changed.emit();
|
|
30517
|
+
this.resolveStatus();
|
|
30518
|
+
}
|
|
30519
|
+
},
|
|
30520
|
+
error: err => {
|
|
30521
|
+
this.working.set(false);
|
|
30522
|
+
this.showToast(this.resolveError(err));
|
|
30523
|
+
},
|
|
30524
|
+
});
|
|
30525
|
+
}
|
|
30526
|
+
/** Copia una lista de códigos de respaldo al portapapeles. */
|
|
30527
|
+
async copyCodes(codes) {
|
|
30528
|
+
try {
|
|
30529
|
+
await navigator.clipboard.writeText(codes.join('\n'));
|
|
30530
|
+
this.showToast(this.t('mfaCodesCopied'));
|
|
30531
|
+
}
|
|
30532
|
+
catch {
|
|
30533
|
+
/* sin recurso de copia disponible */
|
|
30534
|
+
}
|
|
30535
|
+
}
|
|
30536
|
+
/** Etiqueta i18n legible para un método MFA. */
|
|
30537
|
+
methodLabel(method) {
|
|
30538
|
+
switch (method) {
|
|
30539
|
+
case 'TOTP':
|
|
30540
|
+
return this.t('mfaMethodTotp');
|
|
30541
|
+
case 'EMAIL':
|
|
30542
|
+
return this.t('mfaMethodEmail');
|
|
30543
|
+
case 'SMS':
|
|
30544
|
+
return this.t('mfaMethodSms');
|
|
30545
|
+
default:
|
|
30546
|
+
return '';
|
|
30547
|
+
}
|
|
30548
|
+
}
|
|
30549
|
+
// ===========================================================================
|
|
30550
|
+
// Helpers
|
|
30551
|
+
// ===========================================================================
|
|
30552
|
+
resetFlow() {
|
|
30553
|
+
this.pinControl.reset();
|
|
30554
|
+
this.passwordControl.reset();
|
|
30555
|
+
this.phoneControl.reset();
|
|
30556
|
+
this.totpSetup.set(null);
|
|
30557
|
+
this.totpQr.set(null);
|
|
30558
|
+
this.regeneratedCodes.set(null);
|
|
30559
|
+
this.selectedMethod.set('TOTP');
|
|
30560
|
+
this.stopCooldown();
|
|
30561
|
+
}
|
|
30562
|
+
startCooldown() {
|
|
30563
|
+
this.stopCooldown();
|
|
30564
|
+
this.resendCooldown.set(RESEND_COOLDOWN_SECONDS);
|
|
30565
|
+
this.resendTimer = setInterval(() => {
|
|
30566
|
+
this.resendCooldown.update(v => v - 1);
|
|
30567
|
+
if (this.resendCooldown() <= 0) {
|
|
30568
|
+
this.stopCooldown();
|
|
30569
|
+
}
|
|
30570
|
+
}, 1000);
|
|
30571
|
+
}
|
|
30572
|
+
stopCooldown() {
|
|
30573
|
+
if (this.resendTimer) {
|
|
30574
|
+
clearInterval(this.resendTimer);
|
|
30575
|
+
this.resendTimer = null;
|
|
30576
|
+
}
|
|
30577
|
+
this.resendCooldown.set(0);
|
|
30578
|
+
}
|
|
30579
|
+
/** Mapea los códigos de error MFA del backend a mensajes del namespace `_auth`. */
|
|
30580
|
+
resolveError(err) {
|
|
30581
|
+
const code = err?.code;
|
|
30582
|
+
switch (code) {
|
|
30583
|
+
case 'AUTHV2_MFA_INVALID_CODE':
|
|
30584
|
+
return this.t('mfaErrorInvalidCode');
|
|
30585
|
+
case 'AUTHV2_EXPIRED_CODE':
|
|
30586
|
+
return this.t('mfaErrorExpiredCode');
|
|
30587
|
+
case 'AUTHV2_CODE_USED':
|
|
30588
|
+
return this.t('mfaErrorCodeUsed');
|
|
30589
|
+
case 'AUTHV2_MFA_ALREADY_ACTIVE':
|
|
30590
|
+
return this.t('mfaErrorAlreadyActive');
|
|
30591
|
+
case 'AUTHV2_MFA_NOT_ENABLED':
|
|
30592
|
+
return this.t('mfaErrorNotEnabled');
|
|
30593
|
+
case 'AUTHV2_PHONE_REQUIRED':
|
|
30594
|
+
return this.t('mfaErrorPhoneRequired');
|
|
30595
|
+
case 'AUTHV2_PHONE_EXISTS':
|
|
30596
|
+
return this.t('mfaErrorPhoneExists');
|
|
30597
|
+
case 'AUTHV2_TOO_MANY_ATTEMPTS':
|
|
30598
|
+
return this.t('errorTooManyAttempts');
|
|
30599
|
+
case 'AUTHV2_INVALID_CURRENT_PASSWORD':
|
|
30600
|
+
return this.t('errorCurrentPasswordWrong');
|
|
30601
|
+
default:
|
|
30602
|
+
return this.t('errorGeneric');
|
|
30603
|
+
}
|
|
30604
|
+
}
|
|
30605
|
+
showToast(message) {
|
|
30606
|
+
this.toast.show({ message, duration: 3500 });
|
|
30607
|
+
}
|
|
30608
|
+
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"] }] }); }
|
|
30610
|
+
}
|
|
30611
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: MfaModalComponent, decorators: [{
|
|
30612
|
+
type: Component,
|
|
30613
|
+
args: [{ selector: 'val-mfa-modal', standalone: true, imports: [
|
|
30614
|
+
ReactiveFormsModule,
|
|
30615
|
+
IonButton,
|
|
30616
|
+
IonButtons,
|
|
30617
|
+
IonContent,
|
|
30618
|
+
IonHeader,
|
|
30619
|
+
IonIcon,
|
|
30620
|
+
IonInput,
|
|
30621
|
+
IonItem,
|
|
30622
|
+
IonLabel,
|
|
30623
|
+
IonModal,
|
|
30624
|
+
IonRadio,
|
|
30625
|
+
IonRadioGroup,
|
|
30626
|
+
IonSpinner,
|
|
30627
|
+
IonToolbar,
|
|
30628
|
+
QrCodeComponent,
|
|
30629
|
+
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"] }]
|
|
30631
|
+
}], ctorParameters: () => [], propDecorators: { isOpen: [{
|
|
30632
|
+
type: Input
|
|
30633
|
+
}], changed: [{
|
|
30634
|
+
type: Output
|
|
30635
|
+
}], dismissed: [{
|
|
30636
|
+
type: Output
|
|
30637
|
+
}] } });
|
|
30638
|
+
|
|
30008
30639
|
/**
|
|
30009
30640
|
* ItemListComponent
|
|
30010
30641
|
*
|
|
@@ -46441,5 +47072,5 @@ function buildFooterLinks(links, t, resolver) {
|
|
|
46441
47072
|
* Generated bundle index. Do not edit.
|
|
46442
47073
|
*/
|
|
46443
47074
|
|
|
46444
|
-
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 };
|
|
47075
|
+
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 };
|
|
46445
47076
|
//# sourceMappingURL=valtech-components.mjs.map
|