login.vue 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775
  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. changePwdContinuationToken: {
  251. type: String,
  252. default: null
  253. }
  254. },
  255. data () {
  256. return {
  257. error: false,
  258. strategies: [],
  259. selectedStrategyKey: 'unselected',
  260. selectedStrategy: { key: 'unselected', strategy: { useForm: false, usernameType: 'email' } },
  261. screen: 'login',
  262. username: '',
  263. password: '',
  264. hidePassword: true,
  265. securityCode: '',
  266. continuationToken: '',
  267. isLoading: false,
  268. loaderColor: 'grey darken-4',
  269. loaderTitle: 'Working...',
  270. isShown: false,
  271. newPassword: '',
  272. newPasswordVerify: '',
  273. isTFAShown: false,
  274. isTFASetupShown: false,
  275. tfaQRImage: '',
  276. errorShown: false,
  277. errorMessage: ''
  278. }
  279. },
  280. computed: {
  281. activeModal: sync('editor/activeModal'),
  282. siteTitle () {
  283. return siteConfig.title
  284. },
  285. isSocialShown () {
  286. return this.strategies.length > 1
  287. },
  288. logoUrl () { return siteConfig.logoUrl },
  289. filteredStrategies () {
  290. const qParams = new URLSearchParams(window.location.search)
  291. if (this.hideLocal && !qParams.has('all')) {
  292. return _.reject(this.strategies, ['key', 'local'])
  293. } else {
  294. return this.strategies
  295. }
  296. },
  297. isUsernameEmail () {
  298. return this.selectedStrategy.strategy.usernameType === `email`
  299. }
  300. },
  301. watch: {
  302. filteredStrategies (newValue, oldValue) {
  303. if (_.head(newValue).strategy.useForm) {
  304. this.selectedStrategyKey = _.head(newValue).key
  305. }
  306. },
  307. selectedStrategyKey (newValue, oldValue) {
  308. this.selectedStrategy = _.find(this.strategies, ['key', newValue])
  309. if (this.screen === 'changePwd') {
  310. return
  311. }
  312. this.screen = 'login'
  313. if (!this.selectedStrategy.strategy.useForm) {
  314. this.isLoading = true
  315. window.location.assign('/login/' + newValue)
  316. } else {
  317. this.$nextTick(() => {
  318. this.$refs.iptEmail.focus()
  319. })
  320. }
  321. }
  322. },
  323. mounted () {
  324. this.isShown = true
  325. if (this.changePwdContinuationToken) {
  326. this.screen = 'changePwd'
  327. this.continuationToken = this.changePwdContinuationToken
  328. }
  329. },
  330. methods: {
  331. /**
  332. * LOGIN
  333. */
  334. async login () {
  335. this.errorShown = false
  336. if (this.username.length < 2) {
  337. this.errorMessage = this.$t('auth:invalidEmailUsername')
  338. this.errorShown = true
  339. this.$refs.iptEmail.focus()
  340. } else if (this.password.length < 2) {
  341. this.errorMessage = this.$t('auth:invalidPassword')
  342. this.errorShown = true
  343. this.$refs.iptPassword.focus()
  344. } else {
  345. this.loaderColor = 'grey darken-4'
  346. this.loaderTitle = this.$t('auth:signingIn')
  347. this.isLoading = true
  348. try {
  349. const resp = await this.$apollo.mutate({
  350. mutation: gql`
  351. mutation($username: String!, $password: String!, $strategy: String!) {
  352. authentication {
  353. login(username: $username, password: $password, strategy: $strategy) {
  354. responseResult {
  355. succeeded
  356. errorCode
  357. slug
  358. message
  359. }
  360. jwt
  361. mustChangePwd
  362. mustProvideTFA
  363. mustSetupTFA
  364. continuationToken
  365. redirect
  366. tfaQRImage
  367. }
  368. }
  369. }
  370. `,
  371. variables: {
  372. username: this.username,
  373. password: this.password,
  374. strategy: this.selectedStrategy.key
  375. }
  376. })
  377. if (_.has(resp, 'data.authentication.login')) {
  378. const respObj = _.get(resp, 'data.authentication.login', {})
  379. if (respObj.responseResult.succeeded === true) {
  380. this.handleLoginResponse(respObj)
  381. } else {
  382. throw new Error(respObj.responseResult.message)
  383. }
  384. } else {
  385. throw new Error(this.$t('auth:genericError'))
  386. }
  387. } catch (err) {
  388. console.error(err)
  389. this.$store.commit('showNotification', {
  390. style: 'red',
  391. message: err.message,
  392. icon: 'alert'
  393. })
  394. this.isLoading = false
  395. }
  396. }
  397. },
  398. /**
  399. * VERIFY TFA CODE
  400. */
  401. async verifySecurityCode (setup = false) {
  402. if (this.securityCode.length !== 6) {
  403. this.$store.commit('showNotification', {
  404. style: 'red',
  405. message: 'Enter a valid security code.',
  406. icon: 'alert'
  407. })
  408. if (setup) {
  409. this.$refs.iptTFASetup.focus()
  410. } else {
  411. this.$refs.iptTFA.focus()
  412. }
  413. } else {
  414. this.loaderColor = 'grey darken-4'
  415. this.loaderTitle = this.$t('auth:signingIn')
  416. this.isLoading = true
  417. try {
  418. const resp = await this.$apollo.mutate({
  419. mutation: gql`
  420. mutation(
  421. $continuationToken: String!
  422. $securityCode: String!
  423. $setup: Boolean
  424. ) {
  425. authentication {
  426. loginTFA(
  427. continuationToken: $continuationToken
  428. securityCode: $securityCode
  429. setup: $setup
  430. ) {
  431. responseResult {
  432. succeeded
  433. errorCode
  434. slug
  435. message
  436. }
  437. jwt
  438. mustChangePwd
  439. continuationToken
  440. redirect
  441. }
  442. }
  443. }
  444. `,
  445. variables: {
  446. continuationToken: this.continuationToken,
  447. securityCode: this.securityCode,
  448. setup
  449. }
  450. })
  451. if (_.has(resp, 'data.authentication.loginTFA')) {
  452. let respObj = _.get(resp, 'data.authentication.loginTFA', {})
  453. if (respObj.responseResult.succeeded === true) {
  454. this.handleLoginResponse(respObj)
  455. } else {
  456. if (!setup) {
  457. this.isTFAShown = false
  458. }
  459. throw new Error(respObj.responseResult.message)
  460. }
  461. } else {
  462. throw new Error(this.$t('auth:genericError'))
  463. }
  464. } catch (err) {
  465. console.error(err)
  466. this.$store.commit('showNotification', {
  467. style: 'red',
  468. message: err.message,
  469. icon: 'alert'
  470. })
  471. this.isLoading = false
  472. }
  473. }
  474. },
  475. /**
  476. * CHANGE PASSWORD
  477. */
  478. async changePassword () {
  479. this.loaderColor = 'grey darken-4'
  480. this.loaderTitle = this.$t('auth:changePwd.loading')
  481. this.isLoading = true
  482. try {
  483. const resp = await this.$apollo.mutate({
  484. mutation: gql`
  485. mutation (
  486. $continuationToken: String!
  487. $newPassword: String!
  488. ) {
  489. authentication {
  490. loginChangePassword (
  491. continuationToken: $continuationToken
  492. newPassword: $newPassword
  493. ) {
  494. responseResult {
  495. succeeded
  496. errorCode
  497. slug
  498. message
  499. }
  500. jwt
  501. continuationToken
  502. redirect
  503. }
  504. }
  505. }
  506. `,
  507. variables: {
  508. continuationToken: this.continuationToken,
  509. newPassword: this.newPassword
  510. }
  511. })
  512. if (_.has(resp, 'data.authentication.loginChangePassword')) {
  513. let respObj = _.get(resp, 'data.authentication.loginChangePassword', {})
  514. if (respObj.responseResult.succeeded === true) {
  515. this.handleLoginResponse(respObj)
  516. } else {
  517. throw new Error(respObj.responseResult.message)
  518. }
  519. } else {
  520. throw new Error(this.$t('auth:genericError'))
  521. }
  522. } catch (err) {
  523. console.error(err)
  524. this.$store.commit('showNotification', {
  525. style: 'red',
  526. message: err.message,
  527. icon: 'alert'
  528. })
  529. this.isLoading = false
  530. }
  531. },
  532. /**
  533. * SWITCH TO FORGOT PASSWORD SCREEN
  534. */
  535. forgotPassword () {
  536. this.screen = 'forgot'
  537. this.$nextTick(() => {
  538. this.$refs.iptForgotPwdEmail.focus()
  539. })
  540. },
  541. /**
  542. * FORGOT PASSWORD SUBMIT
  543. */
  544. async forgotPasswordSubmit () {
  545. this.loaderColor = 'grey darken-4'
  546. this.loaderTitle = this.$t('auth:forgotPasswordLoading')
  547. this.isLoading = true
  548. try {
  549. const resp = await this.$apollo.mutate({
  550. mutation: gql`
  551. mutation (
  552. $email: String!
  553. ) {
  554. authentication {
  555. forgotPassword (
  556. email: $email
  557. ) {
  558. responseResult {
  559. succeeded
  560. errorCode
  561. slug
  562. message
  563. }
  564. }
  565. }
  566. }
  567. `,
  568. variables: {
  569. email: this.username
  570. }
  571. })
  572. if (_.has(resp, 'data.authentication.forgotPassword.responseResult')) {
  573. let respObj = _.get(resp, 'data.authentication.forgotPassword.responseResult', {})
  574. if (respObj.succeeded === true) {
  575. this.$store.commit('showNotification', {
  576. style: 'success',
  577. message: this.$t('auth:forgotPasswordSuccess'),
  578. icon: 'email'
  579. })
  580. this.screen = 'login'
  581. } else {
  582. throw new Error(respObj.message)
  583. }
  584. } else {
  585. throw new Error(this.$t('auth:genericError'))
  586. }
  587. } catch (err) {
  588. console.error(err)
  589. this.$store.commit('showNotification', {
  590. style: 'red',
  591. message: err.message,
  592. icon: 'alert'
  593. })
  594. }
  595. this.isLoading = false
  596. },
  597. handleLoginResponse (respObj) {
  598. this.continuationToken = respObj.continuationToken
  599. if (respObj.mustChangePwd === true) {
  600. this.screen = 'changePwd'
  601. this.$nextTick(() => {
  602. this.$refs.iptNewPassword.focus()
  603. })
  604. this.isLoading = false
  605. } else if (respObj.mustProvideTFA === true) {
  606. this.securityCode = ''
  607. this.isTFAShown = true
  608. setTimeout(() => {
  609. this.$refs.iptTFA.focus()
  610. }, 500)
  611. this.isLoading = false
  612. } else if (respObj.mustSetupTFA === true) {
  613. this.securityCode = ''
  614. this.isTFASetupShown = true
  615. this.tfaQRImage = respObj.tfaQRImage
  616. setTimeout(() => {
  617. this.$refs.iptTFASetup.focus()
  618. }, 500)
  619. this.isLoading = false
  620. } else {
  621. this.loaderColor = 'green darken-1'
  622. this.loaderTitle = this.$t('auth:loginSuccess')
  623. Cookies.set('jwt', respObj.jwt, { expires: 365 })
  624. _.delay(() => {
  625. const loginRedirect = Cookies.get('loginRedirect')
  626. if (loginRedirect) {
  627. Cookies.remove('loginRedirect')
  628. window.location.replace(loginRedirect)
  629. } else if (respObj.redirect) {
  630. window.location.replace(respObj.redirect)
  631. } else {
  632. window.location.replace('/')
  633. }
  634. }, 1000)
  635. }
  636. }
  637. },
  638. apollo: {
  639. strategies: {
  640. query: gql`
  641. {
  642. authentication {
  643. activeStrategies {
  644. key
  645. strategy {
  646. key
  647. logo
  648. color
  649. icon
  650. useForm
  651. usernameType
  652. }
  653. displayName
  654. order
  655. selfRegistration
  656. }
  657. }
  658. }
  659. `,
  660. update: (data) => _.sortBy(data.authentication.activeStrategies, ['order']),
  661. watchLoading (isLoading) {
  662. this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'login-strategies-refresh')
  663. }
  664. }
  665. }
  666. }
  667. </script>
  668. <style lang="scss">
  669. .login {
  670. // background-image: url('/_assets/img/splash/1.jpg');
  671. background-color: mc('grey', '900');
  672. background-size: cover;
  673. background-position: center center;
  674. width: 100%;
  675. height: 100%;
  676. &-sd {
  677. background-color: rgba(255,255,255,.8);
  678. backdrop-filter: blur(10px);
  679. -webkit-backdrop-filter: blur(10px);
  680. border-left: 1px solid rgba(255,255,255,.85);
  681. border-right: 1px solid rgba(255,255,255,.85);
  682. width: 450px;
  683. height: 100%;
  684. margin-left: 5vw;
  685. @at-root .no-backdropfilter & {
  686. background-color: rgba(255,255,255,.95);
  687. }
  688. @include until($tablet) {
  689. margin-left: 0;
  690. width: 100%;
  691. }
  692. }
  693. &-logo {
  694. padding: 12px 0 0 12px;
  695. width: 58px;
  696. height: 58px;
  697. background-color: #222;
  698. margin-left: 12px;
  699. border-bottom-left-radius: 7px;
  700. border-bottom-right-radius: 7px;
  701. }
  702. &-title {
  703. height: 58px;
  704. padding-left: 12px;
  705. display: flex;
  706. align-items: center;
  707. text-shadow: .5px .5px #FFF;
  708. }
  709. &-subtitle {
  710. padding: 24px 12px 12px 12px;
  711. color: #111;
  712. font-weight: 500;
  713. text-shadow: 1px 1px rgba(255,255,255,.5);
  714. background-image: linear-gradient(to bottom, rgba(0,0,0,0), rgba(0,0,0,.15));
  715. text-align: center;
  716. border-bottom: 1px solid rgba(0,0,0,.3);
  717. }
  718. &-info {
  719. border-top: 1px solid rgba(255,255,255,.85);
  720. background-color: rgba(255,255,255,.15);
  721. border-bottom: 1px solid rgba(0,0,0,.15);
  722. padding: 12px;
  723. font-size: 13px;
  724. text-align: center;
  725. }
  726. &-list {
  727. border-top: 1px solid rgba(255,255,255,.85);
  728. padding: 12px;
  729. }
  730. &-form {
  731. padding: 12px;
  732. border-top: 1px solid rgba(255,255,255,.85);
  733. }
  734. &-main {
  735. flex: 1 0 100vw;
  736. height: 100vh;
  737. }
  738. &-tfa {
  739. background-color: #EEE;
  740. border: 7px solid #FFF;
  741. &-field input {
  742. text-align: center;
  743. }
  744. &-qr {
  745. background-color: #FFF;
  746. padding: 5px;
  747. border-radius: 5px;
  748. width: 200px;
  749. height: 200px;
  750. margin: 0 auto;
  751. }
  752. }
  753. }
  754. </style>