login.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  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-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. v-card-actions.pb-4
  61. v-spacer
  62. v-btn.md2(
  63. v-if='screen === "login"'
  64. block
  65. large
  66. color='primary'
  67. @click='login'
  68. round
  69. :loading='isLoading'
  70. ) {{ $t('auth:actions.login') }}
  71. v-btn.md2(
  72. v-if='screen === "tfa"'
  73. block
  74. large
  75. color='primary'
  76. @click='verifySecurityCode'
  77. round
  78. :loading='isLoading'
  79. ) {{ $t('auth:tfa.verifyToken') }}
  80. v-spacer
  81. v-card-actions.pb-3(v-if='selectedStrategy.key === "local"')
  82. v-spacer
  83. a.caption(href='') {{ $t('auth:forgotPasswordLink') }}
  84. v-spacer
  85. template(v-if='isSocialShown')
  86. v-divider
  87. v-card-text.grey.lighten-4.text-xs-center
  88. .pb-2.body-2.text-xs-center.grey--text.text--darken-2 {{ $t('auth:orLoginUsingStrategy') }}
  89. v-tooltip(top, v-for='strategy in strategies', :key='strategy.key')
  90. .social-login-btn.mr-2(
  91. slot='activator'
  92. v-ripple
  93. v-html='strategy.icon'
  94. :class='strategy.color + " elevation-" + (strategy.key === selectedStrategy.key ? "0" : "4")'
  95. @click='selectStrategy(strategy)'
  96. )
  97. span {{ strategy.title }}
  98. template(v-if='selectedStrategy.selfRegistration')
  99. v-divider
  100. v-card-actions.py-3(:class='isSocialShown ? "" : "grey lighten-4"')
  101. v-spacer
  102. i18next.caption(path='auth:switchToRegister.text', tag='div')
  103. a.caption(href='/register', place='link') {{ $t('auth:switchToRegister.link') }}
  104. v-spacer
  105. loader(v-model='isLoading', :color='loaderColor', :title='loaderTitle', :subtitle='$t(`auth:pleaseWait`)')
  106. nav-footer(color='grey darken-4')
  107. </template>
  108. <script>
  109. /* global siteConfig */
  110. import _ from 'lodash'
  111. import Cookies from 'js-cookie'
  112. import strategiesQuery from 'gql/login/login-query-strategies.gql'
  113. import loginMutation from 'gql/login/login-mutation-login.gql'
  114. import tfaMutation from 'gql/login/login-mutation-tfa.gql'
  115. export default {
  116. i18nOptions: { namespaces: 'auth' },
  117. data () {
  118. return {
  119. error: false,
  120. strategies: [],
  121. selectedStrategy: { key: 'local' },
  122. screen: 'login',
  123. username: '',
  124. password: '',
  125. hidePassword: true,
  126. securityCode: '',
  127. loginToken: '',
  128. isLoading: false,
  129. loaderColor: 'grey darken-4',
  130. loaderTitle: 'Working...',
  131. isShown: false
  132. }
  133. },
  134. computed: {
  135. siteTitle () {
  136. return siteConfig.title
  137. },
  138. isSocialShown () {
  139. return this.strategies.length > 1
  140. }
  141. },
  142. watch: {
  143. strategies(newValue, oldValue) {
  144. this.selectedStrategy = _.find(newValue, ['key', 'local'])
  145. }
  146. },
  147. mounted () {
  148. this.isShown = true
  149. this.$nextTick(() => {
  150. this.$refs.iptEmail.focus()
  151. })
  152. },
  153. methods: {
  154. /**
  155. * SELECT STRATEGY
  156. */
  157. selectStrategy (strategy) {
  158. this.selectedStrategy = strategy
  159. this.screen = 'login'
  160. if (!strategy.useForm) {
  161. this.isLoading = true
  162. window.location.assign(this.$helpers.resolvePath('login/' + strategy.key))
  163. } else {
  164. this.$nextTick(() => {
  165. this.$refs.iptEmail.focus()
  166. })
  167. }
  168. },
  169. /**
  170. * LOGIN
  171. */
  172. async login () {
  173. if (this.username.length < 2) {
  174. this.$store.commit('showNotification', {
  175. style: 'red',
  176. message: this.$t('auth:invalidEmailUsername'),
  177. icon: 'warning'
  178. })
  179. this.$refs.iptEmail.focus()
  180. } else if (this.password.length < 2) {
  181. this.$store.commit('showNotification', {
  182. style: 'red',
  183. message: this.$t('auth:invalidPassword'),
  184. icon: 'warning'
  185. })
  186. this.$refs.iptPassword.focus()
  187. } else {
  188. this.loaderColor = 'grey darken-4'
  189. this.loaderTitle = this.$t('auth:signingIn')
  190. this.isLoading = true
  191. try {
  192. let resp = await this.$apollo.mutate({
  193. mutation: loginMutation,
  194. variables: {
  195. username: this.username,
  196. password: this.password,
  197. strategy: this.selectedStrategy.key
  198. }
  199. })
  200. if (_.has(resp, 'data.authentication.login')) {
  201. let respObj = _.get(resp, 'data.authentication.login', {})
  202. if (respObj.responseResult.succeeded === true) {
  203. if (respObj.tfaRequired === true) {
  204. this.screen = 'tfa'
  205. this.securityCode = ''
  206. this.loginToken = respObj.tfaLoginToken
  207. this.$nextTick(() => {
  208. this.$refs.iptTFA.focus()
  209. })
  210. this.isLoading = false
  211. } else {
  212. this.loaderColor = 'green darken-1'
  213. this.loaderTitle = this.$t('auth:loginSuccess')
  214. Cookies.set('jwt', respObj.jwt, { expires: 365 })
  215. _.delay(() => {
  216. window.location.replace('/') // TEMPORARY - USE RETURNURL
  217. }, 1000)
  218. }
  219. } else {
  220. throw new Error(respObj.responseResult.message)
  221. }
  222. } else {
  223. throw new Error(this.$t('auth:genericError'))
  224. }
  225. } catch (err) {
  226. console.error(err)
  227. this.$store.commit('showNotification', {
  228. style: 'red',
  229. message: err.message,
  230. icon: 'warning'
  231. })
  232. this.isLoading = false
  233. }
  234. }
  235. },
  236. /**
  237. * VERIFY TFA CODE
  238. */
  239. verifySecurityCode () {
  240. if (this.securityCode.length !== 6) {
  241. this.$store.commit('showNotification', {
  242. style: 'red',
  243. message: 'Enter a valid security code.',
  244. icon: 'warning'
  245. })
  246. this.$refs.iptTFA.focus()
  247. } else {
  248. this.isLoading = true
  249. this.$apollo.mutate({
  250. mutation: tfaMutation,
  251. variables: {
  252. loginToken: this.loginToken,
  253. securityCode: this.securityCode
  254. }
  255. }).then(resp => {
  256. if (_.has(resp, 'data.authentication.loginTFA')) {
  257. let respObj = _.get(resp, 'data.authentication.loginTFA', {})
  258. if (respObj.responseResult.succeeded === true) {
  259. this.$store.commit('showNotification', {
  260. message: 'Login successful!',
  261. style: 'success',
  262. icon: 'check'
  263. })
  264. _.delay(() => {
  265. window.location.replace('/') // TEMPORARY - USE RETURNURL
  266. }, 1000)
  267. this.isLoading = false
  268. } else {
  269. throw new Error(respObj.responseResult.message)
  270. }
  271. } else {
  272. throw new Error(this.$t('auth:genericError'))
  273. }
  274. }).catch(err => {
  275. console.error(err)
  276. this.$store.commit('showNotification', {
  277. style: 'red',
  278. message: err.message,
  279. icon: 'warning'
  280. })
  281. this.isLoading = false
  282. })
  283. }
  284. }
  285. },
  286. apollo: {
  287. strategies: {
  288. query: strategiesQuery,
  289. update: (data) => data.authentication.strategies,
  290. watchLoading (isLoading) {
  291. this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'login-strategies-refresh')
  292. }
  293. }
  294. }
  295. }
  296. </script>
  297. <style lang="scss">
  298. .login {
  299. background-color: mc('grey', '900');
  300. background-image: url('../static/svg/motif-blocks.svg');
  301. background-repeat: repeat;
  302. background-size: 200px;
  303. width: 100%;
  304. height: 100%;
  305. animation: loginBgReveal 20s linear infinite;
  306. @include keyframes(loginBgReveal) {
  307. 0% {
  308. background-position-y: 0;
  309. }
  310. 100% {
  311. background-position-y: -800px;
  312. }
  313. }
  314. &::before {
  315. content: '';
  316. position: absolute;
  317. background-image: url('../static/svg/motif-overlay.svg');
  318. background-attachment: fixed;
  319. background-size: cover;
  320. opacity: .5;
  321. top: 0;
  322. left: 0;
  323. width: 100vw;
  324. height: 100vh;
  325. }
  326. > .container {
  327. height: 100%;
  328. align-items: center;
  329. display: flex;
  330. }
  331. h1 {
  332. font-family: 'Varela Round' !important;
  333. }
  334. .social-login-btn {
  335. display: inline-flex;
  336. justify-content: center;
  337. align-items: center;
  338. border-radius: 50%;
  339. width: 54px;
  340. height: 54px;
  341. cursor: pointer;
  342. transition: opacity .2s ease;
  343. &:hover {
  344. opacity: .8;
  345. }
  346. margin: .5rem 0;
  347. svg {
  348. width: 24px;
  349. height: 24px;
  350. bottom: 0;
  351. path {
  352. fill: #FFF;
  353. }
  354. }
  355. }
  356. .v-text-field.centered input {
  357. text-align: center;
  358. }
  359. }
  360. </style>