refactor(editor): Migrate AuthView and associated components to composition api (#10713)
Some checks are pending
Test Master / install-and-build (push) Waiting to run
Test Master / Unit tests (18.x) (push) Blocked by required conditions
Test Master / Unit tests (20.x) (push) Blocked by required conditions
Test Master / Unit tests (22.4) (push) Blocked by required conditions
Test Master / Lint (push) Blocked by required conditions
Test Master / Notify Slack on failure (push) Blocked by required conditions

This commit is contained in:
Milorad FIlipović 2024-09-19 05:38:45 +02:00 committed by GitHub
parent ee7147c6b3
commit 91008b2676
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 577 additions and 572 deletions

View file

@ -1,44 +1,37 @@
<script lang="ts"> <script setup lang="ts">
import { type PropType, defineComponent } from 'vue';
import Logo from '@/components/Logo.vue';
import SSOLogin from '@/components/SSOLogin.vue'; import SSOLogin from '@/components/SSOLogin.vue';
import type { IFormBoxConfig } from '@/Interface'; import type { IFormBoxConfig } from '@/Interface';
export default defineComponent({ withDefaults(
name: 'AuthView', defineProps<{
components: { form: IFormBoxConfig;
Logo, formLoading?: boolean;
SSOLogin, subtitle?: string;
withSso?: boolean;
}>(),
{
formLoading: false,
withSso: false,
}, },
props: { );
form: {
type: Object as PropType<IFormBoxConfig>, const emit = defineEmits<{
}, update: [{ name: string; value: string }];
formLoading: { submit: [values: { [key: string]: string }];
type: Boolean, secondaryClick: [];
default: false, }>();
},
subtitle: { const onUpdate = (e: { name: string; value: string }) => {
type: String, emit('update', e);
}, };
withSso: {
type: Boolean, const onSubmit = (values: { [key: string]: string }) => {
default: false, emit('submit', values);
}, };
},
methods: { const onSecondaryClick = () => {
onUpdate(e: { name: string; value: string }) { emit('secondaryClick');
this.$emit('update', e); };
},
onSubmit(values: { [key: string]: string }) {
this.$emit('submit', values);
},
onSecondaryClick() {
this.$emit('secondaryClick');
},
},
});
</script> </script>
<template> <template>

View file

@ -1,48 +1,111 @@
<script lang="ts"> <script setup lang="ts">
import AuthView from '@/views/AuthView.vue'; import { onMounted, ref } from 'vue';
import { useToast } from '@/composables/useToast'; import { useRouter } from 'vue-router';
import { defineComponent } from 'vue'; import AuthView from '@/views/AuthView.vue';
import type { IFormBoxConfig } from '@/Interface';
import { MFA_AUTHENTICATION_TOKEN_INPUT_MAX_LENGTH, VIEWS } from '@/constants'; import { useI18n } from '@/composables/useI18n';
import { mapStores } from 'pinia'; import { useToast } from '@/composables/useToast';
import { useUsersStore } from '@/stores/users.store'; import { useUsersStore } from '@/stores/users.store';
export default defineComponent({ import type { IFormBoxConfig } from '@/Interface';
name: 'ChangePasswordView', import { MFA_AUTHENTICATION_TOKEN_INPUT_MAX_LENGTH, VIEWS } from '@/constants';
components: {
AuthView, const usersStore = useUsersStore();
},
setup() { const locale = useI18n();
const toast = useToast();
const router = useRouter();
const password = ref('');
const loading = ref(false);
const config = ref<IFormBoxConfig | null>(null);
const passwordsMatch = (value: string | number | boolean | null | undefined) => {
if (typeof value !== 'string') {
return false;
}
if (value !== password.value) {
return { return {
...useToast(), messageKey: 'auth.changePassword.passwordsMustMatchError',
}; };
}, }
data() {
return { return false;
password: '',
loading: false,
config: null as null | IFormBoxConfig,
}; };
},
computed: { const getResetToken = () => {
...mapStores(useUsersStore), return !router.currentRoute.value.query.token ||
}, typeof router.currentRoute.value.query.token !== 'string'
async mounted() { ? null
: router.currentRoute.value.query.token;
};
const getMfaEnabled = () => {
if (!router.currentRoute.value.query.mfaEnabled) return null;
return router.currentRoute.value.query.mfaEnabled === 'true' ? true : false;
};
const isFormWithMFAToken = (values: { [key: string]: string }): values is { mfaToken: string } => {
return 'mfaToken' in values;
};
const onSubmit = async (values: { [key: string]: string }) => {
if (!isFormWithMFAToken(values)) return;
try {
loading.value = true;
const token = getResetToken();
if (token) {
const changePasswordParameters = {
token,
password: password.value,
...(values.mfaToken && { mfaToken: values.mfaToken }),
};
await usersStore.changePassword(changePasswordParameters);
toast.showMessage({
type: 'success',
title: locale.baseText('auth.changePassword.passwordUpdated'),
message: locale.baseText('auth.changePassword.passwordUpdatedMessage'),
});
await router.push({ name: VIEWS.SIGNIN });
} else {
toast.showError(
new Error(locale.baseText('auth.validation.missingParameters')),
locale.baseText('auth.changePassword.error'),
);
}
} catch (error) {
toast.showError(error, locale.baseText('auth.changePassword.error'));
}
loading.value = false;
};
const onInput = (e: { name: string; value: string }) => {
if (e.name === 'password') {
password.value = e.value;
}
};
onMounted(async () => {
const form: IFormBoxConfig = { const form: IFormBoxConfig = {
title: this.$locale.baseText('auth.changePassword'), title: locale.baseText('auth.changePassword'),
buttonText: this.$locale.baseText('auth.changePassword'), buttonText: locale.baseText('auth.changePassword'),
redirectText: this.$locale.baseText('auth.signin'), redirectText: locale.baseText('auth.signin'),
redirectLink: '/signin', redirectLink: '/signin',
inputs: [ inputs: [
{ {
name: 'password', name: 'password',
properties: { properties: {
label: this.$locale.baseText('auth.newPassword'), label: locale.baseText('auth.newPassword'),
type: 'password', type: 'password',
required: true, required: true,
validationRules: [{ name: 'DEFAULT_PASSWORD_RULES' }], validationRules: [{ name: 'DEFAULT_PASSWORD_RULES' }],
infoText: this.$locale.baseText('auth.defaultPasswordRequirements'), infoText: locale.baseText('auth.defaultPasswordRequirements'),
autocomplete: 'new-password', autocomplete: 'new-password',
capitalize: true, capitalize: true,
}, },
@ -50,12 +113,12 @@ export default defineComponent({
{ {
name: 'password2', name: 'password2',
properties: { properties: {
label: this.$locale.baseText('auth.changePassword.reenterNewPassword'), label: locale.baseText('auth.changePassword.reenterNewPassword'),
type: 'password', type: 'password',
required: true, required: true,
validators: { validators: {
TWO_PASSWORDS_MATCH: { TWO_PASSWORDS_MATCH: {
validate: this.passwordsMatch, validate: passwordsMatch,
}, },
}, },
validationRules: [{ name: 'TWO_PASSWORDS_MATCH' }], validationRules: [{ name: 'TWO_PASSWORDS_MATCH' }],
@ -66,8 +129,8 @@ export default defineComponent({
], ],
}; };
const token = this.getResetToken(); const token = getResetToken();
const mfaEnabled = this.getMfaEnabled(); const mfaEnabled = getMfaEnabled();
if (mfaEnabled) { if (mfaEnabled) {
form.inputs.push({ form.inputs.push({
@ -75,8 +138,8 @@ export default defineComponent({
initialValue: '', initialValue: '',
properties: { properties: {
required: true, required: true,
label: this.$locale.baseText('mfa.code.input.label'), label: locale.baseText('mfa.code.input.label'),
placeholder: this.$locale.baseText('mfa.code.input.placeholder'), placeholder: locale.baseText('mfa.code.input.placeholder'),
maxlength: MFA_AUTHENTICATION_TOKEN_INPUT_MAX_LENGTH, maxlength: MFA_AUTHENTICATION_TOKEN_INPUT_MAX_LENGTH,
capitalize: true, capitalize: true,
validateOnBlur: true, validateOnBlur: true,
@ -84,80 +147,18 @@ export default defineComponent({
}); });
} }
this.config = form; config.value = form;
try { try {
if (!token) { if (!token) {
throw new Error(this.$locale.baseText('auth.changePassword.missingTokenError')); throw new Error(locale.baseText('auth.changePassword.missingTokenError'));
} }
await this.usersStore.validatePasswordToken({ token }); await usersStore.validatePasswordToken({ token });
} catch (e) { } catch (e) {
this.showError(e, this.$locale.baseText('auth.changePassword.tokenValidationError')); toast.showError(e, locale.baseText('auth.changePassword.tokenValidationError'));
void this.$router.replace({ name: VIEWS.SIGNIN }); void router.replace({ name: VIEWS.SIGNIN });
} }
},
methods: {
passwordsMatch(value: string | number | boolean | null | undefined) {
if (typeof value !== 'string') {
return false;
}
if (value !== this.password) {
return {
messageKey: 'auth.changePassword.passwordsMustMatchError',
};
}
return false;
},
onInput(e: { name: string; value: string }) {
if (e.name === 'password') {
this.password = e.value;
}
},
getResetToken() {
return !this.$route.query.token || typeof this.$route.query.token !== 'string'
? null
: this.$route.query.token;
},
getMfaEnabled() {
if (!this.$route.query.mfaEnabled) return null;
return this.$route.query.mfaEnabled === 'true' ? true : false;
},
async onSubmit(values: { mfaToken: string }) {
try {
this.loading = true;
const token = this.getResetToken();
if (token) {
const changePasswordParameters = {
token,
password: this.password,
...(values.mfaToken && { mfaToken: values.mfaToken }),
};
await this.usersStore.changePassword(changePasswordParameters);
this.showMessage({
type: 'success',
title: this.$locale.baseText('auth.changePassword.passwordUpdated'),
message: this.$locale.baseText('auth.changePassword.passwordUpdatedMessage'),
});
await this.$router.push({ name: VIEWS.SIGNIN });
} else {
this.showError(
new Error(this.$locale.baseText('auth.validation.missingParameters')),
this.$locale.baseText('auth.changePassword.error'),
);
}
} catch (error) {
this.showError(error, this.$locale.baseText('auth.changePassword.error'));
}
this.loading = false;
},
},
}); });
</script> </script>

View file

@ -1,36 +1,26 @@
<script lang="ts"> <script setup lang="ts">
import AuthView from './AuthView.vue'; import AuthView from './AuthView.vue';
import { useToast } from '@/composables/useToast';
import { defineComponent } from 'vue';
import type { IFormBoxConfig } from '@/Interface'; import type { IFormBoxConfig } from '@/Interface';
import { mapStores } from 'pinia'; import { useI18n } from '@/composables/useI18n';
import { useToast } from '@/composables/useToast';
import { useSettingsStore } from '@/stores/settings.store'; import { useSettingsStore } from '@/stores/settings.store';
import { useUsersStore } from '@/stores/users.store'; import { useUsersStore } from '@/stores/users.store';
import { computed, ref } from 'vue';
export default defineComponent({ const settingsStore = useSettingsStore();
name: 'ForgotMyPasswordView', const usersStore = useUsersStore();
components: {
AuthView, const toast = useToast();
}, const locale = useI18n();
setup() {
return { const loading = ref(false);
...useToast(),
}; const formConfig = computed(() => {
},
data() {
return {
loading: false,
};
},
computed: {
...mapStores(useSettingsStore, useUsersStore),
formConfig(): IFormBoxConfig {
const EMAIL_INPUTS: IFormBoxConfig['inputs'] = [ const EMAIL_INPUTS: IFormBoxConfig['inputs'] = [
{ {
name: 'email', name: 'email',
properties: { properties: {
label: this.$locale.baseText('auth.email'), label: locale.baseText('auth.email'),
type: 'email', type: 'email',
required: true, required: true,
validationRules: [{ name: 'VALID_EMAIL' }], validationRules: [{ name: 'VALID_EMAIL' }],
@ -44,22 +34,22 @@ export default defineComponent({
{ {
name: 'no-smtp-warning', name: 'no-smtp-warning',
properties: { properties: {
label: this.$locale.baseText('forgotPassword.noSMTPToSendEmailWarning'), label: locale.baseText('forgotPassword.noSMTPToSendEmailWarning'),
type: 'info', type: 'info',
}, },
}, },
]; ];
const DEFAULT_FORM_CONFIG = { const DEFAULT_FORM_CONFIG = {
title: this.$locale.baseText('forgotPassword.recoverPassword'), title: locale.baseText('forgotPassword.recoverPassword'),
redirectText: this.$locale.baseText('forgotPassword.returnToSignIn'), redirectText: locale.baseText('forgotPassword.returnToSignIn'),
redirectLink: '/signin', redirectLink: '/signin',
}; };
if (this.settingsStore.isSmtpSetup) { if (settingsStore.isSmtpSetup) {
return { return {
...DEFAULT_FORM_CONFIG, ...DEFAULT_FORM_CONFIG,
buttonText: this.$locale.baseText('forgotPassword.getRecoveryLink'), buttonText: locale.baseText('forgotPassword.getRecoveryLink'),
inputs: EMAIL_INPUTS, inputs: EMAIL_INPUTS,
}; };
} }
@ -67,42 +57,46 @@ export default defineComponent({
...DEFAULT_FORM_CONFIG, ...DEFAULT_FORM_CONFIG,
inputs: NO_SMTP_INPUTS, inputs: NO_SMTP_INPUTS,
}; };
}, });
},
methods: {
async onSubmit(values: { email: string }) {
try {
this.loading = true;
await this.usersStore.sendForgotPasswordEmail(values);
this.showMessage({ const isFormWithEmail = (values: { [key: string]: string }): values is { email: string } => {
return 'email' in values;
};
const onSubmit = async (values: { [key: string]: string }) => {
if (!isFormWithEmail(values)) {
return;
}
try {
loading.value = true;
await usersStore.sendForgotPasswordEmail(values);
toast.showMessage({
type: 'success', type: 'success',
title: this.$locale.baseText('forgotPassword.recoveryEmailSent'), title: locale.baseText('forgotPassword.recoveryEmailSent'),
message: this.$locale.baseText('forgotPassword.emailSentIfExists', { message: locale.baseText('forgotPassword.emailSentIfExists', {
interpolate: { email: values.email }, interpolate: { email: values.email },
}), }),
}); });
} catch (error) { } catch (error) {
let message = this.$locale.baseText('forgotPassword.smtpErrorContactAdministrator'); let message = locale.baseText('forgotPassword.smtpErrorContactAdministrator');
if (error.httpStatusCode) { if (error.httpStatusCode) {
const { httpStatusCode: status } = error; const { httpStatusCode: status } = error;
if (status === 429) { if (status === 429) {
message = this.$locale.baseText('forgotPassword.tooManyRequests'); message = locale.baseText('forgotPassword.tooManyRequests');
} else if (error.httpStatusCode === 422) { } else if (error.httpStatusCode === 422) {
message = this.$locale.baseText(error.message); message = locale.baseText(error.message);
} }
this.showMessage({ toast.showMessage({
type: 'error', type: 'error',
title: this.$locale.baseText('forgotPassword.sendingEmailError'), title: locale.baseText('forgotPassword.sendingEmailError'),
message, message,
}); });
} }
} }
this.loading = false; loading.value = false;
}, };
},
});
</script> </script>
<template> <template>

View file

@ -1,14 +1,17 @@
<script lang="ts" setup> <script lang="ts" setup>
import { reactive, ref } from 'vue'; import { reactive, ref } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { ElNotification as Notification } from 'element-plus';
import type { IFormBoxConfig } from 'n8n-design-system'; import type { IFormBoxConfig } from 'n8n-design-system';
import AuthView from '@/views/AuthView.vue'; import AuthView from '@/views/AuthView.vue';
import { i18n as locale } from '@/plugins/i18n';
import { useSSOStore } from '@/stores/sso.store';
import { VIEWS } from '@/constants'; import { VIEWS } from '@/constants';
import { useI18n } from '@/composables/useI18n';
import { useToast } from '@/composables/useToast';
import { useSSOStore } from '@/stores/sso.store';
const router = useRouter(); const router = useRouter();
const locale = useI18n();
const toast = useToast();
const ssoStore = useSSOStore(); const ssoStore = useSSOStore();
const loading = ref(false); const loading = ref(false);
@ -40,18 +43,22 @@ const FORM_CONFIG: IFormBoxConfig = reactive({
}, },
], ],
}); });
const onSubmit = async (values: { firstName: string; lastName: string }) => {
const isFormWithFirstAndLastName = (values: {
[key: string]: string;
}): values is { firstName: string; lastName: string } => {
return 'firstName' in values && 'lastName' in values;
};
const onSubmit = async (values: { [key: string]: string }) => {
if (!isFormWithFirstAndLastName(values)) return;
try { try {
loading.value = true; loading.value = true;
await ssoStore.updateUser(values); await ssoStore.updateUser(values);
await router.push({ name: VIEWS.HOMEPAGE }); await router.push({ name: VIEWS.HOMEPAGE });
} catch (error) { } catch (error) {
loading.value = false; loading.value = false;
Notification.error({ toast.showError(error, 'Error', error.message);
title: 'Error',
message: error.message,
position: 'bottom-right',
});
} }
}; };
</script> </script>

View file

@ -1,33 +1,38 @@
<script lang="ts"> <script setup lang="ts">
import AuthView from './AuthView.vue'; import { reactive, ref } from 'vue';
import { defineComponent } from 'vue'; import { useRouter } from 'vue-router';
import { useToast } from '@/composables/useToast'; import { useToast } from '@/composables/useToast';
import { useI18n } from '@/composables/useI18n';
import { usePostHog } from '@/stores/posthog.store';
import { useSettingsStore } from '@/stores/settings.store';
import { useUIStore } from '@/stores/ui.store';
import { useUsersStore } from '@/stores/users.store';
import type { IFormBoxConfig } from '@/Interface'; import type { IFormBoxConfig } from '@/Interface';
import { MORE_ONBOARDING_OPTIONS_EXPERIMENT, VIEWS } from '@/constants'; import { MORE_ONBOARDING_OPTIONS_EXPERIMENT, VIEWS } from '@/constants';
import { mapStores } from 'pinia';
import { useUIStore } from '@/stores/ui.store';
import { useSettingsStore } from '@/stores/settings.store';
import { useUsersStore } from '@/stores/users.store';
import { usePostHog } from '@/stores/posthog.store';
export default defineComponent({ import AuthView from '@/views/AuthView.vue';
name: 'SetupView',
components: { const posthogStore = usePostHog();
AuthView, const settingsStore = useSettingsStore();
}, const uiStore = useUIStore();
setup() { const usersStore = useUsersStore();
return useToast();
}, const toast = useToast();
data() { const locale = useI18n();
const FORM_CONFIG: IFormBoxConfig = { const router = useRouter();
title: this.$locale.baseText('auth.setup.setupOwner'),
buttonText: this.$locale.baseText('auth.setup.next'), const loading = ref(false);
const formConfig: IFormBoxConfig = reactive({
title: locale.baseText('auth.setup.setupOwner'),
buttonText: locale.baseText('auth.setup.next'),
inputs: [ inputs: [
{ {
name: 'email', name: 'email',
properties: { properties: {
label: this.$locale.baseText('auth.email'), label: locale.baseText('auth.email'),
type: 'email', type: 'email',
required: true, required: true,
validationRules: [{ name: 'VALID_EMAIL' }], validationRules: [{ name: 'VALID_EMAIL' }],
@ -38,7 +43,7 @@ export default defineComponent({
{ {
name: 'firstName', name: 'firstName',
properties: { properties: {
label: this.$locale.baseText('auth.firstName'), label: locale.baseText('auth.firstName'),
maxlength: 32, maxlength: 32,
required: true, required: true,
autocomplete: 'given-name', autocomplete: 'given-name',
@ -48,7 +53,7 @@ export default defineComponent({
{ {
name: 'lastName', name: 'lastName',
properties: { properties: {
label: this.$locale.baseText('auth.lastName'), label: locale.baseText('auth.lastName'),
maxlength: 32, maxlength: 32,
required: true, required: true,
autocomplete: 'family-name', autocomplete: 'family-name',
@ -58,11 +63,11 @@ export default defineComponent({
{ {
name: 'password', name: 'password',
properties: { properties: {
label: this.$locale.baseText('auth.password'), label: locale.baseText('auth.password'),
type: 'password', type: 'password',
required: true, required: true,
validationRules: [{ name: 'DEFAULT_PASSWORD_RULES' }], validationRules: [{ name: 'DEFAULT_PASSWORD_RULES' }],
infoText: this.$locale.baseText('auth.defaultPasswordRequirements'), infoText: locale.baseText('auth.defaultPasswordRequirements'),
autocomplete: 'new-password', autocomplete: 'new-password',
capitalize: true, capitalize: true,
}, },
@ -70,60 +75,49 @@ export default defineComponent({
{ {
name: 'agree', name: 'agree',
properties: { properties: {
label: this.$locale.baseText('auth.agreement.label'), label: locale.baseText('auth.agreement.label'),
type: 'checkbox', type: 'checkbox',
}, },
}, },
], ],
}; });
return { const onSubmit = async (values: { [key: string]: string | boolean }) => {
FORM_CONFIG,
loading: false,
};
},
computed: {
...mapStores(useSettingsStore, useUIStore, useUsersStore, usePostHog),
},
methods: {
async onSubmit(values: { [key: string]: string | boolean }) {
try { try {
const forceRedirectedHere = this.settingsStore.showSetupPage; const forceRedirectedHere = settingsStore.showSetupPage;
const isPartOfOnboardingExperiment = const isPartOfOnboardingExperiment =
this.posthogStore.getVariant(MORE_ONBOARDING_OPTIONS_EXPERIMENT.name) === posthogStore.getVariant(MORE_ONBOARDING_OPTIONS_EXPERIMENT.name) ===
MORE_ONBOARDING_OPTIONS_EXPERIMENT.variant; MORE_ONBOARDING_OPTIONS_EXPERIMENT.variant;
this.loading = true; loading.value = true;
await this.usersStore.createOwner( await usersStore.createOwner(
values as { firstName: string; lastName: string; email: string; password: string }, values as { firstName: string; lastName: string; email: string; password: string },
); );
if (values.agree === true) { if (values.agree === true) {
try { try {
await this.uiStore.submitContactEmail(values.email.toString(), values.agree); await uiStore.submitContactEmail(values.email.toString(), values.agree);
} catch {} } catch {}
} }
if (forceRedirectedHere) { if (forceRedirectedHere) {
if (isPartOfOnboardingExperiment) { if (isPartOfOnboardingExperiment) {
await this.$router.push({ name: VIEWS.WORKFLOWS }); await router.push({ name: VIEWS.WORKFLOWS });
} else { } else {
await this.$router.push({ name: VIEWS.NEW_WORKFLOW }); await router.push({ name: VIEWS.NEW_WORKFLOW });
} }
} else { } else {
await this.$router.push({ name: VIEWS.USERS_SETTINGS }); await router.push({ name: VIEWS.USERS_SETTINGS });
} }
} catch (error) { } catch (error) {
this.showError(error, this.$locale.baseText('auth.setup.settingUpOwnerError')); toast.showError(error, locale.baseText('auth.setup.settingUpOwnerError'));
} }
this.loading = false; loading.value = false;
}, };
},
});
</script> </script>
<template> <template>
<AuthView <AuthView
:form="FORM_CONFIG" :form="formConfig"
:form-loading="loading" :form-loading="loading"
data-test-id="setup-form" data-test-id="setup-form"
@submit="onSubmit" @submit="onSubmit"

View file

@ -1,62 +1,68 @@
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue'; import { computed, reactive, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import AuthView from './AuthView.vue'; import AuthView from './AuthView.vue';
import MfaView from './MfaView.vue'; import MfaView from './MfaView.vue';
import { useToast } from '@/composables/useToast'; import { useToast } from '@/composables/useToast';
import type { IFormBoxConfig } from '@/Interface'; import { useI18n } from '@/composables/useI18n';
import { MFA_AUTHENTICATION_REQUIRED_ERROR_CODE, VIEWS, MFA_FORM } from '@/constants'; import { useTelemetry } from '@/composables/useTelemetry';
import { mapStores } from 'pinia';
import { useUsersStore } from '@/stores/users.store'; import { useUsersStore } from '@/stores/users.store';
import { useSettingsStore } from '@/stores/settings.store'; import { useSettingsStore } from '@/stores/settings.store';
import { useCloudPlanStore } from '@/stores/cloudPlan.store'; import { useCloudPlanStore } from '@/stores/cloudPlan.store';
import { useUIStore } from '@/stores/ui.store';
export default defineComponent({ import type { IFormBoxConfig } from '@/Interface';
name: 'SigninView', import { MFA_AUTHENTICATION_REQUIRED_ERROR_CODE, VIEWS, MFA_FORM } from '@/constants';
components: {
AuthView, const usersStore = useUsersStore();
MfaView, const settingsStore = useSettingsStore();
}, const cloudPlanStore = useCloudPlanStore();
setup() {
return { const route = useRoute();
...useToast(), const router = useRouter();
};
}, const toast = useToast();
data() { const locale = useI18n();
return { const telemetry = useTelemetry();
FORM_CONFIG: {} as IFormBoxConfig,
loading: false, const loading = ref(false);
showMfaView: false, const showMfaView = ref(false);
email: '', const email = ref('');
password: '', const password = ref('');
reportError: false, const reportError = ref(false);
};
}, const ldapLoginLabel = computed(() => settingsStore.ldapLoginLabel);
computed: { const isLdapLoginEnabled = computed(() => settingsStore.isLdapLoginEnabled);
...mapStores(useUsersStore, useSettingsStore, useUIStore, useCloudPlanStore), const emailLabel = computed(() => {
userHasMfaEnabled() { let label = locale.baseText('auth.email');
return !!this.usersStore.currentUser?.mfaEnabled; if (isLdapLoginEnabled.value && ldapLoginLabel.value) {
}, label = ldapLoginLabel.value;
},
mounted() {
let emailLabel = this.$locale.baseText('auth.email');
const ldapLoginLabel = this.settingsStore.ldapLoginLabel;
const isLdapLoginEnabled = this.settingsStore.isLdapLoginEnabled;
if (isLdapLoginEnabled && ldapLoginLabel) {
emailLabel = ldapLoginLabel;
} }
this.FORM_CONFIG = { return label;
title: this.$locale.baseText('auth.signin'), });
buttonText: this.$locale.baseText('auth.signin'),
redirectText: this.$locale.baseText('forgotPassword'), const redirectLink = computed(() => {
if (!settingsStore.isDesktopDeployment) {
return '/forgot-password';
}
return undefined;
});
const formConfig: IFormBoxConfig = reactive({
title: locale.baseText('auth.signin'),
buttonText: locale.baseText('auth.signin'),
redirectText: locale.baseText('forgotPassword'),
redirectLink: redirectLink.value,
inputs: [ inputs: [
{ {
name: 'email', name: 'email',
properties: { properties: {
label: emailLabel, label: emailLabel.value,
type: 'email', type: 'email',
required: true, required: true,
...(!isLdapLoginEnabled && { validationRules: [{ name: 'VALID_EMAIL' }] }), ...(!isLdapLoginEnabled.value && { validationRules: [{ name: 'VALID_EMAIL' }] }),
showRequiredAsterisk: false, showRequiredAsterisk: false,
validateOnBlur: false, validateOnBlur: false,
autocomplete: 'email', autocomplete: 'email',
@ -66,7 +72,7 @@ export default defineComponent({
{ {
name: 'password', name: 'password',
properties: { properties: {
label: this.$locale.baseText('auth.password'), label: locale.baseText('auth.password'),
type: 'password', type: 'password',
required: true, required: true,
showRequiredAsterisk: false, showRequiredAsterisk: false,
@ -76,116 +82,126 @@ export default defineComponent({
}, },
}, },
], ],
}; });
if (!this.settingsStore.isDesktopDeployment) { const onMFASubmitted = async (form: { token?: string; recoveryCode?: string }) => {
this.FORM_CONFIG.redirectLink = '/forgot-password'; await login({
} email: email.value,
}, password: password.value,
methods: {
async onMFASubmitted(form: { token?: string; recoveryCode?: string }) {
await this.login({
email: this.email,
password: this.password,
token: form.token, token: form.token,
recoveryCode: form.recoveryCode, recoveryCode: form.recoveryCode,
}); });
}, };
async onEmailPasswordSubmitted(form: { email: string; password: string }) {
await this.login(form); const isFormWithEmailAndPassword = (values: {
}, [key: string]: string;
isRedirectSafe() { }): values is { email: string; password: string } => {
const redirect = this.getRedirectQueryParameter(); return 'email' in values && 'password' in values;
};
const onEmailPasswordSubmitted = async (form: { [key: string]: string }) => {
if (!isFormWithEmailAndPassword(form)) return;
await login(form);
};
const isRedirectSafe = () => {
const redirect = getRedirectQueryParameter();
return redirect.startsWith('/') || redirect.startsWith(window.location.origin); return redirect.startsWith('/') || redirect.startsWith(window.location.origin);
}, };
getRedirectQueryParameter() {
const getRedirectQueryParameter = () => {
let redirect = ''; let redirect = '';
if (typeof this.$route.query?.redirect === 'string') { if (typeof route.query?.redirect === 'string') {
redirect = decodeURIComponent(this.$route.query?.redirect); redirect = decodeURIComponent(route.query?.redirect);
} }
return redirect; return redirect;
}, };
async login(form: { email: string; password: string; token?: string; recoveryCode?: string }) {
const login = async (form: {
email: string;
password: string;
token?: string;
recoveryCode?: string;
}) => {
try { try {
this.loading = true; loading.value = true;
await this.usersStore.loginWithCreds({ await usersStore.loginWithCreds({
email: form.email, email: form.email,
password: form.password, password: form.password,
mfaToken: form.token, mfaToken: form.token,
mfaRecoveryCode: form.recoveryCode, mfaRecoveryCode: form.recoveryCode,
}); });
this.loading = false; loading.value = false;
if (this.settingsStore.isCloudDeployment) { if (settingsStore.isCloudDeployment) {
try { try {
await this.cloudPlanStore.checkForCloudPlanData(); await cloudPlanStore.checkForCloudPlanData();
} catch (error) { } catch (error) {
console.warn('Failed to check for cloud plan data', error); console.warn('Failed to check for cloud plan data', error);
} }
} }
await this.settingsStore.getSettings(); await settingsStore.getSettings();
this.clearAllStickyNotifications(); toast.clearAllStickyNotifications();
this.$telemetry.track('User attempted to login', { telemetry.track('User attempted to login', {
result: this.showMfaView ? 'mfa_success' : 'success', result: showMfaView.value ? 'mfa_success' : 'success',
}); });
if (this.isRedirectSafe()) { if (isRedirectSafe()) {
const redirect = this.getRedirectQueryParameter(); const redirect = getRedirectQueryParameter();
if (redirect.startsWith('http')) { if (redirect.startsWith('http')) {
window.location.href = redirect; window.location.href = redirect;
return; return;
} }
void this.$router.push(redirect); void router.push(redirect);
return; return;
} }
await this.$router.push({ name: VIEWS.HOMEPAGE }); await router.push({ name: VIEWS.HOMEPAGE });
} catch (error) { } catch (error) {
if (error.errorCode === MFA_AUTHENTICATION_REQUIRED_ERROR_CODE) { if (error.errorCode === MFA_AUTHENTICATION_REQUIRED_ERROR_CODE) {
this.showMfaView = true; showMfaView.value = true;
this.cacheCredentials(form); cacheCredentials(form);
return; return;
} }
this.$telemetry.track('User attempted to login', { telemetry.track('User attempted to login', {
result: this.showMfaView ? 'mfa_token_rejected' : 'credentials_error', result: showMfaView.value ? 'mfa_token_rejected' : 'credentials_error',
}); });
if (!this.showMfaView) { if (!showMfaView.value) {
this.showError(error, this.$locale.baseText('auth.signin.error')); toast.showError(error, locale.baseText('auth.signin.error'));
this.loading = false; loading.value = false;
return; return;
} }
this.reportError = true; reportError.value = true;
} }
}, };
onBackClick(fromForm: string) {
this.reportError = false; const onBackClick = (fromForm: string) => {
reportError.value = false;
if (fromForm === MFA_FORM.MFA_TOKEN) { if (fromForm === MFA_FORM.MFA_TOKEN) {
this.showMfaView = false; showMfaView.value = false;
this.loading = false; loading.value = false;
} }
}, };
onFormChanged(toForm: string) { const onFormChanged = (toForm: string) => {
if (toForm === MFA_FORM.MFA_RECOVERY_CODE) { if (toForm === MFA_FORM.MFA_RECOVERY_CODE) {
this.reportError = false; reportError.value = false;
} }
}, };
cacheCredentials(form: { email: string; password: string }) { const cacheCredentials = (form: { email: string; password: string }) => {
this.email = form.email; email.value = form.email;
this.password = form.password; password.value = form.password;
}, };
},
});
</script> </script>
<template> <template>
<div> <div>
<AuthView <AuthView
v-if="!showMfaView" v-if="!showMfaView"
:form="FORM_CONFIG" :form="formConfig"
:form-loading="loading" :form-loading="loading"
:with-sso="true" :with-sso="true"
data-test-id="signin-form" data-test-id="signin-form"