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.

514 lines
13 KiB

  1. <template lang="pug">
  2. v-app
  3. .login
  4. .login-container(:class='{ "is-expanded": strategies.length > 1, "is-loading": isLoading }')
  5. .login-mascot
  6. img(src='/svg/henry-reading.svg', alt='Henry')
  7. .login-providers(v-show='strategies.length > 1')
  8. button(v-for='strategy in strategies', :class='{ "is-active": strategy.key === selectedStrategy }', @click='selectStrategy(strategy.key, strategy.useForm)', :title='strategy.title')
  9. em(v-html='strategy.icon')
  10. span {{ strategy.title }}
  11. .login-providers-fill
  12. .login-frame(v-show='screen === "login"')
  13. h1.text-xs-center.display-1 {{ siteTitle }}
  14. h2.text-xs-center.subheading {{ $t('auth:loginRequired') }}
  15. v-text-field(solo, hide-details, ref='iptEmail', v-model='username', :placeholder='$t("auth:fields.emailUser")')
  16. v-text-field.mt-2(
  17. solo
  18. hide-details
  19. ref='iptPassword'
  20. v-model='password'
  21. :append-icon='hidePassword ? "visibility" : "visibility_off"'
  22. :append-icon-cb='() => (hidePassword = !hidePassword)'
  23. :type='hidePassword ? "password" : "text"'
  24. :placeholder='$t("auth:fields.password")'
  25. @keyup.enter='login'
  26. )
  27. v-btn.mt-3(block, large, color='primary', @click='login') {{ $t('auth:actions.login') }}
  28. .login-frame(v-show='screen === "tfa"')
  29. .login-frame-icon
  30. svg.icons.is-48(role='img')
  31. title {{ $t('auth:tfa.title') }}
  32. use(xlink:href='#nc-key')
  33. h2 {{ $t('auth:tfa.subtitle') }}
  34. input(type='text', ref='iptTFA', v-model='securityCode', :placeholder='$t("auth:tfa.placeholder")', @keyup.enter='verifySecurityCode')
  35. button.button.is-blue.is-fullwidth(@click='verifySecurityCode')
  36. span {{ $t('auth:tfa.verifyToken') }}
  37. nav-footer(altbg)
  38. </template>
  39. <script>
  40. /* global siteConfig */
  41. import _ from 'lodash'
  42. import { mapState } from 'vuex'
  43. import strategiesQuery from 'gql/login/login-query-strategies.gql'
  44. import loginMutation from 'gql/login/login-mutation-login.gql'
  45. import tfaMutation from 'gql/login/login-mutation-tfa.gql'
  46. export default {
  47. i18nOptions: { namespaces: 'auth' },
  48. data () {
  49. return {
  50. error: false,
  51. strategies: [],
  52. selectedStrategy: 'local',
  53. screen: 'login',
  54. username: '',
  55. password: '',
  56. hidePassword: true,
  57. securityCode: '',
  58. loginToken: '',
  59. isLoading: false
  60. }
  61. },
  62. computed: {
  63. ...mapState(['notification']),
  64. notificationState: {
  65. get() { return this.notification.isActive },
  66. set(newState) { this.$store.commit('updateNotificationState', newState) }
  67. },
  68. siteTitle () {
  69. return siteConfig.title
  70. }
  71. },
  72. mounted () {
  73. this.$refs.iptEmail.focus()
  74. },
  75. methods: {
  76. /**
  77. * SELECT STRATEGY
  78. */
  79. selectStrategy (key, useForm) {
  80. this.selectedStrategy = key
  81. this.screen = 'login'
  82. if (!useForm) {
  83. this.isLoading = true
  84. window.location.assign(this.$helpers.resolvePath('login/' + key))
  85. } else {
  86. this.$refs.iptEmail.focus()
  87. }
  88. },
  89. /**
  90. * LOGIN
  91. */
  92. async login () {
  93. if (this.username.length < 2) {
  94. this.$store.commit('showNotification', {
  95. style: 'red',
  96. message: 'Enter a valid email / username.',
  97. icon: 'warning'
  98. })
  99. this.$refs.iptEmail.focus()
  100. } else if (this.password.length < 2) {
  101. this.$store.commit('showNotification', {
  102. style: 'red',
  103. message: 'Enter a valid password.',
  104. icon: 'warning'
  105. })
  106. this.$refs.iptPassword.focus()
  107. } else {
  108. this.isLoading = true
  109. try {
  110. let resp = await this.$apollo.mutate({
  111. mutation: loginMutation,
  112. variables: {
  113. username: this.username,
  114. password: this.password,
  115. strategy: this.selectedStrategy
  116. }
  117. })
  118. if (_.has(resp, 'data.authentication.login')) {
  119. let respObj = _.get(resp, 'data.authentication.login', {})
  120. if (respObj.responseResult.succeeded === true) {
  121. if (respObj.tfaRequired === true) {
  122. this.screen = 'tfa'
  123. this.securityCode = ''
  124. this.loginToken = respObj.tfaLoginToken
  125. this.$nextTick(() => {
  126. this.$refs.iptTFA.focus()
  127. })
  128. } else {
  129. this.$store.commit('showNotification', {
  130. message: 'Login Successful! Redirecting...',
  131. style: 'success',
  132. icon: 'check'
  133. })
  134. _.delay(() => {
  135. window.location.replace('/') // TEMPORARY - USE RETURNURL
  136. }, 1000)
  137. }
  138. this.isLoading = false
  139. } else {
  140. throw new Error(respObj.responseResult.message)
  141. }
  142. } else {
  143. throw new Error('Authentication is unavailable.')
  144. }
  145. } catch (err) {
  146. console.error(err)
  147. this.$store.commit('showNotification', {
  148. style: 'red',
  149. message: err.message,
  150. icon: 'warning'
  151. })
  152. this.isLoading = false
  153. }
  154. }
  155. },
  156. /**
  157. * VERIFY TFA CODE
  158. */
  159. verifySecurityCode () {
  160. if (this.securityCode.length !== 6) {
  161. this.$store.commit('showNotification', {
  162. style: 'red',
  163. message: 'Enter a valid security code.',
  164. icon: 'warning'
  165. })
  166. this.$refs.iptTFA.focus()
  167. } else {
  168. this.isLoading = true
  169. this.$apollo.mutate({
  170. mutation: tfaMutation,
  171. variables: {
  172. loginToken: this.loginToken,
  173. securityCode: this.securityCode
  174. }
  175. }).then(resp => {
  176. if (_.has(resp, 'data.authentication.loginTFA')) {
  177. let respObj = _.get(resp, 'data.authentication.loginTFA', {})
  178. if (respObj.responseResult.succeeded === true) {
  179. this.$store.commit('showNotification', {
  180. message: 'Login successful!',
  181. style: 'success',
  182. icon: 'check'
  183. })
  184. _.delay(() => {
  185. window.location.replace('/') // TEMPORARY - USE RETURNURL
  186. }, 1000)
  187. this.isLoading = false
  188. } else {
  189. throw new Error(respObj.responseResult.message)
  190. }
  191. } else {
  192. throw new Error('Authentication is unavailable.')
  193. }
  194. }).catch(err => {
  195. console.error(err)
  196. this.$store.commit('showNotification', {
  197. style: 'red',
  198. message: err.message,
  199. icon: 'warning'
  200. })
  201. this.isLoading = false
  202. })
  203. }
  204. }
  205. },
  206. apollo: {
  207. strategies: {
  208. query: strategiesQuery,
  209. update: (data) => data.authentication.strategies,
  210. watchLoading (isLoading) {
  211. this.isLoading = isLoading
  212. this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'login-strategies-refresh')
  213. }
  214. }
  215. }
  216. }
  217. </script>
  218. <style lang="scss">
  219. .login {
  220. background-color: mc('blue', '800');
  221. background-image: url('../static/svg/motif-blocks.svg');
  222. background-repeat: repeat;
  223. background-size: 200px;
  224. width: 100%;
  225. height: 100%;
  226. display: flex;
  227. align-items: center;
  228. justify-content: center;
  229. animation: loginBgReveal 20s linear infinite;
  230. @include keyframes(loginBgReveal) {
  231. 0% {
  232. background-position-y: 0;
  233. }
  234. 100% {
  235. background-position-y: -800px;
  236. }
  237. }
  238. &::before {
  239. content: '';
  240. position: absolute;
  241. background-color: #0d47a1;
  242. background-image: url('../static/svg/motif-overlay.svg');
  243. background-attachment: fixed;
  244. background-size: cover;
  245. opacity: .5;
  246. top: 0;
  247. left: 0;
  248. width: 100vw;
  249. height: 100vh;
  250. }
  251. &::after {
  252. content: '';
  253. position: absolute;
  254. background-image: linear-gradient(to bottom, rgba(mc('blue', '800'), .9) 0%, rgba(mc('blue', '800'), 0) 100%);
  255. top: 0;
  256. left: 0;
  257. width: 100vw;
  258. height: 25vh;
  259. z-index: 1;
  260. }
  261. &-mascot {
  262. width: 200px;
  263. height: 200px;
  264. position: absolute;
  265. top: -180px;
  266. left: 50%;
  267. margin-left: -100px;
  268. z-index: 10;
  269. @include until($tablet) {
  270. display: none;
  271. }
  272. }
  273. &-container {
  274. position: relative;
  275. display: flex;
  276. width: 400px;
  277. align-items: stretch;
  278. box-shadow: 0 14px 28px rgba(0,0,0,0.2);
  279. border-radius: 6px;
  280. animation: zoomIn .5s ease;
  281. z-index: 2;
  282. &::after {
  283. position: absolute;
  284. top: 1rem;
  285. right: 1rem;
  286. content: " ";
  287. @include spinner(mc('blue', '500'),0.5s,16px);
  288. display: none;
  289. }
  290. &.is-expanded {
  291. width: 650px;
  292. .login-frame {
  293. border-radius: 0 6px 6px 0;
  294. border-left: none;
  295. @include until($tablet) {
  296. border-radius: 0;
  297. }
  298. }
  299. }
  300. &.is-loading::after {
  301. display: block;
  302. }
  303. @include until($tablet) {
  304. width: 95vw;
  305. border-radius: 0;
  306. &.is-expanded {
  307. width: 95vw;
  308. }
  309. }
  310. }
  311. &-providers {
  312. display: flex;
  313. flex-direction: column;
  314. width: 250px;
  315. border-right: none;
  316. border-radius: 6px 0 0 6px;
  317. z-index: 1;
  318. overflow: hidden;
  319. @include until($tablet) {
  320. width: 50px;
  321. border-radius: 0;
  322. }
  323. button {
  324. flex: 0 1 50px;
  325. padding: 5px 15px;
  326. border: none;
  327. color: #FFF;
  328. // background: linear-gradient(to right, rgba(mc('light-blue', '800'), .7), rgba(mc('light-blue', '800'), 1));
  329. // border-top: 1px solid rgba(mc('light-blue', '900'), .5);
  330. background: linear-gradient(to right, rgba(0,0,0, .5), rgba(0,0,0, .7));
  331. border-top: 1px solid rgba(0,0,0, .2);
  332. font-weight: 600;
  333. text-align: left;
  334. min-height: 40px;
  335. display: flex;
  336. justify-content: flex-start;
  337. align-items: center;
  338. transition: all .4s ease;
  339. &:focus {
  340. outline: none;
  341. }
  342. @include until($tablet) {
  343. justify-content: center;
  344. }
  345. &:hover {
  346. background-color: rgba(0,0,0, .4);
  347. }
  348. &:first-child {
  349. border-top: none;
  350. &.is-active {
  351. border-top: 1px solid rgba(255,255,255, .5);
  352. }
  353. }
  354. &.is-active {
  355. background-image: linear-gradient(to right, rgba(255,255,255,1) 0%,rgba(255,255,255,.77) 100%);
  356. color: mc('grey', '800');
  357. cursor: default;
  358. &:hover {
  359. background-color: transparent;
  360. }
  361. svg path {
  362. fill: mc('grey', '800');
  363. }
  364. }
  365. i {
  366. margin-right: 10px;
  367. font-size: 16px;
  368. @include until($tablet) {
  369. margin-right: 0;
  370. font-size: 20px;
  371. }
  372. }
  373. svg {
  374. margin-right: 10px;
  375. width: auto;
  376. height: 20px;
  377. max-width: 18px;
  378. max-height: 20px;
  379. path {
  380. fill: #FFF;
  381. }
  382. @include until($tablet) {
  383. margin-right: 0;
  384. font-size: 20px;
  385. }
  386. }
  387. em {
  388. height: 20px;
  389. }
  390. span {
  391. font-weight: 600;
  392. @include until($tablet) {
  393. display: none;
  394. }
  395. }
  396. }
  397. &-fill {
  398. flex: 1 1 0;
  399. background: linear-gradient(to right, rgba(mc('light-blue', '800'), .7), rgba(mc('light-blue', '800'), 1));
  400. }
  401. }
  402. &-frame {
  403. background-image: radial-gradient(circle at top center, rgba(255,255,255,1) 5%,rgba(255,255,255,.6) 100%);
  404. border: 1px solid rgba(255,255,255, .5);
  405. border-radius: 6px;
  406. width: 400px;
  407. padding: 1rem;
  408. color: mc('grey', '700');
  409. display: block;
  410. @include until($tablet) {
  411. width: 100%;
  412. border-radius: 0;
  413. border: none;
  414. }
  415. h1 {
  416. font-size: 2rem;
  417. font-weight: 400;
  418. color: mc('light-blue', '700');
  419. text-shadow: 1px 1px 0 #FFF;
  420. padding: 1rem 0 0 0;
  421. margin: 0;
  422. }
  423. h2 {
  424. font-size: 1.5rem;
  425. font-weight: 300;
  426. color: mc('grey', '700');
  427. text-shadow: 1px 1px 0 #FFF;
  428. padding: 0;
  429. margin: 0 0 25px 0;
  430. }
  431. }
  432. &-tfa {
  433. position: relative;
  434. display: flex;
  435. width: 400px;
  436. align-items: stretch;
  437. box-shadow: 0 14px 28px rgba(0,0,0,0.2);
  438. border-radius: 6px;
  439. animation: zoomIn .5s ease;
  440. }
  441. &-copyright {
  442. display: flex;
  443. align-items: center;
  444. justify-content: center;
  445. position: absolute;
  446. left: 0;
  447. bottom: 10vh;
  448. width: 100%;
  449. z-index: 2;
  450. color: mc('grey', '500');
  451. font-weight: 400;
  452. a {
  453. font-weight: 600;
  454. color: mc('blue', '500');
  455. margin-left: .25rem;
  456. @include until($tablet) {
  457. color: mc('blue', '200');
  458. }
  459. }
  460. @include until($tablet) {
  461. color: mc('blue', '50');
  462. }
  463. }
  464. }
  465. </style>