You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

LoginForm.vue 5.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. <!--
  2. - @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
  3. -
  4. - @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
  5. -
  6. - @license GNU AGPL version 3 or any later version
  7. -
  8. - This program is free software: you can redistribute it and/or modify
  9. - it under the terms of the GNU Affero General Public License as
  10. - published by the Free Software Foundation, either version 3 of the
  11. - License, or (at your option) any later version.
  12. -
  13. - This program is distributed in the hope that it will be useful,
  14. - but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. - GNU Affero General Public License for more details.
  17. -
  18. - You should have received a copy of the GNU Affero General Public License
  19. - along with this program. If not, see <http://www.gnu.org/licenses/>.
  20. -->
  21. <template>
  22. <form ref="loginForm"
  23. class="login-form"
  24. method="post"
  25. name="login"
  26. :action="loginActionUrl"
  27. @submit="submit">
  28. <fieldset class="login-form__fieldset" data-login-form>
  29. <NcNoteCard v-if="apacheAuthFailed"
  30. :title="t('core', 'Server side authentication failed!')"
  31. type="warning">
  32. {{ t('core', 'Please contact your administrator.') }}
  33. </NcNoteCard>
  34. <NcNoteCard v-if="messages.length > 0">
  35. <div v-for="(message, index) in messages"
  36. :key="index">
  37. {{ message }}<br>
  38. </div>
  39. </NcNoteCard>
  40. <NcNoteCard v-if="internalException"
  41. :class="t('core', 'An internal error occurred.')"
  42. type="warning">
  43. {{ t('core', 'Please try again or contact your administrator.') }}
  44. </NcNoteCard>
  45. <div id="message"
  46. class="hidden">
  47. <img class="float-spinner"
  48. alt=""
  49. :src="loadingIcon">
  50. <span id="messageText" />
  51. <!-- the following div ensures that the spinner is always inside the #message div -->
  52. <div style="clear: both;" />
  53. </div>
  54. <h2 class="login-form__headline" data-login-form-headline v-html="headline" />
  55. <NcTextField id="user"
  56. ref="user"
  57. :label="t('core', 'Account name or email')"
  58. :label-visible="true"
  59. name="user"
  60. :value.sync="user"
  61. :class="{shake: invalidPassword}"
  62. autocapitalize="none"
  63. :spellchecking="false"
  64. :autocomplete="autoCompleteAllowed ? 'username' : 'off'"
  65. required
  66. data-login-form-input-user
  67. @change="updateUsername" />
  68. <NcPasswordField id="password"
  69. ref="password"
  70. name="password"
  71. :label-visible="true"
  72. :class="{shake: invalidPassword}"
  73. :value.sync="password"
  74. :spellchecking="false"
  75. autocapitalize="none"
  76. :autocomplete="autoCompleteAllowed ? 'current-password' : 'off'"
  77. :label="t('core', 'Password')"
  78. :helper-text="errorLabel"
  79. :error="isError"
  80. data-login-form-input-password
  81. required />
  82. <LoginButton data-login-form-submit :loading="loading" />
  83. <input v-if="redirectUrl"
  84. type="hidden"
  85. name="redirect_url"
  86. :value="redirectUrl">
  87. <input type="hidden"
  88. name="timezone"
  89. :value="timezone">
  90. <input type="hidden"
  91. name="timezone_offset"
  92. :value="timezoneOffset">
  93. <input type="hidden"
  94. name="requesttoken"
  95. :value="OC.requestToken">
  96. <input v-if="directLogin"
  97. type="hidden"
  98. name="direct"
  99. value="1">
  100. </fieldset>
  101. </form>
  102. </template>
  103. <script>
  104. import { generateUrl, imagePath } from '@nextcloud/router'
  105. import NcPasswordField from '@nextcloud/vue/dist/Components/NcPasswordField.js'
  106. import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
  107. import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js'
  108. import LoginButton from './LoginButton.vue'
  109. export default {
  110. name: 'LoginForm',
  111. components: {
  112. LoginButton,
  113. NcPasswordField,
  114. NcTextField,
  115. NcNoteCard,
  116. },
  117. props: {
  118. username: {
  119. type: String,
  120. default: '',
  121. },
  122. redirectUrl: {
  123. type: [String, Boolean],
  124. default: false,
  125. },
  126. errors: {
  127. type: Array,
  128. default: () => [],
  129. },
  130. messages: {
  131. type: Array,
  132. default: () => [],
  133. },
  134. throttleDelay: {
  135. type: Number,
  136. default: 0,
  137. },
  138. autoCompleteAllowed: {
  139. type: Boolean,
  140. default: true,
  141. },
  142. directLogin: {
  143. type: Boolean,
  144. default: false,
  145. },
  146. },
  147. data() {
  148. return {
  149. loading: false,
  150. timezone: (new Intl.DateTimeFormat())?.resolvedOptions()?.timeZone,
  151. timezoneOffset: (-new Date().getTimezoneOffset() / 60),
  152. headline: t('core', 'Log in to {productName}', { productName: OC.theme.name }),
  153. user: '',
  154. password: '',
  155. }
  156. },
  157. computed: {
  158. isError() {
  159. return this.invalidPassword || this.userDisabled
  160. || this.throttleDelay > 5000
  161. },
  162. errorLabel() {
  163. if (this.invalidPassword) {
  164. return t('core', 'Wrong username or password.')
  165. }
  166. if (this.userDisabled) {
  167. return t('core', 'User disabled')
  168. }
  169. if (this.throttleDelay > 5000) {
  170. return t('core', 'We have detected multiple invalid login attempts from your IP. Therefore your next login is throttled up to 30 seconds.')
  171. }
  172. return undefined
  173. },
  174. apacheAuthFailed() {
  175. return this.errors.indexOf('apacheAuthFailed') !== -1
  176. },
  177. internalException() {
  178. return this.errors.indexOf('internalexception') !== -1
  179. },
  180. invalidPassword() {
  181. return this.errors.indexOf('invalidpassword') !== -1
  182. },
  183. userDisabled() {
  184. return this.errors.indexOf('userdisabled') !== -1
  185. },
  186. loadingIcon() {
  187. return imagePath('core', 'loading-dark.gif')
  188. },
  189. loginActionUrl() {
  190. return generateUrl('login')
  191. },
  192. },
  193. mounted() {
  194. if (this.username === '') {
  195. this.$refs.user.$refs.inputField.$refs.input.focus()
  196. } else {
  197. this.user = this.username
  198. this.$refs.password.$refs.inputField.$refs.input.focus()
  199. }
  200. },
  201. methods: {
  202. updateUsername() {
  203. this.$emit('update:username', this.user)
  204. },
  205. submit() {
  206. this.loading = true
  207. this.$emit('submit')
  208. },
  209. },
  210. }
  211. </script>
  212. <style lang="scss" scoped>
  213. .login-form {
  214. text-align: left;
  215. font-size: 1rem;
  216. &__fieldset {
  217. width: 100%;
  218. display: flex;
  219. flex-direction: column;
  220. gap: .5rem;
  221. }
  222. &__headline {
  223. text-align: center;
  224. }
  225. }
  226. </style>