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.

410 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='zoom')
  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 }) }}
  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. </template>
  133. <script>
  134. /* global siteConfig */
  135. import _ from 'lodash'
  136. import Cookies from 'js-cookie'
  137. import strategiesQuery from 'gql/login/login-query-strategies.gql'
  138. import loginMutation from 'gql/login/login-mutation-login.gql'
  139. import tfaMutation from 'gql/login/login-mutation-tfa.gql'
  140. export default {
  141. i18nOptions: { namespaces: 'auth' },
  142. data () {
  143. return {
  144. error: false,
  145. strategies: [],
  146. selectedStrategy: { key: 'local' },
  147. screen: 'login',
  148. username: '',
  149. password: '',
  150. hidePassword: true,
  151. securityCode: '',
  152. loginToken: '',
  153. isLoading: false,
  154. loaderColor: 'grey darken-4',
  155. loaderTitle: 'Working...',
  156. isShown: false
  157. }
  158. },
  159. computed: {
  160. siteTitle () {
  161. return siteConfig.title
  162. },
  163. isSocialShown () {
  164. return this.strategies.length > 1
  165. }
  166. },
  167. watch: {
  168. strategies(newValue, oldValue) {
  169. this.selectedStrategy = _.find(newValue, ['key', 'local'])
  170. }
  171. },
  172. mounted () {
  173. this.isShown = true
  174. this.$nextTick(() => {
  175. this.$refs.iptEmail.focus()
  176. })
  177. },
  178. methods: {
  179. /**
  180. * SELECT STRATEGY
  181. */
  182. selectStrategy (strategy) {
  183. this.selectedStrategy = strategy
  184. this.screen = 'login'
  185. if (!strategy.useForm) {
  186. this.isLoading = true
  187. window.location.assign(this.$helpers.resolvePath('login/' + strategy.key))
  188. } else {
  189. this.$nextTick(() => {
  190. this.$refs.iptEmail.focus()
  191. })
  192. }
  193. },
  194. /**
  195. * LOGIN
  196. */
  197. async login () {
  198. if (this.username.length < 2) {
  199. this.$store.commit('showNotification', {
  200. style: 'red',
  201. message: this.$t('auth:invalidEmailUsername'),
  202. icon: 'warning'
  203. })
  204. this.$refs.iptEmail.focus()
  205. } else if (this.password.length < 2) {
  206. this.$store.commit('showNotification', {
  207. style: 'red',
  208. message: this.$t('auth:invalidPassword'),
  209. icon: 'warning'
  210. })
  211. this.$refs.iptPassword.focus()
  212. } else {
  213. this.loaderColor = 'grey darken-4'
  214. this.loaderTitle = this.$t('auth:signingIn')
  215. this.isLoading = true
  216. try {
  217. let resp = await this.$apollo.mutate({
  218. mutation: loginMutation,
  219. variables: {
  220. username: this.username,
  221. password: this.password,
  222. strategy: this.selectedStrategy.key
  223. }
  224. })
  225. if (_.has(resp, 'data.authentication.login')) {
  226. let respObj = _.get(resp, 'data.authentication.login', {})
  227. if (respObj.responseResult.succeeded === true) {
  228. if (respObj.tfaRequired === true) {
  229. this.screen = 'tfa'
  230. this.securityCode = ''
  231. this.loginToken = respObj.tfaLoginToken
  232. this.$nextTick(() => {
  233. this.$refs.iptTFA.focus()
  234. })
  235. this.isLoading = false
  236. } else {
  237. this.loaderColor = 'green darken-1'
  238. this.loaderTitle = this.$t('auth:loginSuccess')
  239. Cookies.set('jwt', respObj.jwt, { expires: 365 })
  240. _.delay(() => {
  241. window.location.replace('/') // TEMPORARY - USE RETURNURL
  242. }, 1000)
  243. }
  244. } else {
  245. throw new Error(respObj.responseResult.message)
  246. }
  247. } else {
  248. throw new Error(this.$t('auth:genericError'))
  249. }
  250. } catch (err) {
  251. console.error(err)
  252. this.$store.commit('showNotification', {
  253. style: 'red',
  254. message: err.message,
  255. icon: 'warning'
  256. })
  257. this.isLoading = false
  258. }
  259. }
  260. },
  261. /**
  262. * VERIFY TFA CODE
  263. */
  264. verifySecurityCode () {
  265. if (this.securityCode.length !== 6) {
  266. this.$store.commit('showNotification', {
  267. style: 'red',
  268. message: 'Enter a valid security code.',
  269. icon: 'warning'
  270. })
  271. this.$refs.iptTFA.focus()
  272. } else {
  273. this.isLoading = true
  274. this.$apollo.mutate({
  275. mutation: tfaMutation,
  276. variables: {
  277. loginToken: this.loginToken,
  278. securityCode: this.securityCode
  279. }
  280. }).then(resp => {
  281. if (_.has(resp, 'data.authentication.loginTFA')) {
  282. let respObj = _.get(resp, 'data.authentication.loginTFA', {})
  283. if (respObj.responseResult.succeeded === true) {
  284. this.$store.commit('showNotification', {
  285. message: 'Login successful!',
  286. style: 'success',
  287. icon: 'check'
  288. })
  289. _.delay(() => {
  290. window.location.replace('/') // TEMPORARY - USE RETURNURL
  291. }, 1000)
  292. this.isLoading = false
  293. } else {
  294. throw new Error(respObj.responseResult.message)
  295. }
  296. } else {
  297. throw new Error(this.$t('auth:genericError'))
  298. }
  299. }).catch(err => {
  300. console.error(err)
  301. this.$store.commit('showNotification', {
  302. style: 'red',
  303. message: err.message,
  304. icon: 'warning'
  305. })
  306. this.isLoading = false
  307. })
  308. }
  309. },
  310. forgotPassword() {
  311. this.screen = 'forgot'
  312. this.$nextTick(() => {
  313. this.$refs.iptEmailForgot.focus()
  314. })
  315. },
  316. async forgotPasswordSubmit() {
  317. this.$store.commit('showNotification', {
  318. style: 'pink',
  319. message: 'Coming soon!',
  320. icon: 'free_breakfast'
  321. })
  322. }
  323. },
  324. apollo: {
  325. strategies: {
  326. query: strategiesQuery,
  327. update: (data) => data.authentication.strategies,
  328. watchLoading (isLoading) {
  329. this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'login-strategies-refresh')
  330. }
  331. }
  332. }
  333. }
  334. </script>
  335. <style lang="scss">
  336. .login {
  337. background-color: mc('grey', '900');
  338. background-image: url('../static/svg/motif-blocks.svg');
  339. background-repeat: repeat;
  340. background-size: 200px;
  341. width: 100%;
  342. height: 100%;
  343. animation: loginBgReveal 20s linear infinite;
  344. @include keyframes(loginBgReveal) {
  345. 0% {
  346. background-position-x: 0;
  347. }
  348. 100% {
  349. background-position-x: 800px;
  350. }
  351. }
  352. &::before {
  353. content: '';
  354. position: absolute;
  355. background-image: url('../static/svg/motif-overlay.svg');
  356. background-attachment: fixed;
  357. background-size: cover;
  358. opacity: .5;
  359. top: 0;
  360. left: 0;
  361. width: 100vw;
  362. height: 100vh;
  363. }
  364. > .container {
  365. height: 100%;
  366. align-items: center;
  367. display: flex;
  368. }
  369. h1 {
  370. font-family: 'Varela Round' !important;
  371. }
  372. .social-login-btn {
  373. display: inline-flex;
  374. justify-content: center;
  375. align-items: center;
  376. border-radius: 50%;
  377. width: 54px;
  378. height: 54px;
  379. cursor: pointer;
  380. transition: opacity .2s ease;
  381. &:hover {
  382. opacity: .8;
  383. }
  384. margin: .5rem 0;
  385. svg {
  386. width: 24px;
  387. height: 24px;
  388. bottom: 0;
  389. path {
  390. fill: #FFF;
  391. }
  392. }
  393. }
  394. .v-text-field.centered input {
  395. text-align: center;
  396. }
  397. }
  398. </style>