TwofaccountShow.vue 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. <template>
  2. <div>
  3. <figure class="image is-64x64" :class="{ 'no-icon': !internal_icon }" style="display: inline-block">
  4. <img :src="'storage/icons/' + internal_icon" v-if="internal_icon">
  5. </figure>
  6. <p class="is-size-4 has-text-grey-light has-ellipsis">{{ internal_service }}</p>
  7. <p class="is-size-6 has-text-grey has-ellipsis">{{ internal_account }}</p>
  8. <p id="otp" class="is-size-1 has-text-white" :title="$t('commons.copy_to_clipboard')" v-clipboard="() => otp.replace(/ /g, '')" v-clipboard:success="clipboardSuccessHandler">{{ displayedOtp }}</p>
  9. <ul class="dots" v-if="type === 'totp'">
  10. <li v-for="n in 30"></li>
  11. </ul>
  12. <ul v-else-if="type === 'hotp'">
  13. <li>counter: {{ counter }}</li>
  14. </ul>
  15. </div>
  16. </template>
  17. <script>
  18. export default {
  19. data() {
  20. return {
  21. id: null,
  22. internal_service: '',
  23. internal_account: '',
  24. internal_uri: '',
  25. next_uri: '',
  26. internal_icon: '',
  27. type: '',
  28. otp : '',
  29. timerID: null,
  30. position: null,
  31. counter: null,
  32. }
  33. },
  34. props: {
  35. service: '',
  36. account: '',
  37. uri : '',
  38. icon: ''
  39. },
  40. computed: {
  41. displayedOtp() {
  42. return appSettings.showTokenAsDot ? this.otp.replace(/[0-9]/g, '●') : this.otp
  43. }
  44. },
  45. mounted: function() {
  46. this.showAccount()
  47. },
  48. methods: {
  49. async showAccount(id) {
  50. // 2 possible cases :
  51. // - ID is provided so we fetch the account data from db but without the uri.
  52. // This prevent the uri (a sensitive data) to transit via http request unnecessarily. In this
  53. // case this.type is sent by the backend.
  54. // - the URI prop has been set via the create form, we need to preview some OTP before storing the account.
  55. // So this.type is set on client side from the provided URI
  56. this.id = id
  57. if( this.id || this.uri ) {
  58. if( this.id ) {
  59. const { data } = await this.axios.get('api/twofaccounts/' + this.id)
  60. this.internal_service = data.service
  61. this.internal_account = data.account
  62. this.internal_icon = data.icon
  63. this.type = data.type
  64. }
  65. else {
  66. this.internal_service = this.service
  67. this.internal_account = this.account
  68. this.internal_icon = this.icon
  69. this.internal_uri = this.uri
  70. this.type = this.internal_uri.slice(0, 15 ) === "otpauth://totp/" ? 'totp' : 'hotp';
  71. }
  72. this.type === 'totp' ? await this.getTOTP() : await this.getHOTP()
  73. this.$parent.isActive = true
  74. }
  75. },
  76. getTOTP: function() {
  77. this.axios.post('api/twofaccounts/otp', {data: this.id ? this.id : this.internal_uri }).then(response => {
  78. let spacePosition = Math.ceil(response.data.otp.length / 2);
  79. this.otp = response.data.otp.substr(0, spacePosition) + " " + response.data.otp.substr(spacePosition);
  80. this.position = response.data.position;
  81. let dots = this.$el.querySelector('.dots');
  82. // clear active dots
  83. while (dots.querySelector('[data-is-active]')) {
  84. dots.querySelector('[data-is-active]').removeAttribute('data-is-active');
  85. }
  86. // set dot at given position as the active one
  87. let active = dots.querySelector('li:nth-child(' + (this.position + 1 ) + ')');
  88. active.setAttribute('data-is-active', true);
  89. let self = this;
  90. this.timerID = setInterval(function() {
  91. let sibling = active.nextSibling;
  92. if(active.nextSibling === null) {
  93. console.log('no more sibling to activate, we refresh the OTP')
  94. self.stopLoop()
  95. self.getTOTP();
  96. }
  97. else
  98. {
  99. active.removeAttribute('data-is-active');
  100. sibling.setAttribute('data-is-active', true);
  101. active = sibling
  102. }
  103. }, 1000);
  104. })
  105. },
  106. getHOTP: function() {
  107. this.axios.post('api/twofaccounts/otp', {data: this.id ? this.id : this.internal_uri }).then(response => {
  108. let spacePosition = Math.ceil(response.data.otp.length / 2);
  109. this.otp = response.data.otp.substr(0, spacePosition) + " " + response.data.otp.substr(spacePosition)
  110. this.counter = response.data.counter
  111. this.next_uri = response.data.nextUri
  112. })
  113. },
  114. clearOTP: function() {
  115. this.stopLoop()
  116. this.id = this.timerID = this.position = this.counter = null
  117. this.internal_service = this.internal_account = this.internal_icon = this.internal_uri = ''
  118. this.otp = '... ...'
  119. try {
  120. this.$el.querySelector('[data-is-active]').removeAttribute('data-is-active');
  121. this.$el.querySelector('.dots li:first-child').setAttribute('data-is-active', true);
  122. }
  123. catch(e) {
  124. // we do not throw anything
  125. }
  126. },
  127. stopLoop: function() {
  128. if( this.type === 'totp' ) {
  129. clearInterval(this.timerID)
  130. }
  131. },
  132. clipboardSuccessHandler ({ value, event }) {
  133. console.log('success', value)
  134. },
  135. clipboardErrorHandler ({ value, event }) {
  136. console.log('error', value)
  137. }
  138. },
  139. beforeDestroy () {
  140. this.stopLoop()
  141. }
  142. }
  143. </script>