login.vue 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699
  1. <template lang="pug">
  2. v-app
  3. .login(:style='`background-image: url(` + bgUrl + `);`')
  4. .login-sd
  5. .d-flex.mb-5
  6. .login-logo
  7. v-avatar(tile, size='34')
  8. v-img(:src='logoUrl')
  9. .login-title
  10. .text-h6 {{ siteTitle }}
  11. v-alert.mb-0(
  12. v-model='errorShown'
  13. transition='slide-y-reverse-transition'
  14. color='red darken-2'
  15. tile
  16. dark
  17. dense
  18. icon='mdi-alert'
  19. )
  20. .body-2 {{errorMessage}}
  21. //-------------------------------------------------
  22. //- PROVIDERS LIST
  23. //-------------------------------------------------
  24. template(v-if='screen === `login` && strategies.length > 1')
  25. .login-subtitle
  26. .text-subtitle-1 Select Authentication Provider
  27. .login-list
  28. v-list.elevation-1.radius-7(nav)
  29. v-list-item-group(v-model='selectedStrategyKey')
  30. v-list-item(
  31. v-for='(stg, idx) of filteredStrategies'
  32. :key='stg.key'
  33. :value='stg.key'
  34. :color='stg.strategy.color'
  35. )
  36. v-avatar.mr-3(tile, size='24', v-html='stg.strategy.icon')
  37. span.text-none {{stg.displayName}}
  38. //-------------------------------------------------
  39. //- LOGIN FORM
  40. //-------------------------------------------------
  41. template(v-if='screen === `login` && selectedStrategy.strategy.useForm')
  42. .login-subtitle
  43. .text-subtitle-1 Enter your credentials
  44. .login-form
  45. v-text-field(
  46. solo
  47. flat
  48. prepend-inner-icon='mdi-clipboard-account'
  49. background-color='white'
  50. hide-details
  51. ref='iptEmail'
  52. v-model='username'
  53. :placeholder='isUsernameEmail ? $t(`auth:fields.email`) : $t(`auth:fields.username`)'
  54. :type='isUsernameEmail ? `email` : `text`'
  55. :autocomplete='isUsernameEmail ? `email` : `username`'
  56. )
  57. v-text-field.mt-2(
  58. solo
  59. flat
  60. prepend-inner-icon='mdi-form-textbox-password'
  61. background-color='white'
  62. hide-details
  63. ref='iptPassword'
  64. v-model='password'
  65. :append-icon='hidePassword ? "mdi-eye-off" : "mdi-eye"'
  66. @click:append='() => (hidePassword = !hidePassword)'
  67. :type='hidePassword ? "password" : "text"'
  68. :placeholder='$t("auth:fields.password")'
  69. autocomplete='current-password'
  70. @keyup.enter='login'
  71. )
  72. v-btn.mt-2.text-none(
  73. width='100%'
  74. large
  75. color='primary'
  76. dark
  77. @click='login'
  78. :loading='isLoading'
  79. ) {{ $t('auth:actions.login') }}
  80. .text-center.mt-5
  81. v-btn.text-none(
  82. text
  83. rounded
  84. color='grey darken-3'
  85. @click.stop.prevent='forgotPassword'
  86. href='#forgot'
  87. ): .caption {{ $t('auth:forgotPasswordLink') }}
  88. v-btn.text-none(
  89. v-if='selectedStrategyKey === `local` && selectedStrategy.selfRegistration'
  90. color='indigo darken-2'
  91. text
  92. rounded
  93. href='/register'
  94. ): .caption {{ $t('auth:switchToRegister.link') }}
  95. //-------------------------------------------------
  96. //- FORGOT PASSWORD FORM
  97. //-------------------------------------------------
  98. template(v-if='screen === `forgot`')
  99. .login-subtitle
  100. .text-subtitle-1 Forgot your password
  101. .login-info {{ $t('auth:forgotPasswordSubtitle') }}
  102. .login-form
  103. v-text-field(
  104. solo
  105. flat
  106. prepend-inner-icon='mdi-clipboard-account'
  107. background-color='white'
  108. hide-details
  109. ref='iptForgotPwdEmail'
  110. v-model='username'
  111. :placeholder='$t(`auth:fields.email`)'
  112. type='email'
  113. autocomplete='email'
  114. )
  115. v-btn.mt-2.text-none(
  116. width='100%'
  117. large
  118. color='primary'
  119. dark
  120. @click='forgotPasswordSubmit'
  121. :loading='isLoading'
  122. ) {{ $t('auth:sendResetPassword') }}
  123. .text-center.mt-5
  124. v-btn.text-none(
  125. text
  126. rounded
  127. color='grey darken-3'
  128. @click.stop.prevent='screen = `login`'
  129. href='#forgot'
  130. ): .caption {{ $t('auth:forgotPasswordCancel') }}
  131. //-------------------------------------------------
  132. //- CHANGE PASSWORD FORM
  133. //-------------------------------------------------
  134. template(v-if='screen === `changePwd`')
  135. .login-subtitle
  136. .text-subtitle-1 {{ $t('auth:changePwd.subtitle') }}
  137. .login-form
  138. v-text-field.mt-2(
  139. type='password'
  140. solo
  141. flat
  142. prepend-inner-icon='mdi-form-textbox-password'
  143. background-color='white'
  144. hide-details
  145. ref='iptNewPassword'
  146. v-model='newPassword'
  147. :placeholder='$t(`auth:changePwd.newPasswordPlaceholder`)'
  148. autocomplete='new-password'
  149. )
  150. password-strength(slot='progress', v-model='newPassword')
  151. v-text-field.mt-2(
  152. type='password'
  153. solo
  154. flat
  155. prepend-inner-icon='mdi-form-textbox-password'
  156. background-color='white'
  157. hide-details
  158. v-model='newPasswordVerify'
  159. :placeholder='$t(`auth:changePwd.newPasswordVerifyPlaceholder`)'
  160. autocomplete='new-password'
  161. @keyup.enter='changePassword'
  162. )
  163. v-btn.mt-2.text-none(
  164. width='100%'
  165. large
  166. color='primary'
  167. dark
  168. @click='changePassword'
  169. :loading='isLoading'
  170. ) {{ $t('auth:changePwd.proceed') }}
  171. //-------------------------------------------------
  172. //- TFA FORM
  173. //-------------------------------------------------
  174. v-dialog(v-model='isTFAShown', max-width='500', persistent)
  175. v-card
  176. .login-tfa.text-center.pa-5
  177. img(src='_assets/svg/icon-pin-pad.svg')
  178. .subtitle-2 Enter the security code generated from your trusted device:
  179. v-text-field.login-tfa-field.mt-2(
  180. solo
  181. flat
  182. background-color='white'
  183. hide-details
  184. ref='iptTFA'
  185. v-model='securityCode'
  186. :placeholder='$t("auth:tfa.placeholder")'
  187. autocomplete='one-time-code'
  188. @keyup.enter='verifySecurityCode(false)'
  189. )
  190. v-btn.mt-2.text-none(
  191. width='100%'
  192. large
  193. color='primary'
  194. dark
  195. @click='verifySecurityCode(false)'
  196. :loading='isLoading'
  197. ) {{ $t('auth:tfa.verifyToken') }}
  198. //-------------------------------------------------
  199. //- SETUP TFA FORM
  200. //-------------------------------------------------
  201. v-dialog(v-model='isTFASetupShown', max-width='600', persistent)
  202. v-card
  203. .login-tfa.text-center.pa-5
  204. .subtitle-1.primary--text Your administrator has required Two-Factor Authentication (2FA) to be enabled on your account.
  205. v-divider.my-5
  206. .subtitle-2 1) Scan the QR code below from your mobile 2FA application:
  207. .caption (e.g. #[a(href='https://authy.com/', target='_blank', noopener) Authy], #[a(href='https://support.google.com/accounts/answer/1066447', target='_blank', noopener) Google Authenticator], #[a(href='https://www.microsoft.com/en-us/account/authenticator', target='_blank', noopener) Microsoft Authenticator], etc.)
  208. .login-tfa-qr.mt-5(v-if='isTFASetupShown', v-html='tfaQRImage')
  209. .subtitle-2.mt-5 2) Enter the security code generated from your trusted device:
  210. v-text-field.login-tfa-field.mt-2(
  211. solo
  212. flat
  213. background-color='white'
  214. hide-details
  215. ref='iptTFASetup'
  216. v-model='securityCode'
  217. :placeholder='$t("auth:tfa.placeholder")'
  218. autocomplete='one-time-code'
  219. @keyup.enter='verifySecurityCode(true)'
  220. )
  221. v-btn.mt-2.text-none(
  222. width='100%'
  223. large
  224. color='primary'
  225. dark
  226. @click='verifySecurityCode(true)'
  227. :loading='isLoading'
  228. ) {{ $t('auth:tfa.verifyToken') }}
  229. loader(v-model='isLoading', :color='loaderColor', :title='loaderTitle', :subtitle='$t(`auth:pleaseWait`)')
  230. notify(style='padding-top: 64px;')
  231. </template>
  232. <script>
  233. /* global siteConfig */
  234. // <span>Photo by <a href="https://unsplash.com/@isaacquesada?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Isaac Quesada</a> on <a href="/t/textures-patterns?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a></span>
  235. import _ from 'lodash'
  236. import Cookies from 'js-cookie'
  237. import gql from 'graphql-tag'
  238. import { sync } from 'vuex-pathify'
  239. export default {
  240. i18nOptions: { namespaces: 'auth' },
  241. props: {
  242. bgUrl: {
  243. type: String,
  244. default: ''
  245. },
  246. hideLocal: {
  247. type: Boolean,
  248. default: false
  249. }
  250. },
  251. data () {
  252. return {
  253. error: false,
  254. strategies: [],
  255. selectedStrategyKey: 'unselected',
  256. selectedStrategy: { key: 'unselected', strategy: { useForm: false, usernameType: 'email' } },
  257. screen: 'login',
  258. username: '',
  259. password: '',
  260. hidePassword: true,
  261. securityCode: '',
  262. continuationToken: '',
  263. isLoading: false,
  264. loaderColor: 'grey darken-4',
  265. loaderTitle: 'Working...',
  266. isShown: false,
  267. newPassword: '',
  268. newPasswordVerify: '',
  269. isTFAShown: false,
  270. isTFASetupShown: false,
  271. tfaQRImage: '',
  272. errorShown: false,
  273. errorMessage: ''
  274. }
  275. },
  276. computed: {
  277. activeModal: sync('editor/activeModal'),
  278. siteTitle () {
  279. return siteConfig.title
  280. },
  281. isSocialShown () {
  282. return this.strategies.length > 1
  283. },
  284. logoUrl () { return siteConfig.logoUrl },
  285. filteredStrategies () {
  286. const qParams = new URLSearchParams(window.location.search)
  287. if (this.hideLocal && !qParams.has('all')) {
  288. return _.reject(this.strategies, ['key', 'local'])
  289. } else {
  290. return this.strategies
  291. }
  292. },
  293. isUsernameEmail () {
  294. return this.selectedStrategy.strategy.usernameType === `email`
  295. }
  296. },
  297. watch: {
  298. filteredStrategies (newValue, oldValue) {
  299. if (_.head(newValue).strategy.useForm) {
  300. this.selectedStrategyKey = _.head(newValue).key
  301. }
  302. },
  303. selectedStrategyKey (newValue, oldValue) {
  304. this.selectedStrategy = _.find(this.strategies, ['key', newValue])
  305. this.screen = 'login'
  306. if (!this.selectedStrategy.strategy.useForm) {
  307. this.isLoading = true
  308. window.location.assign('/login/' + newValue)
  309. } else {
  310. this.$nextTick(() => {
  311. this.$refs.iptEmail.focus()
  312. })
  313. }
  314. }
  315. },
  316. mounted () {
  317. this.isShown = true
  318. },
  319. methods: {
  320. /**
  321. * LOGIN
  322. */
  323. async login () {
  324. this.errorShown = false
  325. if (this.username.length < 2) {
  326. this.errorMessage = this.$t('auth:invalidEmailUsername')
  327. this.errorShown = true
  328. this.$refs.iptEmail.focus()
  329. } else if (this.password.length < 2) {
  330. this.errorMessage = this.$t('auth:invalidPassword')
  331. this.errorShown = true
  332. this.$refs.iptPassword.focus()
  333. } else {
  334. this.loaderColor = 'grey darken-4'
  335. this.loaderTitle = this.$t('auth:signingIn')
  336. this.isLoading = true
  337. try {
  338. const resp = await this.$apollo.mutate({
  339. mutation: gql`
  340. mutation($username: String!, $password: String!, $strategy: String!) {
  341. authentication {
  342. login(username: $username, password: $password, strategy: $strategy) {
  343. responseResult {
  344. succeeded
  345. errorCode
  346. slug
  347. message
  348. }
  349. jwt
  350. mustChangePwd
  351. mustProvideTFA
  352. mustSetupTFA
  353. continuationToken
  354. redirect
  355. tfaQRImage
  356. }
  357. }
  358. }
  359. `,
  360. variables: {
  361. username: this.username,
  362. password: this.password,
  363. strategy: this.selectedStrategy.key
  364. }
  365. })
  366. if (_.has(resp, 'data.authentication.login')) {
  367. const respObj = _.get(resp, 'data.authentication.login', {})
  368. if (respObj.responseResult.succeeded === true) {
  369. this.handleLoginResponse(respObj)
  370. } else {
  371. throw new Error(respObj.responseResult.message)
  372. }
  373. } else {
  374. throw new Error(this.$t('auth:genericError'))
  375. }
  376. } catch (err) {
  377. console.error(err)
  378. this.$store.commit('showNotification', {
  379. style: 'red',
  380. message: err.message,
  381. icon: 'alert'
  382. })
  383. this.isLoading = false
  384. }
  385. }
  386. },
  387. /**
  388. * VERIFY TFA CODE
  389. */
  390. async verifySecurityCode (setup = false) {
  391. if (this.securityCode.length !== 6) {
  392. this.$store.commit('showNotification', {
  393. style: 'red',
  394. message: 'Enter a valid security code.',
  395. icon: 'alert'
  396. })
  397. if (setup) {
  398. this.$refs.iptTFASetup.focus()
  399. } else {
  400. this.$refs.iptTFA.focus()
  401. }
  402. } else {
  403. this.loaderColor = 'grey darken-4'
  404. this.loaderTitle = this.$t('auth:signingIn')
  405. this.isLoading = true
  406. try {
  407. const resp = await this.$apollo.mutate({
  408. mutation: gql`
  409. mutation(
  410. $continuationToken: String!
  411. $securityCode: String!
  412. $setup: Boolean
  413. ) {
  414. authentication {
  415. loginTFA(
  416. continuationToken: $continuationToken
  417. securityCode: $securityCode
  418. setup: $setup
  419. ) {
  420. responseResult {
  421. succeeded
  422. errorCode
  423. slug
  424. message
  425. }
  426. jwt
  427. mustChangePwd
  428. continuationToken
  429. redirect
  430. }
  431. }
  432. }
  433. `,
  434. variables: {
  435. continuationToken: this.continuationToken,
  436. securityCode: this.securityCode,
  437. setup
  438. }
  439. })
  440. if (_.has(resp, 'data.authentication.loginTFA')) {
  441. let respObj = _.get(resp, 'data.authentication.loginTFA', {})
  442. if (respObj.responseResult.succeeded === true) {
  443. this.handleLoginResponse(respObj)
  444. } else {
  445. if (!setup) {
  446. this.isTFAShown = false
  447. }
  448. throw new Error(respObj.responseResult.message)
  449. }
  450. } else {
  451. throw new Error(this.$t('auth:genericError'))
  452. }
  453. } catch (err) {
  454. console.error(err)
  455. this.$store.commit('showNotification', {
  456. style: 'red',
  457. message: err.message,
  458. icon: 'alert'
  459. })
  460. this.isLoading = false
  461. }
  462. }
  463. },
  464. /**
  465. * CHANGE PASSWORD
  466. */
  467. async changePassword () {
  468. this.loaderColor = 'grey darken-4'
  469. this.loaderTitle = this.$t('auth:changePwd.loading')
  470. this.isLoading = true
  471. const resp = await this.$apollo.mutate({
  472. mutation: gql`
  473. {
  474. authentication {
  475. activeStrategies {
  476. key
  477. }
  478. }
  479. }
  480. `,
  481. variables: {
  482. continuationToken: this.continuationToken,
  483. newPassword: this.newPassword
  484. }
  485. })
  486. if (_.get(resp, 'data.authentication.loginChangePassword.responseResult.succeeded', false) === true) {
  487. this.loaderColor = 'green darken-1'
  488. this.loaderTitle = this.$t('auth:loginSuccess')
  489. Cookies.set('jwt', _.get(resp, 'data.authentication.loginChangePassword.jwt', ''), { expires: 365 })
  490. _.delay(() => {
  491. window.location.replace('/') // TEMPORARY - USE RETURNURL
  492. }, 1000)
  493. } else {
  494. this.$store.commit('showNotification', {
  495. style: 'red',
  496. message: _.get(resp, 'data.authentication.loginChangePassword.responseResult.message', false),
  497. icon: 'alert'
  498. })
  499. this.isLoading = false
  500. }
  501. },
  502. /**
  503. * SWITCH TO FORGOT PASSWORD SCREEN
  504. */
  505. forgotPassword () {
  506. this.screen = 'forgot'
  507. this.$nextTick(() => {
  508. this.$refs.iptForgotPwdEmail.focus()
  509. })
  510. },
  511. /**
  512. * FORGOT PASSWORD SUBMIT
  513. */
  514. async forgotPasswordSubmit () {
  515. this.$store.commit('showNotification', {
  516. style: 'pink',
  517. message: 'Coming soon!',
  518. icon: 'ferry'
  519. })
  520. },
  521. handleLoginResponse (respObj) {
  522. this.continuationToken = respObj.continuationToken
  523. if (respObj.mustChangePwd === true) {
  524. this.screen = 'changePwd'
  525. this.$nextTick(() => {
  526. this.$refs.iptNewPassword.focus()
  527. })
  528. this.isLoading = false
  529. } else if (respObj.mustProvideTFA === true) {
  530. this.securityCode = ''
  531. this.isTFAShown = true
  532. setTimeout(() => {
  533. this.$refs.iptTFA.focus()
  534. }, 500)
  535. this.isLoading = false
  536. } else if (respObj.mustSetupTFA === true) {
  537. this.securityCode = ''
  538. this.isTFASetupShown = true
  539. this.tfaQRImage = respObj.tfaQRImage
  540. setTimeout(() => {
  541. this.$refs.iptTFASetup.focus()
  542. }, 500)
  543. this.isLoading = false
  544. } else {
  545. this.loaderColor = 'green darken-1'
  546. this.loaderTitle = this.$t('auth:loginSuccess')
  547. Cookies.set('jwt', respObj.jwt, { expires: 365 })
  548. _.delay(() => {
  549. const loginRedirect = Cookies.get('loginRedirect')
  550. if (loginRedirect) {
  551. Cookies.remove('loginRedirect')
  552. window.location.replace(loginRedirect)
  553. } else if (respObj.redirect) {
  554. window.location.replace(respObj.redirect)
  555. } else {
  556. window.location.replace('/')
  557. }
  558. }, 1000)
  559. }
  560. }
  561. },
  562. apollo: {
  563. strategies: {
  564. query: gql`
  565. {
  566. authentication {
  567. activeStrategies {
  568. key
  569. strategy {
  570. key
  571. logo
  572. color
  573. icon
  574. useForm
  575. usernameType
  576. }
  577. displayName
  578. order
  579. selfRegistration
  580. }
  581. }
  582. }
  583. `,
  584. update: (data) => _.sortBy(data.authentication.activeStrategies, ['order']),
  585. watchLoading (isLoading) {
  586. this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'login-strategies-refresh')
  587. }
  588. }
  589. }
  590. }
  591. </script>
  592. <style lang="scss">
  593. .login {
  594. // background-image: url('/_assets/img/splash/1.jpg');
  595. background-color: mc('grey', '900');
  596. background-size: cover;
  597. background-position: center center;
  598. width: 100%;
  599. height: 100%;
  600. &-sd {
  601. background-color: rgba(255,255,255,.8);
  602. backdrop-filter: blur(10px);
  603. -webkit-backdrop-filter: blur(10px);
  604. border-left: 1px solid rgba(255,255,255,.85);
  605. border-right: 1px solid rgba(255,255,255,.85);
  606. width: 450px;
  607. height: 100%;
  608. margin-left: 5vw;
  609. @at-root .no-backdropfilter & {
  610. background-color: rgba(255,255,255,.95);
  611. }
  612. @include until($tablet) {
  613. margin-left: 0;
  614. width: 100%;
  615. }
  616. }
  617. &-logo {
  618. padding: 12px 0 0 12px;
  619. width: 58px;
  620. height: 58px;
  621. background-color: #222;
  622. margin-left: 12px;
  623. border-bottom-left-radius: 7px;
  624. border-bottom-right-radius: 7px;
  625. }
  626. &-title {
  627. height: 58px;
  628. padding-left: 12px;
  629. display: flex;
  630. align-items: center;
  631. text-shadow: .5px .5px #FFF;
  632. }
  633. &-subtitle {
  634. padding: 24px 12px 12px 12px;
  635. color: #111;
  636. font-weight: 500;
  637. text-shadow: 1px 1px rgba(255,255,255,.5);
  638. background-image: linear-gradient(to bottom, rgba(0,0,0,0), rgba(0,0,0,.15));
  639. text-align: center;
  640. border-bottom: 1px solid rgba(0,0,0,.3);
  641. }
  642. &-info {
  643. border-top: 1px solid rgba(255,255,255,.85);
  644. background-color: rgba(255,255,255,.15);
  645. border-bottom: 1px solid rgba(0,0,0,.15);
  646. padding: 12px;
  647. font-size: 13px;
  648. text-align: center;
  649. }
  650. &-list {
  651. border-top: 1px solid rgba(255,255,255,.85);
  652. padding: 12px;
  653. }
  654. &-form {
  655. padding: 12px;
  656. border-top: 1px solid rgba(255,255,255,.85);
  657. }
  658. &-main {
  659. flex: 1 0 100vw;
  660. height: 100vh;
  661. }
  662. &-tfa {
  663. background-color: #EEE;
  664. border: 7px solid #FFF;
  665. &-field input {
  666. text-align: center;
  667. }
  668. &-qr {
  669. background-color: #FFF;
  670. padding: 5px;
  671. border-radius: 5px;
  672. width: 200px;
  673. height: 200px;
  674. margin: 0 auto;
  675. }
  676. }
  677. }
  678. </style>