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.

493 lines
16 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(v-show='isShown', light)
  15. v-toolbar(color='indigo', flat, dense, dark)
  16. v-spacer
  17. .subheading(v-if='screen === "tfa"') {{ $t('auth:tfa.subtitle') }}
  18. .subheading(v-if='screen === "changePwd"') {{ $t('auth:changePwd.subtitle') }}
  19. .subheading(v-else-if='selectedStrategy.key !== "local"') {{ $t('auth:loginUsingStrategy', { strategy: selectedStrategy.title, interpolation: { escapeValue: false } }) }}
  20. .subheading(v-else) {{ $t('auth:loginRequired') }}
  21. v-spacer
  22. v-card-text.text-center
  23. h1.display-1.indigo--text.py-2 {{ siteTitle }}
  24. template(v-if='screen === "login"')
  25. v-text-field.mt-3(
  26. solo
  27. flat
  28. prepend-icon='mdi-clipboard-account'
  29. background-color='grey lighten-4'
  30. hide-details
  31. ref='iptEmail'
  32. v-model='username'
  33. :placeholder='$t("auth:fields.emailUser")'
  34. )
  35. v-text-field.mt-2(
  36. solo
  37. flat
  38. prepend-icon='mdi-textbox-password'
  39. background-color='grey lighten-4'
  40. hide-details
  41. ref='iptPassword'
  42. v-model='password'
  43. :append-icon='hidePassword ? "mdi-eye-off" : "mdi-eye"'
  44. @click:append='() => (hidePassword = !hidePassword)'
  45. :type='hidePassword ? "password" : "text"'
  46. :placeholder='$t("auth:fields.password")'
  47. @keyup.enter='login'
  48. )
  49. template(v-else-if='screen === "tfa"')
  50. .body-2 Enter the security code generated from your trusted device:
  51. v-text-field.centered.mt-2(
  52. solo
  53. flat
  54. background-color='grey lighten-4'
  55. hide-details
  56. ref='iptTFA'
  57. v-model='securityCode'
  58. :placeholder='$t("auth:tfa.placeholder")'
  59. @keyup.enter='verifySecurityCode'
  60. )
  61. template(v-else-if='screen === "changePwd"')
  62. .body-2 {{$t('auth:changePwd.instructions')}}
  63. v-text-field.mt-2(
  64. type='password'
  65. solo
  66. flat
  67. background-color='grey lighten-4'
  68. hide-details
  69. ref='iptNewPassword'
  70. v-model='newPassword'
  71. :placeholder='$t(`auth:changePwd.newPasswordPlaceholder`)'
  72. )
  73. v-text-field.mt-2(
  74. type='password'
  75. solo
  76. flat
  77. background-color='grey lighten-4'
  78. hide-details
  79. v-model='newPasswordVerify'
  80. :placeholder='$t(`auth:changePwd.newPasswordVerifyPlaceholder`)'
  81. @keyup.enter='changePassword'
  82. )
  83. template(v-else-if='screen === "forgot"')
  84. .body-2 {{ $t('auth:forgotPasswordSubtitle') }}
  85. v-text-field.mt-3(
  86. solo
  87. flat
  88. prepend-icon='mdi-email'
  89. background-color='grey lighten-4'
  90. hide-details
  91. ref='iptEmailForgot'
  92. v-model='username'
  93. :placeholder='$t("auth:fields.email")'
  94. )
  95. v-card-actions.pb-4
  96. v-spacer
  97. v-btn(
  98. width='100%'
  99. max-width='250px'
  100. v-if='screen === "login"'
  101. large
  102. color='primary'
  103. dark
  104. @click='login'
  105. rounded
  106. :loading='isLoading'
  107. ) {{ $t('auth:actions.login') }}
  108. v-btn(
  109. width='100%'
  110. max-width='250px'
  111. v-else-if='screen === "tfa"'
  112. large
  113. color='primary'
  114. dark
  115. @click='verifySecurityCode'
  116. rounded
  117. :loading='isLoading'
  118. ) {{ $t('auth:tfa.verifyToken') }}
  119. v-btn(
  120. width='100%'
  121. max-width='250px'
  122. v-else-if='screen === "changePwd"'
  123. large
  124. color='primary'
  125. dark
  126. @click='changePassword'
  127. rounded
  128. :loading='isLoading'
  129. ) {{ $t('auth:changePwd.proceed') }}
  130. v-btn(
  131. width='100%'
  132. max-width='250px'
  133. v-else-if='screen === "forgot"'
  134. large
  135. color='primary'
  136. dark
  137. @click='forgotPasswordSubmit'
  138. rounded
  139. :loading='isLoading'
  140. ) {{ $t('auth:sendResetPassword') }}
  141. v-spacer
  142. v-card-actions.pb-3(v-if='screen === "login" && selectedStrategy.key === "local"')
  143. v-spacer
  144. a.caption(@click.stop.prevent='forgotPassword', href='#forgot') {{ $t('auth:forgotPasswordLink') }}
  145. v-spacer
  146. v-card-actions.pb-3(v-else-if='screen === "forgot"')
  147. v-spacer
  148. a.caption(@click.stop.prevent='screen = `login`', href='#cancelforgot') {{ $t('auth:forgotPasswordCancel') }}
  149. v-spacer
  150. template(v-if='screen === "login" && isSocialShown')
  151. v-divider
  152. v-card-text.grey.lighten-4.text-center
  153. .pb-2.body-2.text-xs-center.grey--text.text--darken-2 {{ $t('auth:orLoginUsingStrategy') }}
  154. v-btn.mx-1.social-login-btn(
  155. v-for='strategy in strategies', :key='strategy.key'
  156. large
  157. @click='selectStrategy(strategy)'
  158. dark
  159. :color='strategy.color'
  160. :depressed='strategy.key === selectedStrategy.key'
  161. )
  162. v-avatar.mr-3(tile, :class='strategy.color', size='24', v-html='strategy.icon')
  163. span(style='text-transform: none;') {{ strategy.title }}
  164. template(v-if='screen === "login" && selectedStrategy.key === `local` && selectedStrategy.selfRegistration')
  165. v-divider
  166. v-card-actions.py-3(:class='isSocialShown ? "" : "grey lighten-4"')
  167. v-spacer
  168. i18next.caption(path='auth:switchToRegister.text', tag='div')
  169. a.caption(href='/register', place='link') {{ $t('auth:switchToRegister.link') }}
  170. v-spacer
  171. loader(v-model='isLoading', :color='loaderColor', :title='loaderTitle', :subtitle='$t(`auth:pleaseWait`)')
  172. nav-footer(color='grey darken-5', dark-color='grey darken-5')
  173. notify
  174. </template>
  175. <script>
  176. /* global siteConfig */
  177. import _ from 'lodash'
  178. import Cookies from 'js-cookie'
  179. import strategiesQuery from 'gql/login/login-query-strategies.gql'
  180. import loginMutation from 'gql/login/login-mutation-login.gql'
  181. import tfaMutation from 'gql/login/login-mutation-tfa.gql'
  182. import changePasswordMutation from 'gql/login/login-mutation-changepassword.gql'
  183. export default {
  184. i18nOptions: { namespaces: 'auth' },
  185. data () {
  186. return {
  187. error: false,
  188. strategies: [],
  189. selectedStrategy: { key: 'local' },
  190. screen: 'login',
  191. username: '',
  192. password: '',
  193. hidePassword: true,
  194. securityCode: '',
  195. continuationToken: '',
  196. isLoading: false,
  197. loaderColor: 'grey darken-4',
  198. loaderTitle: 'Working...',
  199. isShown: false,
  200. newPassword: '',
  201. newPasswordVerify: ''
  202. }
  203. },
  204. computed: {
  205. siteTitle () {
  206. return siteConfig.title
  207. },
  208. isSocialShown () {
  209. return this.strategies.length > 1
  210. }
  211. },
  212. watch: {
  213. strategies(newValue, oldValue) {
  214. this.selectedStrategy = _.find(newValue, ['key', 'local'])
  215. }
  216. },
  217. mounted () {
  218. this.isShown = true
  219. this.$nextTick(() => {
  220. this.$refs.iptEmail.focus()
  221. })
  222. },
  223. methods: {
  224. /**
  225. * SELECT STRATEGY
  226. */
  227. selectStrategy (strategy) {
  228. this.selectedStrategy = strategy
  229. this.screen = 'login'
  230. if (!strategy.useForm) {
  231. this.isLoading = true
  232. window.location.assign('/login/' + strategy.key)
  233. } else {
  234. this.$nextTick(() => {
  235. this.$refs.iptEmail.focus()
  236. })
  237. }
  238. },
  239. /**
  240. * LOGIN
  241. */
  242. async login () {
  243. if (this.username.length < 2) {
  244. this.$store.commit('showNotification', {
  245. style: 'red',
  246. message: this.$t('auth:invalidEmailUsername'),
  247. icon: 'alert'
  248. })
  249. this.$refs.iptEmail.focus()
  250. } else if (this.password.length < 2) {
  251. this.$store.commit('showNotification', {
  252. style: 'red',
  253. message: this.$t('auth:invalidPassword'),
  254. icon: 'alert'
  255. })
  256. this.$refs.iptPassword.focus()
  257. } else {
  258. this.loaderColor = 'grey darken-4'
  259. this.loaderTitle = this.$t('auth:signingIn')
  260. this.isLoading = true
  261. try {
  262. let resp = await this.$apollo.mutate({
  263. mutation: loginMutation,
  264. variables: {
  265. username: this.username,
  266. password: this.password,
  267. strategy: this.selectedStrategy.key
  268. }
  269. })
  270. if (_.has(resp, 'data.authentication.login')) {
  271. let respObj = _.get(resp, 'data.authentication.login', {})
  272. if (respObj.responseResult.succeeded === true) {
  273. this.continuationToken = respObj.continuationToken
  274. if (respObj.mustChangePwd === true) {
  275. this.screen = 'changePwd'
  276. this.$nextTick(() => {
  277. this.$refs.iptNewPassword.focus()
  278. })
  279. this.isLoading = false
  280. } else if (respObj.mustProvideTFA === true) {
  281. this.screen = 'tfa'
  282. this.securityCode = ''
  283. this.$nextTick(() => {
  284. this.$refs.iptTFA.focus()
  285. })
  286. this.isLoading = false
  287. } else {
  288. this.loaderColor = 'green darken-1'
  289. this.loaderTitle = this.$t('auth:loginSuccess')
  290. Cookies.set('jwt', respObj.jwt, { expires: 365 })
  291. _.delay(() => {
  292. const loginRedirect = Cookies.get('loginRedirect')
  293. if (loginRedirect) {
  294. Cookies.remove('loginRedirect')
  295. window.location.replace(loginRedirect)
  296. } else {
  297. window.location.replace('/')
  298. }
  299. }, 1000)
  300. }
  301. } else {
  302. throw new Error(respObj.responseResult.message)
  303. }
  304. } else {
  305. throw new Error(this.$t('auth:genericError'))
  306. }
  307. } catch (err) {
  308. console.error(err)
  309. this.$store.commit('showNotification', {
  310. style: 'red',
  311. message: err.message,
  312. icon: 'alert'
  313. })
  314. this.isLoading = false
  315. }
  316. }
  317. },
  318. /**
  319. * VERIFY TFA CODE
  320. */
  321. verifySecurityCode () {
  322. if (this.securityCode.length !== 6) {
  323. this.$store.commit('showNotification', {
  324. style: 'red',
  325. message: 'Enter a valid security code.',
  326. icon: 'warning'
  327. })
  328. this.$refs.iptTFA.focus()
  329. } else {
  330. this.isLoading = true
  331. this.$apollo.mutate({
  332. mutation: tfaMutation,
  333. variables: {
  334. continuationToken: this.continuationToken,
  335. securityCode: this.securityCode
  336. }
  337. }).then(resp => {
  338. if (_.has(resp, 'data.authentication.loginTFA')) {
  339. let respObj = _.get(resp, 'data.authentication.loginTFA', {})
  340. if (respObj.responseResult.succeeded === true) {
  341. this.$store.commit('showNotification', {
  342. message: 'Login successful!',
  343. style: 'success',
  344. icon: 'check'
  345. })
  346. _.delay(() => {
  347. window.location.replace('/') // TEMPORARY - USE RETURNURL
  348. }, 1000)
  349. this.isLoading = false
  350. } else {
  351. throw new Error(respObj.responseResult.message)
  352. }
  353. } else {
  354. throw new Error(this.$t('auth:genericError'))
  355. }
  356. }).catch(err => {
  357. console.error(err)
  358. this.$store.commit('showNotification', {
  359. style: 'red',
  360. message: err.message,
  361. icon: 'alert'
  362. })
  363. this.isLoading = false
  364. })
  365. }
  366. },
  367. /**
  368. * CHANGE PASSWORD
  369. */
  370. async changePassword () {
  371. this.loaderColor = 'grey darken-4'
  372. this.loaderTitle = this.$t('auth:changePwd.loading')
  373. this.isLoading = true
  374. const resp = await this.$apollo.mutate({
  375. mutation: changePasswordMutation,
  376. variables: {
  377. continuationToken: this.continuationToken,
  378. newPassword: this.newPassword
  379. }
  380. })
  381. if (_.get(resp, 'data.authentication.loginChangePassword.responseResult.succeeded', false) === true) {
  382. this.loaderColor = 'green darken-1'
  383. this.loaderTitle = this.$t('auth:loginSuccess')
  384. Cookies.set('jwt', _.get(resp, 'data.authentication.loginChangePassword.jwt', ''), { expires: 365 })
  385. _.delay(() => {
  386. window.location.replace('/') // TEMPORARY - USE RETURNURL
  387. }, 1000)
  388. } else {
  389. this.$store.commit('showNotification', {
  390. style: 'red',
  391. message: _.get(resp, 'data.authentication.loginChangePassword.responseResult.message', false),
  392. icon: 'alert'
  393. })
  394. this.isLoading = false
  395. }
  396. },
  397. /**
  398. * SWITCH TO FORGOT PASSWORD SCREEN
  399. */
  400. forgotPassword () {
  401. this.screen = 'forgot'
  402. this.$nextTick(() => {
  403. this.$refs.iptEmailForgot.focus()
  404. })
  405. },
  406. /**
  407. * FORGOT PASSWORD SUBMIT
  408. */
  409. async forgotPasswordSubmit () {
  410. this.$store.commit('showNotification', {
  411. style: 'pink',
  412. message: 'Coming soon!',
  413. icon: 'ferry'
  414. })
  415. }
  416. },
  417. apollo: {
  418. strategies: {
  419. query: strategiesQuery,
  420. update: (data) => data.authentication.strategies,
  421. watchLoading (isLoading) {
  422. this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'login-strategies-refresh')
  423. }
  424. }
  425. }
  426. }
  427. </script>
  428. <style lang="scss">
  429. .login {
  430. background-color: mc('indigo', '900');
  431. background-image: url('../static/svg/motif-blocks.svg');
  432. background-repeat: repeat;
  433. background-size: 200px;
  434. width: 100%;
  435. height: 100%;
  436. animation: loginBgReveal 20s linear infinite;
  437. @include keyframes(loginBgReveal) {
  438. 0% {
  439. background-position-y: 0;
  440. }
  441. 100% {
  442. background-position-y: 800px;
  443. }
  444. }
  445. &::before {
  446. content: '';
  447. position: absolute;
  448. background-image: url('../static/svg/motif-overlay.svg');
  449. background-attachment: fixed;
  450. background-size: cover;
  451. opacity: .5;
  452. top: 0;
  453. left: 0;
  454. width: 100vw;
  455. height: 100vh;
  456. }
  457. > .container {
  458. height: 100%;
  459. align-items: center;
  460. display: flex;
  461. }
  462. .social-login-btn {
  463. cursor: pointer;
  464. transition: opacity .2s ease;
  465. &:hover {
  466. opacity: .8;
  467. }
  468. margin: .25rem 0;
  469. svg {
  470. width: 24px;
  471. height: 24px;
  472. bottom: 0;
  473. path {
  474. fill: #FFF;
  475. }
  476. }
  477. }
  478. .v-text-field.centered input {
  479. text-align: center;
  480. }
  481. }
  482. </style>