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.

407 lines
13 KiB

  1. <template lang="pug">
  2. v-app
  3. .login
  4. v-container(grid-list-lg)
  5. v-layout(row, wrap)
  6. v-flex(
  7. xs12
  8. offset-sm1, sm10
  9. offset-md2, md8
  10. offset-lg3, lg6
  11. offset-xl4, xl4
  12. )
  13. transition(name='fadeUp')
  14. v-card.elevation-5.md2(v-show='isShown')
  15. v-toolbar(color='primary', flat, dense, dark)
  16. v-spacer
  17. .subheading(v-if='screen === "tfa"') {{ $t('auth:tfa.subtitle') }}
  18. .subheading(v-else-if='selectedStrategy.key !== "local"') {{ $t('auth:loginUsingStrategy', { strategy: selectedStrategy.title, interpolation: { escapeValue: false } }) }}
  19. .subheading(v-else) {{ $t('auth:loginRequired') }}
  20. v-spacer
  21. v-card-text.text-xs-center
  22. h1.display-1.primary--text.py-2 {{ siteTitle }}
  23. template(v-if='screen === "login"')
  24. v-text-field.md2.mt-3(
  25. solo
  26. flat
  27. prepend-icon='email'
  28. background-color='grey lighten-4'
  29. hide-details
  30. ref='iptEmail'
  31. v-model='username'
  32. :placeholder='$t("auth:fields.emailUser")'
  33. )
  34. v-text-field.md2.mt-2(
  35. solo
  36. flat
  37. prepend-icon='vpn_key'
  38. background-color='grey lighten-4'
  39. hide-details
  40. ref='iptPassword'
  41. v-model='password'
  42. :append-icon='hidePassword ? "visibility" : "visibility_off"'
  43. @click:append='() => (hidePassword = !hidePassword)'
  44. :type='hidePassword ? "password" : "text"'
  45. :placeholder='$t("auth:fields.password")'
  46. @keyup.enter='login'
  47. )
  48. template(v-else-if='screen === "tfa"')
  49. .body-2 Enter the security code generated from your trusted device:
  50. v-text-field.md2.centered.mt-2(
  51. solo
  52. flat
  53. background-color='grey lighten-4'
  54. hide-details
  55. ref='iptTFA'
  56. v-model='securityCode'
  57. :placeholder='$t("auth:tfa.placeholder")'
  58. @keyup.enter='verifySecurityCode'
  59. )
  60. template(v-else-if='screen === "forgot"')
  61. .body-2 {{ $t('auth:forgotPasswordSubtitle') }}
  62. v-text-field.md2.mt-3(
  63. solo
  64. flat
  65. prepend-icon='email'
  66. background-color='grey lighten-4'
  67. hide-details
  68. ref='iptEmailForgot'
  69. v-model='username'
  70. :placeholder='$t("auth:fields.email")'
  71. )
  72. v-card-actions.pb-4
  73. v-spacer
  74. v-btn.md2(
  75. v-if='screen === "login"'
  76. block
  77. large
  78. color='primary'
  79. @click='login'
  80. round
  81. :loading='isLoading'
  82. ) {{ $t('auth:actions.login') }}
  83. v-btn.md2(
  84. v-else-if='screen === "tfa"'
  85. block
  86. large
  87. color='primary'
  88. @click='verifySecurityCode'
  89. round
  90. :loading='isLoading'
  91. ) {{ $t('auth:tfa.verifyToken') }}
  92. v-btn.md2(
  93. v-else-if='screen === "forgot"'
  94. block
  95. large
  96. color='primary'
  97. @click='forgotPasswordSubmit'
  98. round
  99. :loading='isLoading'
  100. ) {{ $t('auth:sendResetPassword') }}
  101. v-spacer
  102. v-card-actions.pb-3(v-if='screen === "login" && selectedStrategy.key === "local"')
  103. v-spacer
  104. a.caption(@click.stop.prevent='forgotPassword', href='#forgot') {{ $t('auth:forgotPasswordLink') }}
  105. v-spacer
  106. v-card-actions.pb-3(v-else-if='screen === "forgot"')
  107. v-spacer
  108. a.caption(@click.stop.prevent='screen = `login`', href='#cancelforgot') {{ $t('auth:forgotPasswordCancel') }}
  109. v-spacer
  110. template(v-if='screen === "login" && isSocialShown')
  111. v-divider
  112. v-card-text.grey.lighten-4.text-xs-center
  113. .pb-2.body-2.text-xs-center.grey--text.text--darken-2 {{ $t('auth:orLoginUsingStrategy') }}
  114. v-tooltip(top, v-for='strategy in strategies', :key='strategy.key')
  115. .social-login-btn.mr-2(
  116. slot='activator'
  117. v-ripple
  118. v-html='strategy.icon'
  119. :class='strategy.color + " elevation-" + (strategy.key === selectedStrategy.key ? "0" : "4")'
  120. @click='selectStrategy(strategy)'
  121. )
  122. span {{ strategy.title }}
  123. template(v-if='screen === "login" && selectedStrategy.selfRegistration')
  124. v-divider
  125. v-card-actions.py-3(:class='isSocialShown ? "" : "grey lighten-4"')
  126. v-spacer
  127. i18next.caption(path='auth:switchToRegister.text', tag='div')
  128. a.caption(href='/register', place='link') {{ $t('auth:switchToRegister.link') }}
  129. v-spacer
  130. loader(v-model='isLoading', :color='loaderColor', :title='loaderTitle', :subtitle='$t(`auth:pleaseWait`)')
  131. nav-footer(color='grey darken-4')
  132. notify
  133. </template>
  134. <script>
  135. /* global siteConfig */
  136. import _ from 'lodash'
  137. import Cookies from 'js-cookie'
  138. import strategiesQuery from 'gql/login/login-query-strategies.gql'
  139. import loginMutation from 'gql/login/login-mutation-login.gql'
  140. import tfaMutation from 'gql/login/login-mutation-tfa.gql'
  141. export default {
  142. i18nOptions: { namespaces: 'auth' },
  143. data () {
  144. return {
  145. error: false,
  146. strategies: [],
  147. selectedStrategy: { key: 'local' },
  148. screen: 'login',
  149. username: '',
  150. password: '',
  151. hidePassword: true,
  152. securityCode: '',
  153. loginToken: '',
  154. isLoading: false,
  155. loaderColor: 'grey darken-4',
  156. loaderTitle: 'Working...',
  157. isShown: false
  158. }
  159. },
  160. computed: {
  161. siteTitle () {
  162. return siteConfig.title
  163. },
  164. isSocialShown () {
  165. return this.strategies.length > 1
  166. }
  167. },
  168. watch: {
  169. strategies(newValue, oldValue) {
  170. this.selectedStrategy = _.find(newValue, ['key', 'local'])
  171. }
  172. },
  173. mounted () {
  174. this.isShown = true
  175. this.$nextTick(() => {
  176. this.$refs.iptEmail.focus()
  177. })
  178. },
  179. methods: {
  180. /**
  181. * SELECT STRATEGY
  182. */
  183. selectStrategy (strategy) {
  184. this.selectedStrategy = strategy
  185. this.screen = 'login'
  186. if (!strategy.useForm) {
  187. this.isLoading = true
  188. window.location.assign('/login/' + strategy.key)
  189. } else {
  190. this.$nextTick(() => {
  191. this.$refs.iptEmail.focus()
  192. })
  193. }
  194. },
  195. /**
  196. * LOGIN
  197. */
  198. async login () {
  199. if (this.username.length < 2) {
  200. this.$store.commit('showNotification', {
  201. style: 'red',
  202. message: this.$t('auth:invalidEmailUsername'),
  203. icon: 'warning'
  204. })
  205. this.$refs.iptEmail.focus()
  206. } else if (this.password.length < 2) {
  207. this.$store.commit('showNotification', {
  208. style: 'red',
  209. message: this.$t('auth:invalidPassword'),
  210. icon: 'warning'
  211. })
  212. this.$refs.iptPassword.focus()
  213. } else {
  214. this.loaderColor = 'grey darken-4'
  215. this.loaderTitle = this.$t('auth:signingIn')
  216. this.isLoading = true
  217. try {
  218. let resp = await this.$apollo.mutate({
  219. mutation: loginMutation,
  220. variables: {
  221. username: this.username,
  222. password: this.password,
  223. strategy: this.selectedStrategy.key
  224. }
  225. })
  226. if (_.has(resp, 'data.authentication.login')) {
  227. let respObj = _.get(resp, 'data.authentication.login', {})
  228. if (respObj.responseResult.succeeded === true) {
  229. if (respObj.tfaRequired === true) {
  230. this.screen = 'tfa'
  231. this.securityCode = ''
  232. this.loginToken = respObj.tfaLoginToken
  233. this.$nextTick(() => {
  234. this.$refs.iptTFA.focus()
  235. })
  236. this.isLoading = false
  237. } else {
  238. this.loaderColor = 'green darken-1'
  239. this.loaderTitle = this.$t('auth:loginSuccess')
  240. Cookies.set('jwt', respObj.jwt, { expires: 365 })
  241. _.delay(() => {
  242. window.location.replace('/') // TEMPORARY - USE RETURNURL
  243. }, 1000)
  244. }
  245. } else {
  246. throw new Error(respObj.responseResult.message)
  247. }
  248. } else {
  249. throw new Error(this.$t('auth:genericError'))
  250. }
  251. } catch (err) {
  252. console.error(err)
  253. this.$store.commit('showNotification', {
  254. style: 'red',
  255. message: err.message,
  256. icon: 'warning'
  257. })
  258. this.isLoading = false
  259. }
  260. }
  261. },
  262. /**
  263. * VERIFY TFA CODE
  264. */
  265. verifySecurityCode () {
  266. if (this.securityCode.length !== 6) {
  267. this.$store.commit('showNotification', {
  268. style: 'red',
  269. message: 'Enter a valid security code.',
  270. icon: 'warning'
  271. })
  272. this.$refs.iptTFA.focus()
  273. } else {
  274. this.isLoading = true
  275. this.$apollo.mutate({
  276. mutation: tfaMutation,
  277. variables: {
  278. loginToken: this.loginToken,
  279. securityCode: this.securityCode
  280. }
  281. }).then(resp => {
  282. if (_.has(resp, 'data.authentication.loginTFA')) {
  283. let respObj = _.get(resp, 'data.authentication.loginTFA', {})
  284. if (respObj.responseResult.succeeded === true) {
  285. this.$store.commit('showNotification', {
  286. message: 'Login successful!',
  287. style: 'success',
  288. icon: 'check'
  289. })
  290. _.delay(() => {
  291. window.location.replace('/') // TEMPORARY - USE RETURNURL
  292. }, 1000)
  293. this.isLoading = false
  294. } else {
  295. throw new Error(respObj.responseResult.message)
  296. }
  297. } else {
  298. throw new Error(this.$t('auth:genericError'))
  299. }
  300. }).catch(err => {
  301. console.error(err)
  302. this.$store.commit('showNotification', {
  303. style: 'red',
  304. message: err.message,
  305. icon: 'warning'
  306. })
  307. this.isLoading = false
  308. })
  309. }
  310. },
  311. forgotPassword() {
  312. this.screen = 'forgot'
  313. this.$nextTick(() => {
  314. this.$refs.iptEmailForgot.focus()
  315. })
  316. },
  317. async forgotPasswordSubmit() {
  318. this.$store.commit('showNotification', {
  319. style: 'pink',
  320. message: 'Coming soon!',
  321. icon: 'free_breakfast'
  322. })
  323. }
  324. },
  325. apollo: {
  326. strategies: {
  327. query: strategiesQuery,
  328. update: (data) => data.authentication.strategies,
  329. watchLoading (isLoading) {
  330. this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'login-strategies-refresh')
  331. }
  332. }
  333. }
  334. }
  335. </script>
  336. <style lang="scss">
  337. .login {
  338. background-color: mc('grey', '900');
  339. background-image: url('../static/svg/motif-blocks.svg');
  340. background-repeat: repeat;
  341. background-size: 200px;
  342. width: 100%;
  343. height: 100%;
  344. animation: loginBgReveal 20s linear infinite;
  345. @include keyframes(loginBgReveal) {
  346. 0% {
  347. background-position-x: 0;
  348. }
  349. 100% {
  350. background-position-x: 800px;
  351. }
  352. }
  353. &::before {
  354. content: '';
  355. position: absolute;
  356. background-image: url('../static/svg/motif-overlay.svg');
  357. background-attachment: fixed;
  358. background-size: cover;
  359. opacity: .5;
  360. top: 0;
  361. left: 0;
  362. width: 100vw;
  363. height: 100vh;
  364. }
  365. > .container {
  366. height: 100%;
  367. align-items: center;
  368. display: flex;
  369. }
  370. .social-login-btn {
  371. display: inline-flex;
  372. justify-content: center;
  373. align-items: center;
  374. border-radius: 50%;
  375. width: 54px;
  376. height: 54px;
  377. cursor: pointer;
  378. transition: opacity .2s ease;
  379. &:hover {
  380. opacity: .8;
  381. }
  382. margin: .5rem 0;
  383. svg {
  384. width: 24px;
  385. height: 24px;
  386. bottom: 0;
  387. path {
  388. fill: #FFF;
  389. }
  390. }
  391. }
  392. .v-text-field.centered input {
  393. text-align: center;
  394. }
  395. }
  396. </style>