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.

487 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')
  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. window.location.replace('/') // TEMPORARY - USE RETURNURL
  293. }, 1000)
  294. }
  295. } else {
  296. throw new Error(respObj.responseResult.message)
  297. }
  298. } else {
  299. throw new Error(this.$t('auth:genericError'))
  300. }
  301. } catch (err) {
  302. console.error(err)
  303. this.$store.commit('showNotification', {
  304. style: 'red',
  305. message: err.message,
  306. icon: 'alert'
  307. })
  308. this.isLoading = false
  309. }
  310. }
  311. },
  312. /**
  313. * VERIFY TFA CODE
  314. */
  315. verifySecurityCode () {
  316. if (this.securityCode.length !== 6) {
  317. this.$store.commit('showNotification', {
  318. style: 'red',
  319. message: 'Enter a valid security code.',
  320. icon: 'warning'
  321. })
  322. this.$refs.iptTFA.focus()
  323. } else {
  324. this.isLoading = true
  325. this.$apollo.mutate({
  326. mutation: tfaMutation,
  327. variables: {
  328. continuationToken: this.continuationToken,
  329. securityCode: this.securityCode
  330. }
  331. }).then(resp => {
  332. if (_.has(resp, 'data.authentication.loginTFA')) {
  333. let respObj = _.get(resp, 'data.authentication.loginTFA', {})
  334. if (respObj.responseResult.succeeded === true) {
  335. this.$store.commit('showNotification', {
  336. message: 'Login successful!',
  337. style: 'success',
  338. icon: 'check'
  339. })
  340. _.delay(() => {
  341. window.location.replace('/') // TEMPORARY - USE RETURNURL
  342. }, 1000)
  343. this.isLoading = false
  344. } else {
  345. throw new Error(respObj.responseResult.message)
  346. }
  347. } else {
  348. throw new Error(this.$t('auth:genericError'))
  349. }
  350. }).catch(err => {
  351. console.error(err)
  352. this.$store.commit('showNotification', {
  353. style: 'red',
  354. message: err.message,
  355. icon: 'alert'
  356. })
  357. this.isLoading = false
  358. })
  359. }
  360. },
  361. /**
  362. * CHANGE PASSWORD
  363. */
  364. async changePassword () {
  365. this.loaderColor = 'grey darken-4'
  366. this.loaderTitle = this.$t('auth:changePwd.loading')
  367. this.isLoading = true
  368. const resp = await this.$apollo.mutate({
  369. mutation: changePasswordMutation,
  370. variables: {
  371. continuationToken: this.continuationToken,
  372. newPassword: this.newPassword
  373. }
  374. })
  375. if (_.get(resp, 'data.authentication.loginChangePassword.responseResult.succeeded', false) === true) {
  376. this.loaderColor = 'green darken-1'
  377. this.loaderTitle = this.$t('auth:loginSuccess')
  378. Cookies.set('jwt', _.get(resp, 'data.authentication.loginChangePassword.jwt', ''), { expires: 365 })
  379. _.delay(() => {
  380. window.location.replace('/') // TEMPORARY - USE RETURNURL
  381. }, 1000)
  382. } else {
  383. this.$store.commit('showNotification', {
  384. style: 'red',
  385. message: _.get(resp, 'data.authentication.loginChangePassword.responseResult.message', false),
  386. icon: 'alert'
  387. })
  388. this.isLoading = false
  389. }
  390. },
  391. /**
  392. * SWITCH TO FORGOT PASSWORD SCREEN
  393. */
  394. forgotPassword () {
  395. this.screen = 'forgot'
  396. this.$nextTick(() => {
  397. this.$refs.iptEmailForgot.focus()
  398. })
  399. },
  400. /**
  401. * FORGOT PASSWORD SUBMIT
  402. */
  403. async forgotPasswordSubmit () {
  404. this.$store.commit('showNotification', {
  405. style: 'pink',
  406. message: 'Coming soon!',
  407. icon: 'ferry'
  408. })
  409. }
  410. },
  411. apollo: {
  412. strategies: {
  413. query: strategiesQuery,
  414. update: (data) => data.authentication.strategies,
  415. watchLoading (isLoading) {
  416. this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'login-strategies-refresh')
  417. }
  418. }
  419. }
  420. }
  421. </script>
  422. <style lang="scss">
  423. .login {
  424. background-color: mc('indigo', '900');
  425. background-image: url('../static/svg/motif-blocks.svg');
  426. background-repeat: repeat;
  427. background-size: 200px;
  428. width: 100%;
  429. height: 100%;
  430. animation: loginBgReveal 20s linear infinite;
  431. @include keyframes(loginBgReveal) {
  432. 0% {
  433. background-position-y: 0;
  434. }
  435. 100% {
  436. background-position-y: 800px;
  437. }
  438. }
  439. &::before {
  440. content: '';
  441. position: absolute;
  442. background-image: url('../static/svg/motif-overlay.svg');
  443. background-attachment: fixed;
  444. background-size: cover;
  445. opacity: .5;
  446. top: 0;
  447. left: 0;
  448. width: 100vw;
  449. height: 100vh;
  450. }
  451. > .container {
  452. height: 100%;
  453. align-items: center;
  454. display: flex;
  455. }
  456. .social-login-btn {
  457. cursor: pointer;
  458. transition: opacity .2s ease;
  459. &:hover {
  460. opacity: .8;
  461. }
  462. margin: .25rem 0;
  463. svg {
  464. width: 24px;
  465. height: 24px;
  466. bottom: 0;
  467. path {
  468. fill: #FFF;
  469. }
  470. }
  471. }
  472. .v-text-field.centered input {
  473. text-align: center;
  474. }
  475. }
  476. </style>