mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 01:56:46 +00:00
220 lines
5.4 KiB
Vue
220 lines
5.4 KiB
Vue
<script setup lang="ts">
|
|
import { computed, reactive, ref } from 'vue';
|
|
import { useRoute, useRouter } from 'vue-router';
|
|
|
|
import AuthView from './AuthView.vue';
|
|
import MfaView from './MfaView.vue';
|
|
|
|
import { useToast } from '@/composables/useToast';
|
|
import { useI18n } from '@n8n/i18n';
|
|
import { useTelemetry } from '@/composables/useTelemetry';
|
|
|
|
import { useUsersStore } from '@/stores/users.store';
|
|
import { useSettingsStore } from '@/stores/settings.store';
|
|
import { useSSOStore } from '@/stores/sso.store';
|
|
|
|
import type { IFormBoxConfig } from '@/Interface';
|
|
import { MFA_AUTHENTICATION_REQUIRED_ERROR_CODE, VIEWS, MFA_FORM } from '@/constants';
|
|
import type { LoginRequestDto } from '@n8n/api-types';
|
|
|
|
export type EmailOrLdapLoginIdAndPassword = Pick<
|
|
LoginRequestDto,
|
|
'emailOrLdapLoginId' | 'password'
|
|
>;
|
|
|
|
export type MfaCodeOrMfaRecoveryCode = Pick<LoginRequestDto, 'mfaCode' | 'mfaRecoveryCode'>;
|
|
|
|
const usersStore = useUsersStore();
|
|
const settingsStore = useSettingsStore();
|
|
const ssoStore = useSSOStore();
|
|
|
|
const route = useRoute();
|
|
const router = useRouter();
|
|
|
|
const toast = useToast();
|
|
const locale = useI18n();
|
|
const telemetry = useTelemetry();
|
|
|
|
const loading = ref(false);
|
|
const showMfaView = ref(false);
|
|
const emailOrLdapLoginId = ref('');
|
|
const password = ref('');
|
|
const reportError = ref(false);
|
|
|
|
const ldapLoginLabel = computed(() => ssoStore.ldapLoginLabel);
|
|
const isLdapLoginEnabled = computed(() => ssoStore.isLdapLoginEnabled);
|
|
const emailLabel = computed(() => {
|
|
let label = locale.baseText('auth.email');
|
|
if (isLdapLoginEnabled.value && ldapLoginLabel.value) {
|
|
label = ldapLoginLabel.value;
|
|
}
|
|
return label;
|
|
});
|
|
|
|
const formConfig: IFormBoxConfig = reactive({
|
|
title: locale.baseText('auth.signin'),
|
|
buttonText: locale.baseText('auth.signin'),
|
|
redirectText: locale.baseText('forgotPassword'),
|
|
redirectLink: '/forgot-password',
|
|
inputs: [
|
|
{
|
|
name: 'emailOrLdapLoginId',
|
|
properties: {
|
|
label: emailLabel.value,
|
|
type: 'email',
|
|
required: true,
|
|
...(!isLdapLoginEnabled.value && { validationRules: [{ name: 'VALID_EMAIL' }] }),
|
|
showRequiredAsterisk: false,
|
|
validateOnBlur: false,
|
|
autocomplete: 'email',
|
|
capitalize: true,
|
|
focusInitially: true,
|
|
},
|
|
},
|
|
{
|
|
name: 'password',
|
|
properties: {
|
|
label: locale.baseText('auth.password'),
|
|
type: 'password',
|
|
required: true,
|
|
showRequiredAsterisk: false,
|
|
validateOnBlur: false,
|
|
autocomplete: 'current-password',
|
|
capitalize: true,
|
|
},
|
|
},
|
|
],
|
|
});
|
|
|
|
const onMFASubmitted = async (form: MfaCodeOrMfaRecoveryCode) => {
|
|
await login({
|
|
emailOrLdapLoginId: emailOrLdapLoginId.value,
|
|
password: password.value,
|
|
mfaCode: form.mfaCode,
|
|
mfaRecoveryCode: form.mfaRecoveryCode,
|
|
});
|
|
};
|
|
|
|
const onEmailPasswordSubmitted = async (form: EmailOrLdapLoginIdAndPassword) => {
|
|
await login(form);
|
|
};
|
|
|
|
const isRedirectSafe = () => {
|
|
const redirect = getRedirectQueryParameter();
|
|
|
|
// Allow local redirects
|
|
if (redirect.startsWith('/')) {
|
|
return true;
|
|
}
|
|
|
|
try {
|
|
// Only allow origin domain redirects
|
|
const url = new URL(redirect);
|
|
return url.origin === window.location.origin;
|
|
} catch {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
const getRedirectQueryParameter = () => {
|
|
let redirect = '';
|
|
if (typeof route.query?.redirect === 'string') {
|
|
redirect = decodeURIComponent(route.query?.redirect);
|
|
}
|
|
return redirect;
|
|
};
|
|
|
|
const login = async (form: LoginRequestDto) => {
|
|
try {
|
|
loading.value = true;
|
|
await usersStore.loginWithCreds({
|
|
emailOrLdapLoginId: form.emailOrLdapLoginId,
|
|
password: form.password,
|
|
mfaCode: form.mfaCode,
|
|
mfaRecoveryCode: form.mfaRecoveryCode,
|
|
});
|
|
loading.value = false;
|
|
await settingsStore.getSettings();
|
|
|
|
toast.clearAllStickyNotifications();
|
|
|
|
if (settingsStore.isMFAEnforced && !usersStore.currentUser?.mfaAuthenticated) {
|
|
await router.push({ name: VIEWS.PERSONAL_SETTINGS });
|
|
return;
|
|
}
|
|
|
|
telemetry.track('User attempted to login', {
|
|
result: showMfaView.value ? 'mfa_success' : 'success',
|
|
});
|
|
|
|
if (isRedirectSafe()) {
|
|
const redirect = getRedirectQueryParameter();
|
|
if (redirect.startsWith('http')) {
|
|
window.location.href = redirect;
|
|
return;
|
|
}
|
|
|
|
void router.push(redirect);
|
|
return;
|
|
}
|
|
|
|
await router.push({ name: VIEWS.HOMEPAGE });
|
|
} catch (error) {
|
|
if (error.errorCode === MFA_AUTHENTICATION_REQUIRED_ERROR_CODE) {
|
|
showMfaView.value = true;
|
|
cacheCredentials(form);
|
|
return;
|
|
}
|
|
|
|
telemetry.track('User attempted to login', {
|
|
result: showMfaView.value ? 'mfa_token_rejected' : 'credentials_error',
|
|
});
|
|
|
|
if (!showMfaView.value) {
|
|
toast.showError(error, locale.baseText('auth.signin.error'));
|
|
loading.value = false;
|
|
return;
|
|
}
|
|
|
|
reportError.value = true;
|
|
}
|
|
};
|
|
|
|
const onBackClick = (fromForm: string) => {
|
|
reportError.value = false;
|
|
if (fromForm === MFA_FORM.MFA_TOKEN) {
|
|
showMfaView.value = false;
|
|
loading.value = false;
|
|
}
|
|
};
|
|
const onFormChanged = (toForm: string) => {
|
|
if (toForm === MFA_FORM.MFA_RECOVERY_CODE) {
|
|
reportError.value = false;
|
|
}
|
|
};
|
|
const cacheCredentials = (form: EmailOrLdapLoginIdAndPassword) => {
|
|
emailOrLdapLoginId.value = form.emailOrLdapLoginId;
|
|
password.value = form.password;
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<div>
|
|
<AuthView
|
|
v-if="!showMfaView"
|
|
:form="formConfig"
|
|
:form-loading="loading"
|
|
:with-sso="true"
|
|
data-test-id="signin-form"
|
|
@submit="onEmailPasswordSubmitted"
|
|
/>
|
|
<MfaView
|
|
v-if="showMfaView"
|
|
:report-error="reportError"
|
|
@submit="onMFASubmitted"
|
|
@on-back-click="onBackClick"
|
|
@on-form-changed="onFormChanged"
|
|
/>
|
|
</div>
|
|
</template>
|