Login.vue 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  1. <!-- Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/ -->
  2. <script setup lang="ts">
  3. import { computed, ref, reactive } from 'vue'
  4. import { useRoute, useRouter } from 'vue-router'
  5. import { useAuthenticationStore } from '#shared/stores/authentication.ts'
  6. import UserError from '#shared/errors/UserError.ts'
  7. import Form from '#shared/components/Form/Form.vue'
  8. import { useApplicationStore } from '#shared/stores/application.ts'
  9. import type {
  10. FormSubmitData,
  11. FormSchemaField,
  12. FormValues,
  13. } from '#shared/components/Form/types.ts'
  14. import { useForm } from '#shared/components/Form/useForm.ts'
  15. import { useThirdPartyAuthentication } from '#shared/composables/authentication/useThirdPartyAuthentication.ts'
  16. import CommonLabel from '#shared/components/CommonLabel/CommonLabel.vue'
  17. import CommonLink from '#shared/components/CommonLink/CommonLink.vue'
  18. import CommonButton from '#desktop/components/CommonButton/CommonButton.vue'
  19. import LoginThirdParty from '#desktop/pages/authentication/components/LoginThirdParty.vue'
  20. import LayoutPublicPage from '#desktop/components/layout/LayoutPublicPage/LayoutPublicPage.vue'
  21. import CommonPublicLinks from '#desktop/components/CommonPublicLinks/CommonPublicLinks.vue'
  22. import { EnumPublicLinksScreen } from '#shared/graphql/types.ts'
  23. import type { LoginCredentials } from '#shared/entities/two-factor/types.ts'
  24. import useLoginTwoFactor from '#shared/composables/authentication/useLoginTwoFactor.ts'
  25. import LoginTwoFactor from '../components/LoginTwoFactor.vue'
  26. import LoginTwoFactorMethods from '../components/LoginTwoFactorMethods.vue'
  27. import LoginRecoveryCode from '../components/LoginRecoveryCode.vue'
  28. import { useAdminPasswordAuthVerify } from '../composables/useAdminPasswordAuthVerify.ts'
  29. import { ensureAfterAuth } from '../after-auth/composable/useAfterAuthPlugins.ts'
  30. const application = useApplicationStore()
  31. const router = useRouter()
  32. const route = useRoute()
  33. const authentication = useAuthenticationStore()
  34. const { enabledProviders, hasEnabledProviders } = useThirdPartyAuthentication()
  35. const passwordLoginErrorMessage = ref('')
  36. const showError = (error: UserError) => {
  37. passwordLoginErrorMessage.value = error.generalErrors[0]
  38. }
  39. const clearError = () => {
  40. passwordLoginErrorMessage.value = ''
  41. }
  42. const {
  43. loginFlow,
  44. askTwoFactor,
  45. twoFactorPlugin,
  46. twoFactorAllowedMethods,
  47. updateState,
  48. updateSecondFactor,
  49. hasAlternativeLoginMethod,
  50. loginPageTitle,
  51. cancelAndGoBack,
  52. } = useLoginTwoFactor(clearError)
  53. const finishLogin = () => {
  54. const { redirect: redirectUrl } = route.query
  55. if (typeof redirectUrl === 'string') {
  56. router.replace(redirectUrl)
  57. } else {
  58. router.replace('/')
  59. }
  60. }
  61. const login = async (credentials: LoginCredentials) => {
  62. try {
  63. const { twoFactor, afterAuth } = await authentication.login(credentials)
  64. if (afterAuth) {
  65. ensureAfterAuth(router, afterAuth)
  66. return
  67. }
  68. if (twoFactor?.defaultTwoFactorAuthenticationMethod) {
  69. askTwoFactor(twoFactor, credentials)
  70. return
  71. }
  72. finishLogin()
  73. } catch (error) {
  74. passwordLoginErrorMessage.value =
  75. error instanceof UserError ? error.generalErrors[0] : String(error)
  76. }
  77. }
  78. const loginSchema = [
  79. {
  80. name: 'login',
  81. type: 'text',
  82. label: __('Username / Email'),
  83. required: true,
  84. },
  85. {
  86. name: 'password',
  87. label: __('Password'),
  88. type: 'password',
  89. required: true,
  90. },
  91. {
  92. isLayout: true,
  93. element: 'div',
  94. attrs: {
  95. class: 'flex grow items-center justify-between',
  96. },
  97. children: [
  98. {
  99. type: 'checkbox',
  100. name: 'rememberMe',
  101. label: __('Remember me'),
  102. value: false,
  103. },
  104. {
  105. if: '$userLostPassword === true',
  106. isLayout: true,
  107. component: 'CommonLink',
  108. props: {
  109. class: 'text-right text-sm',
  110. link: '/reset-password',
  111. },
  112. children: __('Forgot password?'),
  113. },
  114. ],
  115. },
  116. ]
  117. const userLostPassword = computed(() => application.config.user_lost_password)
  118. const schemaData = reactive({
  119. userLostPassword,
  120. })
  121. const { form, isDisabled } = useForm()
  122. const formInitialValues: FormValues = {}
  123. const formChangeFields = reactive<Record<string, Partial<FormSchemaField>>>({})
  124. const { verifyTokenResult, verifyTokenMessage, verifyTokenAlertVariant } =
  125. useAdminPasswordAuthVerify({
  126. formChangeFields,
  127. formInitialValues,
  128. })
  129. const showPasswordLogin = computed(
  130. () =>
  131. application.config.user_show_password_login ||
  132. !hasEnabledProviders.value ||
  133. verifyTokenResult?.value,
  134. )
  135. </script>
  136. <template>
  137. <LayoutPublicPage box-size="small" :title="loginPageTitle" show-logo>
  138. <div
  139. v-if="$c.maintenance_mode"
  140. class="my-1 flex items-center rounded-xl bg-red px-4 py-2 text-white"
  141. >
  142. {{
  143. $t(
  144. 'Zammad is currently in maintenance mode. Only administrators can log in. Please wait until the maintenance window is over.',
  145. )
  146. }}
  147. </div>
  148. <!-- eslint-disable vue/no-v-html -->
  149. <div
  150. v-if="$c.maintenance_login && $c.maintenance_login_message"
  151. class="my-1 flex items-center rounded-xl bg-green px-4 py-2 text-white"
  152. v-html="$c.maintenance_login_message"
  153. ></div>
  154. <CommonAlert v-if="verifyTokenMessage" :variant="verifyTokenAlertVariant">{{
  155. $t(verifyTokenMessage)
  156. }}</CommonAlert>
  157. <template v-if="showPasswordLogin">
  158. <CommonAlert v-if="passwordLoginErrorMessage" variant="danger">{{
  159. $t(passwordLoginErrorMessage)
  160. }}</CommonAlert>
  161. <Form
  162. v-if="loginFlow.state === 'credentials' && showPasswordLogin"
  163. id="login"
  164. ref="form"
  165. form-class="mb-2.5 space-y-2.5"
  166. :schema="loginSchema"
  167. :schema-data="schemaData"
  168. :initial-values="formInitialValues"
  169. :change-fields="formChangeFields"
  170. @submit="login($event as FormSubmitData<LoginCredentials>)"
  171. >
  172. <template #after-fields>
  173. <div v-if="$c.user_create_account" class="flex justify-center py-3">
  174. <CommonLabel>
  175. {{ $t('New user?') }}
  176. <CommonLink link="/signup" class="select-none">{{
  177. $t('Register')
  178. }}</CommonLink>
  179. </CommonLabel>
  180. </div>
  181. <CommonButton
  182. type="submit"
  183. variant="submit"
  184. size="large"
  185. block
  186. :disabled="isDisabled"
  187. >
  188. {{ $t('Sign in') }}
  189. </CommonButton>
  190. </template>
  191. </Form>
  192. <LoginTwoFactor
  193. v-else-if="
  194. loginFlow.state === '2fa' && twoFactorPlugin && loginFlow.credentials
  195. "
  196. :credentials="loginFlow.credentials"
  197. :two-factor="twoFactorPlugin"
  198. @error="showError"
  199. @clear-error="clearError"
  200. @finish="finishLogin"
  201. />
  202. <LoginRecoveryCode
  203. v-else-if="loginFlow.state === 'recovery-code' && loginFlow.credentials"
  204. :credentials="loginFlow.credentials"
  205. @error="showError"
  206. @clear-error="clearError"
  207. @finish="finishLogin"
  208. />
  209. <LoginTwoFactorMethods
  210. v-else-if="loginFlow.state === '2fa-select'"
  211. :methods="twoFactorAllowedMethods"
  212. :default-method="loginFlow.defaultMethod"
  213. :recovery-codes-available="loginFlow.recoveryCodesAvailable"
  214. @select="updateSecondFactor"
  215. @use-recovery-code="updateState('recovery-code')"
  216. @cancel="cancelAndGoBack()"
  217. />
  218. <section
  219. v-if="
  220. (loginFlow.state === '2fa' || loginFlow.state === 'recovery-code') &&
  221. hasAlternativeLoginMethod
  222. "
  223. class="mt-3 text-center"
  224. >
  225. <CommonLabel>
  226. {{ $t('Having problems?') }}
  227. <CommonLink
  228. link="#"
  229. class="select-none"
  230. @click="updateState('2fa-select')"
  231. >
  232. {{ $t('Try another method') }}
  233. </CommonLink>
  234. </CommonLabel>
  235. </section>
  236. </template>
  237. <LoginThirdParty
  238. v-if="hasEnabledProviders && loginFlow.state === 'credentials'"
  239. :providers="enabledProviders"
  240. />
  241. <template #bottomContent>
  242. <div
  243. v-if="!showPasswordLogin"
  244. class="p-2 inline-flex items-center justify-center flex-wrap text-sm"
  245. >
  246. <CommonLabel class="text-stone-200 dark:text-neutral-500 text-center">
  247. {{
  248. $t(
  249. 'If you have problems with the third-party login you can request a one-time password login as an admin.',
  250. )
  251. }}
  252. </CommonLabel>
  253. <CommonLink link="/admin-password-auth">{{
  254. $t('Request the password login here.')
  255. }}</CommonLink>
  256. </div>
  257. <CommonLabel
  258. v-if="loginFlow.state === '2fa-select'"
  259. class="text-stone-200 dark:text-neutral-500 mt-3 mb-3"
  260. >
  261. {{
  262. $t('Contact the administrator if you have any problems logging in.')
  263. }}
  264. </CommonLabel>
  265. <!-- TODO: Remember the choice when we have a switch between the two desktop apps -->
  266. <CommonLink
  267. v-if="loginFlow.state === 'credentials'"
  268. class="mt-3 text-sm"
  269. link="/mobile"
  270. external
  271. >
  272. {{ $t('Continue to mobile') }}
  273. </CommonLink>
  274. <CommonPublicLinks :screen="EnumPublicLinksScreen.Login" />
  275. </template>
  276. </LayoutPublicPage>
  277. </template>