Login.vue 9.1 KB

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