2024-06-17 01:52:15 -07:00
|
|
|
<script setup lang="ts">
|
2023-08-23 19:59:16 -07:00
|
|
|
import type { IFormInputs } from '@/Interface';
|
|
|
|
import Logo from '../components/Logo.vue';
|
|
|
|
import {
|
|
|
|
MFA_AUTHENTICATION_RECOVERY_CODE_INPUT_MAX_LENGTH,
|
|
|
|
MFA_AUTHENTICATION_TOKEN_INPUT_MAX_LENGTH,
|
2024-06-17 01:52:15 -07:00
|
|
|
MFA_FORM,
|
2023-08-23 19:59:16 -07:00
|
|
|
} from '@/constants';
|
|
|
|
import { mfaEventBus } from '@/event-bus';
|
2024-06-17 01:52:15 -07:00
|
|
|
import { onMounted, ref } from 'vue';
|
|
|
|
import { useI18n } from '@/composables/useI18n';
|
|
|
|
import { toRefs } from '@vueuse/core';
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// #region Props
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
2024-06-18 00:55:10 -07:00
|
|
|
const props = defineProps<{
|
2024-06-24 03:13:18 -07:00
|
|
|
reportError: boolean;
|
2024-06-18 00:55:10 -07:00
|
|
|
}>();
|
2024-06-17 01:52:15 -07:00
|
|
|
|
|
|
|
// #endregion
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// #region Reactive properties
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
const hasAnyChanges = ref(false);
|
|
|
|
const formBus = ref(mfaEventBus);
|
|
|
|
const formInputs = ref<null | IFormInputs>(null);
|
|
|
|
const showRecoveryCodeForm = ref(false);
|
|
|
|
const verifyingMfaToken = ref(false);
|
|
|
|
const formError = ref('');
|
|
|
|
const { reportError } = toRefs(props);
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// #region Composable
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
const i18 = useI18n();
|
|
|
|
|
|
|
|
// #endregion
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// #region Emit
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
2024-07-03 05:19:24 -07:00
|
|
|
const emit = defineEmits<{
|
2024-07-04 00:30:51 -07:00
|
|
|
onFormChanged: [formField: string];
|
|
|
|
onBackClick: [formField: string];
|
|
|
|
submit: [{ token: string; recoveryCode: string }];
|
2024-07-03 05:19:24 -07:00
|
|
|
}>();
|
2024-06-17 01:52:15 -07:00
|
|
|
|
|
|
|
// #endregion
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// #region Methods
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
const formField = (
|
|
|
|
name: string,
|
|
|
|
label: string,
|
|
|
|
placeholder: string,
|
|
|
|
maxlength: number,
|
|
|
|
focus = true,
|
|
|
|
) => {
|
|
|
|
return {
|
|
|
|
name,
|
|
|
|
initialValue: '',
|
|
|
|
properties: {
|
|
|
|
label,
|
|
|
|
placeholder,
|
|
|
|
maxlength,
|
|
|
|
capitalize: true,
|
|
|
|
validateOnBlur: false,
|
|
|
|
focusInitially: focus,
|
2023-08-23 19:59:16 -07:00
|
|
|
},
|
2024-06-17 01:52:15 -07:00
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
const onRecoveryCodeClick = () => {
|
|
|
|
formError.value = '';
|
|
|
|
showRecoveryCodeForm.value = true;
|
|
|
|
hasAnyChanges.value = false;
|
|
|
|
formInputs.value = [mfaRecoveryCodeFieldWithDefaults()];
|
|
|
|
emit('onFormChanged', MFA_FORM.MFA_RECOVERY_CODE);
|
|
|
|
};
|
|
|
|
|
|
|
|
const onBackClick = () => {
|
|
|
|
if (!showRecoveryCodeForm.value) {
|
|
|
|
emit('onBackClick', MFA_FORM.MFA_TOKEN);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
showRecoveryCodeForm.value = false;
|
|
|
|
hasAnyChanges.value = true;
|
|
|
|
formInputs.value = [mfaTokenFieldWithDefaults()];
|
|
|
|
emit('onBackClick', MFA_FORM.MFA_RECOVERY_CODE);
|
|
|
|
};
|
|
|
|
|
|
|
|
const onSubmit = async (form: { token: string; recoveryCode: string }) => {
|
|
|
|
formError.value = !showRecoveryCodeForm.value
|
|
|
|
? i18.baseText('mfa.code.invalid')
|
|
|
|
: i18.baseText('mfa.recovery.invalid');
|
|
|
|
emit('submit', form);
|
|
|
|
};
|
|
|
|
|
|
|
|
const onInput = ({ target: { value, name } }: { target: { value: string; name: string } }) => {
|
|
|
|
const isSubmittingMfaToken = name === 'token';
|
|
|
|
const inputValidLength = isSubmittingMfaToken
|
|
|
|
? MFA_AUTHENTICATION_TOKEN_INPUT_MAX_LENGTH
|
|
|
|
: MFA_AUTHENTICATION_RECOVERY_CODE_INPUT_MAX_LENGTH;
|
|
|
|
|
|
|
|
if (value.length !== inputValidLength) {
|
|
|
|
hasAnyChanges.value = false;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
verifyingMfaToken.value = true;
|
|
|
|
hasAnyChanges.value = true;
|
|
|
|
|
|
|
|
const dataToSubmit = isSubmittingMfaToken
|
|
|
|
? { token: value, recoveryCode: '' }
|
|
|
|
: { token: '', recoveryCode: value };
|
|
|
|
|
|
|
|
onSubmit(dataToSubmit)
|
|
|
|
.catch(() => {})
|
|
|
|
.finally(() => (verifyingMfaToken.value = false));
|
|
|
|
};
|
|
|
|
|
|
|
|
const mfaRecoveryCodeFieldWithDefaults = () => {
|
|
|
|
return formField(
|
|
|
|
'recoveryCode',
|
|
|
|
i18.baseText('mfa.recovery.input.label'),
|
|
|
|
i18.baseText('mfa.recovery.input.placeholder'),
|
|
|
|
MFA_AUTHENTICATION_RECOVERY_CODE_INPUT_MAX_LENGTH,
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
const mfaTokenFieldWithDefaults = () => {
|
|
|
|
return formField(
|
|
|
|
'token',
|
|
|
|
i18.baseText('mfa.code.input.label'),
|
|
|
|
i18.baseText('mfa.code.input.placeholder'),
|
|
|
|
MFA_AUTHENTICATION_TOKEN_INPUT_MAX_LENGTH,
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
const onSaveClick = () => {
|
|
|
|
formBus.value.emit('submit');
|
|
|
|
};
|
|
|
|
|
|
|
|
// #endregion
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// #region Lifecycle hooks
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
formInputs.value = [mfaTokenFieldWithDefaults()];
|
2023-08-23 19:59:16 -07:00
|
|
|
});
|
2024-06-17 01:52:15 -07:00
|
|
|
|
|
|
|
// #endregion
|
2023-08-23 19:59:16 -07:00
|
|
|
</script>
|
|
|
|
|
2024-08-24 06:24:08 -07:00
|
|
|
<template>
|
|
|
|
<div :class="$style.container">
|
|
|
|
<div :class="$style.logoContainer">
|
|
|
|
<Logo />
|
|
|
|
</div>
|
|
|
|
<n8n-card>
|
|
|
|
<div :class="$style.headerContainer">
|
|
|
|
<n8n-heading size="xlarge" color="text-dark">{{
|
|
|
|
showRecoveryCodeForm
|
|
|
|
? i18.baseText('mfa.recovery.modal.title')
|
|
|
|
: i18.baseText('mfa.code.modal.title')
|
|
|
|
}}</n8n-heading>
|
|
|
|
</div>
|
|
|
|
<div :class="[$style.formContainer, reportError ? $style.formError : '']">
|
|
|
|
<n8n-form-inputs
|
|
|
|
v-if="formInputs"
|
|
|
|
data-test-id="mfa-login-form"
|
|
|
|
:inputs="formInputs"
|
|
|
|
:event-bus="formBus"
|
|
|
|
@input="onInput"
|
|
|
|
@submit="onSubmit"
|
|
|
|
/>
|
|
|
|
<div :class="$style.infoBox">
|
|
|
|
<n8n-text
|
|
|
|
v-if="!showRecoveryCodeForm && !reportError"
|
|
|
|
size="small"
|
|
|
|
color="text-base"
|
|
|
|
:bold="false"
|
|
|
|
>{{ i18.baseText('mfa.code.input.info') }}
|
|
|
|
<a data-test-id="mfa-enter-recovery-code-button" @click="onRecoveryCodeClick">{{
|
|
|
|
i18.baseText('mfa.code.input.info.action')
|
|
|
|
}}</a></n8n-text
|
|
|
|
>
|
|
|
|
<n8n-text v-if="reportError" color="danger" size="small"
|
|
|
|
>{{ formError }}
|
|
|
|
<a
|
|
|
|
v-if="!showRecoveryCodeForm"
|
|
|
|
:class="$style.recoveryCodeLink"
|
|
|
|
@click="onRecoveryCodeClick"
|
|
|
|
>
|
|
|
|
{{ i18.baseText('mfa.recovery.input.info.action') }}</a
|
|
|
|
>
|
|
|
|
</n8n-text>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div>
|
|
|
|
<n8n-button
|
|
|
|
float="right"
|
|
|
|
:loading="verifyingMfaToken"
|
|
|
|
:label="
|
|
|
|
showRecoveryCodeForm
|
|
|
|
? i18.baseText('mfa.recovery.button.verify')
|
|
|
|
: i18.baseText('mfa.code.button.continue')
|
|
|
|
"
|
|
|
|
size="large"
|
|
|
|
:disabled="!hasAnyChanges"
|
|
|
|
@click="onSaveClick"
|
|
|
|
/>
|
|
|
|
<n8n-button
|
|
|
|
float="left"
|
|
|
|
:label="i18.baseText('mfa.button.back')"
|
|
|
|
size="large"
|
|
|
|
type="tertiary"
|
|
|
|
@click="onBackClick"
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
</n8n-card>
|
|
|
|
</div>
|
|
|
|
</template>
|
|
|
|
|
2023-08-23 19:59:16 -07:00
|
|
|
<style lang="scss" module>
|
|
|
|
body {
|
|
|
|
background-color: var(--color-background-light);
|
|
|
|
}
|
|
|
|
|
|
|
|
.container {
|
|
|
|
display: flex;
|
|
|
|
align-items: center;
|
|
|
|
flex-direction: column;
|
|
|
|
padding-top: var(--spacing-2xl);
|
|
|
|
|
|
|
|
> * {
|
|
|
|
margin-bottom: var(--spacing-l);
|
|
|
|
width: 352px;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.logoContainer {
|
|
|
|
display: flex;
|
|
|
|
justify-content: center;
|
|
|
|
}
|
|
|
|
|
|
|
|
.formContainer {
|
|
|
|
padding-bottom: var(--spacing-xl);
|
|
|
|
}
|
|
|
|
|
|
|
|
.headerContainer {
|
|
|
|
text-align: center;
|
|
|
|
margin-bottom: var(--spacing-xl);
|
|
|
|
}
|
|
|
|
|
|
|
|
.formError input {
|
|
|
|
border-color: var(--color-danger);
|
|
|
|
}
|
|
|
|
|
|
|
|
.recoveryCodeLink {
|
|
|
|
text-decoration: underline;
|
|
|
|
}
|
|
|
|
|
|
|
|
.infoBox {
|
|
|
|
padding-top: var(--spacing-4xs);
|
|
|
|
}
|
|
|
|
</style>
|