123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308 |
- <!-- Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/ -->
- <script setup lang="ts">
- import { computed, ref, reactive } from 'vue'
- import { useRoute, useRouter } from 'vue-router'
- import { useAuthenticationStore } from '#shared/stores/authentication.ts'
- import UserError from '#shared/errors/UserError.ts'
- import Form from '#shared/components/Form/Form.vue'
- import { useApplicationStore } from '#shared/stores/application.ts'
- import type {
- FormSubmitData,
- FormSchemaField,
- FormValues,
- } from '#shared/components/Form/types.ts'
- import { useForm } from '#shared/components/Form/useForm.ts'
- import { useThirdPartyAuthentication } from '#shared/composables/authentication/useThirdPartyAuthentication.ts'
- import CommonLabel from '#shared/components/CommonLabel/CommonLabel.vue'
- import CommonLink from '#shared/components/CommonLink/CommonLink.vue'
- import CommonButton from '#desktop/components/CommonButton/CommonButton.vue'
- import LoginThirdParty from '#desktop/pages/authentication/components/LoginThirdParty.vue'
- import LayoutPublicPage from '#desktop/components/layout/LayoutPublicPage/LayoutPublicPage.vue'
- import CommonPublicLinks from '#desktop/components/CommonPublicLinks/CommonPublicLinks.vue'
- import { EnumPublicLinksScreen } from '#shared/graphql/types.ts'
- import type { LoginCredentials } from '#shared/entities/two-factor/types.ts'
- import useLoginTwoFactor from '#shared/composables/authentication/useLoginTwoFactor.ts'
- import LoginTwoFactor from '../components/LoginTwoFactor.vue'
- import LoginTwoFactorMethods from '../components/LoginTwoFactorMethods.vue'
- import LoginRecoveryCode from '../components/LoginRecoveryCode.vue'
- import { useAdminPasswordAuthVerify } from '../composables/useAdminPasswordAuthVerify.ts'
- import { ensureAfterAuth } from '../after-auth/composable/useAfterAuthPlugins.ts'
- const application = useApplicationStore()
- const router = useRouter()
- const route = useRoute()
- const authentication = useAuthenticationStore()
- const { enabledProviders, hasEnabledProviders } = useThirdPartyAuthentication()
- const passwordLoginErrorMessage = ref('')
- const showError = (error: UserError) => {
- passwordLoginErrorMessage.value = error.generalErrors[0]
- }
- const clearError = () => {
- passwordLoginErrorMessage.value = ''
- }
- const {
- loginFlow,
- askTwoFactor,
- twoFactorPlugin,
- twoFactorAllowedMethods,
- updateState,
- updateSecondFactor,
- hasAlternativeLoginMethod,
- loginPageTitle,
- cancelAndGoBack,
- } = useLoginTwoFactor(clearError)
- const finishLogin = () => {
- const { redirect: redirectUrl } = route.query
- if (typeof redirectUrl === 'string') {
- router.replace(redirectUrl)
- } else {
- router.replace('/')
- }
- }
- const login = async (credentials: LoginCredentials) => {
- try {
- const { twoFactor, afterAuth } = await authentication.login(credentials)
- if (afterAuth) {
- ensureAfterAuth(router, afterAuth)
- return
- }
- if (twoFactor?.defaultTwoFactorAuthenticationMethod) {
- askTwoFactor(twoFactor, credentials)
- return
- }
- finishLogin()
- } catch (error) {
- passwordLoginErrorMessage.value =
- error instanceof UserError ? error.generalErrors[0] : String(error)
- }
- }
- const loginSchema = [
- {
- name: 'login',
- type: 'text',
- label: __('Username / Email'),
- required: true,
- },
- {
- name: 'password',
- label: __('Password'),
- type: 'password',
- required: true,
- },
- {
- isLayout: true,
- element: 'div',
- attrs: {
- class: 'flex grow items-center justify-between',
- },
- children: [
- {
- type: 'checkbox',
- name: 'rememberMe',
- label: __('Remember me'),
- value: false,
- },
- {
- if: '$userLostPassword === true',
- isLayout: true,
- component: 'CommonLink',
- props: {
- class: 'text-right text-sm',
- link: '/reset-password',
- },
- children: __('Forgot password?'),
- },
- ],
- },
- ]
- const userLostPassword = computed(() => application.config.user_lost_password)
- const schemaData = reactive({
- userLostPassword,
- })
- const { form, isDisabled } = useForm()
- const formInitialValues: FormValues = {}
- const formChangeFields = reactive<Record<string, Partial<FormSchemaField>>>({})
- const { verifyTokenResult, verifyTokenMessage, verifyTokenAlertVariant } =
- useAdminPasswordAuthVerify({
- formChangeFields,
- formInitialValues,
- })
- const showPasswordLogin = computed(
- () =>
- application.config.user_show_password_login ||
- !hasEnabledProviders.value ||
- verifyTokenResult?.value,
- )
- </script>
- <template>
- <LayoutPublicPage box-size="small" :title="loginPageTitle" show-logo>
- <div
- v-if="$c.maintenance_mode"
- class="my-1 flex items-center rounded-xl bg-red px-4 py-2 text-white"
- >
- {{
- $t(
- 'Zammad is currently in maintenance mode. Only administrators can log in. Please wait until the maintenance window is over.',
- )
- }}
- </div>
- <!-- eslint-disable vue/no-v-html -->
- <div
- v-if="$c.maintenance_login && $c.maintenance_login_message"
- class="my-1 flex items-center rounded-xl bg-green px-4 py-2 text-white"
- v-html="$c.maintenance_login_message"
- ></div>
- <CommonAlert v-if="verifyTokenMessage" :variant="verifyTokenAlertVariant">{{
- $t(verifyTokenMessage)
- }}</CommonAlert>
- <template v-if="showPasswordLogin">
- <CommonAlert v-if="passwordLoginErrorMessage" variant="danger">{{
- $t(passwordLoginErrorMessage)
- }}</CommonAlert>
- <Form
- v-if="loginFlow.state === 'credentials' && showPasswordLogin"
- id="login"
- ref="form"
- form-class="mb-2.5 space-y-2.5"
- :schema="loginSchema"
- :schema-data="schemaData"
- :initial-values="formInitialValues"
- :change-fields="formChangeFields"
- @submit="login($event as FormSubmitData<LoginCredentials>)"
- >
- <template #after-fields>
- <div v-if="$c.user_create_account" class="flex justify-center py-3">
- <CommonLabel>
- {{ $t('New user?') }}
- <CommonLink link="/signup" class="select-none">{{
- $t('Register')
- }}</CommonLink>
- </CommonLabel>
- </div>
- <CommonButton
- type="submit"
- variant="submit"
- size="large"
- block
- :disabled="isDisabled"
- >
- {{ $t('Sign in') }}
- </CommonButton>
- </template>
- </Form>
- <LoginTwoFactor
- v-else-if="
- loginFlow.state === '2fa' && twoFactorPlugin && loginFlow.credentials
- "
- :credentials="loginFlow.credentials"
- :two-factor="twoFactorPlugin"
- @error="showError"
- @clear-error="clearError"
- @finish="finishLogin"
- />
- <LoginRecoveryCode
- v-else-if="loginFlow.state === 'recovery-code' && loginFlow.credentials"
- :credentials="loginFlow.credentials"
- @error="showError"
- @clear-error="clearError"
- @finish="finishLogin"
- />
- <LoginTwoFactorMethods
- v-else-if="loginFlow.state === '2fa-select'"
- :methods="twoFactorAllowedMethods"
- :default-method="loginFlow.defaultMethod"
- :recovery-codes-available="loginFlow.recoveryCodesAvailable"
- @select="updateSecondFactor"
- @use-recovery-code="updateState('recovery-code')"
- @cancel="cancelAndGoBack()"
- />
- <section
- v-if="
- (loginFlow.state === '2fa' || loginFlow.state === 'recovery-code') &&
- hasAlternativeLoginMethod
- "
- class="mt-3 text-center"
- >
- <CommonLabel>
- {{ $t('Having problems?') }}
- <CommonLink
- link="#"
- class="select-none"
- @click="updateState('2fa-select')"
- >
- {{ $t('Try another method') }}
- </CommonLink>
- </CommonLabel>
- </section>
- </template>
- <LoginThirdParty
- v-if="hasEnabledProviders && loginFlow.state === 'credentials'"
- :providers="enabledProviders"
- />
- <template #bottomContent>
- <div
- v-if="!showPasswordLogin"
- class="p-2 inline-flex items-center justify-center flex-wrap text-sm"
- >
- <CommonLabel class="text-stone-200 dark:text-neutral-500 text-center">
- {{
- $t(
- 'If you have problems with the third-party login you can request a one-time password login as an admin.',
- )
- }}
- </CommonLabel>
- <CommonLink link="/admin-password-auth">{{
- $t('Request the password login here.')
- }}</CommonLink>
- </div>
- <CommonLabel
- v-if="loginFlow.state === '2fa-select'"
- class="text-stone-200 dark:text-neutral-500 mt-3 mb-3"
- >
- {{
- $t('Contact the administrator if you have any problems logging in.')
- }}
- </CommonLabel>
- <!-- TODO: Remember the choice when we have a switch between the two desktop apps -->
- <CommonLink
- v-if="loginFlow.state === 'credentials'"
- class="mt-3 text-sm"
- link="/mobile"
- external
- >
- {{ $t('Continue to mobile') }}
- </CommonLink>
- <CommonPublicLinks :screen="EnumPublicLinksScreen.Login" />
- </template>
- </LayoutPublicPage>
- </template>
|