feat(core): Add MFA (#4767)
https://linear.app/n8n/issue/ADO-947/sync-branch-with-master-and-fix-fe-e2e-tets --------- Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
This commit is contained in:
@@ -1,29 +1,43 @@
|
||||
<template>
|
||||
<AuthView
|
||||
:form="FORM_CONFIG"
|
||||
:formLoading="loading"
|
||||
:with-sso="true"
|
||||
data-test-id="signin-form"
|
||||
@submit="onSubmit"
|
||||
/>
|
||||
<div>
|
||||
<AuthView
|
||||
v-if="!showMfaView"
|
||||
:form="FORM_CONFIG"
|
||||
:formLoading="loading"
|
||||
:with-sso="true"
|
||||
data-test-id="signin-form"
|
||||
@submit="onEmailPasswordSubmitted"
|
||||
/>
|
||||
<MfaView
|
||||
v-if="showMfaView"
|
||||
@submit="onMFASubmitted"
|
||||
@onBackClick="onBackClick"
|
||||
@onFormChanged="onFormChanged"
|
||||
:reportError="reportError"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import AuthView from './AuthView.vue';
|
||||
import MfaView from './MfaView.vue';
|
||||
import { useToast } from '@/composables';
|
||||
|
||||
import type { IFormBoxConfig } from '@/Interface';
|
||||
import { VIEWS } from '@/constants';
|
||||
import { MFA_AUTHENTICATION_REQUIRED_ERROR_CODE, VIEWS } from '@/constants';
|
||||
import { mapStores } from 'pinia';
|
||||
import { useUsersStore } from '@/stores/users.store';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import { useCloudPlanStore, useUIStore } from '@/stores';
|
||||
import { genericHelpers } from '@/mixins/genericHelpers';
|
||||
import { FORM } from './MfaView.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'SigninView',
|
||||
mixins: [genericHelpers],
|
||||
components: {
|
||||
AuthView,
|
||||
MfaView,
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
@@ -34,10 +48,17 @@ export default defineComponent({
|
||||
return {
|
||||
FORM_CONFIG: {} as IFormBoxConfig,
|
||||
loading: false,
|
||||
showMfaView: false,
|
||||
email: '',
|
||||
password: '',
|
||||
reportError: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useUsersStore, useSettingsStore, useUIStore, useCloudPlanStore),
|
||||
userHasMfaEnabled() {
|
||||
return !!this.usersStore.currentUser?.mfaEnabled;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
let emailLabel = this.$locale.baseText('auth.email');
|
||||
@@ -84,31 +105,94 @@ export default defineComponent({
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async onSubmit(values: { [key: string]: string }) {
|
||||
async onMFASubmitted(form: { token?: string; recoveryCode?: string }) {
|
||||
await this.login({
|
||||
email: this.email,
|
||||
password: this.password,
|
||||
token: form.token,
|
||||
recoveryCode: form.recoveryCode,
|
||||
});
|
||||
},
|
||||
async onEmailPasswordSubmitted(form: { email: string; password: string }) {
|
||||
await this.login(form);
|
||||
},
|
||||
async login(form: { email: string; password: string; token?: string; recoveryCode?: string }) {
|
||||
try {
|
||||
this.loading = true;
|
||||
await this.usersStore.loginWithCreds(values as { email: string; password: string });
|
||||
await this.usersStore.loginWithCreds({
|
||||
email: form.email,
|
||||
password: form.password,
|
||||
mfaToken: form.token,
|
||||
mfaRecoveryCode: form.recoveryCode,
|
||||
});
|
||||
this.loading = false;
|
||||
await this.cloudPlanStore.checkForCloudPlanData();
|
||||
await this.uiStore.initBanners();
|
||||
this.clearAllStickyNotifications();
|
||||
this.loading = false;
|
||||
this.checkRecoveryCodesLeft();
|
||||
|
||||
if (typeof this.$route.query.redirect === 'string') {
|
||||
const redirect = decodeURIComponent(this.$route.query.redirect);
|
||||
if (redirect.startsWith('/')) {
|
||||
// protect against phishing
|
||||
void this.$router.push(redirect);
|
||||
this.$telemetry.track('User attempted to login', {
|
||||
result: this.showMfaView ? 'mfa_success' : 'success',
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
if (this.isRedirectSafe()) {
|
||||
const redirect = this.getRedirectQueryParameter();
|
||||
void this.$router.push(redirect);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.$router.push({ name: VIEWS.HOMEPAGE });
|
||||
} catch (error) {
|
||||
this.showError(error, this.$locale.baseText('auth.signin.error'));
|
||||
if (error.errorCode === MFA_AUTHENTICATION_REQUIRED_ERROR_CODE) {
|
||||
this.showMfaView = true;
|
||||
this.cacheCredentials(form);
|
||||
return;
|
||||
}
|
||||
|
||||
this.$telemetry.track('User attempted to login', {
|
||||
result: this.showMfaView ? 'mfa_token_rejected' : 'credentials_error',
|
||||
});
|
||||
|
||||
if (!this.showMfaView) {
|
||||
this.showError(error, this.$locale.baseText('auth.signin.error'));
|
||||
this.loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.reportError = true;
|
||||
}
|
||||
},
|
||||
onBackClick(fromForm: string) {
|
||||
this.reportError = false;
|
||||
if (fromForm === FORM.MFA_TOKEN) {
|
||||
this.showMfaView = false;
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
onFormChanged(toForm: string) {
|
||||
if (toForm === FORM.MFA_RECOVERY_CODE) {
|
||||
this.reportError = false;
|
||||
}
|
||||
},
|
||||
cacheCredentials(form: { email: string; password: string }) {
|
||||
this.email = form.email;
|
||||
this.password = form.password;
|
||||
},
|
||||
checkRecoveryCodesLeft() {
|
||||
if (this.usersStore.currentUser) {
|
||||
const { hasRecoveryCodesLeft, mfaEnabled } = this.usersStore.currentUser;
|
||||
|
||||
if (mfaEnabled && !hasRecoveryCodesLeft) {
|
||||
this.showToast({
|
||||
title: this.$locale.baseText('settings.mfa.toast.noRecoveryCodeLeft.title'),
|
||||
message: this.$locale.baseText('settings.mfa.toast.noRecoveryCodeLeft.message'),
|
||||
type: 'info',
|
||||
duration: 0,
|
||||
dangerouslyUseHTMLString: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user