Edit.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. <template>
  2. <form-wrapper :title="$t('twofaccounts.forms.edit_account')">
  3. <form @submit.prevent="updateAccount" @keydown="form.onKeydown($event)">
  4. <!-- service -->
  5. <form-field :isDisabled="form.otp_type === 'steamtotp'" :form="form" fieldName="service" inputType="text" :label="$t('twofaccounts.service')" :placeholder="$t('twofaccounts.forms.service.placeholder')" autofocus />
  6. <!-- account -->
  7. <form-field :form="form" fieldName="account" inputType="text" :label="$t('twofaccounts.account')" :placeholder="$t('twofaccounts.forms.account.placeholder')" />
  8. <!-- icon -->
  9. <div class="field">
  10. <label class="label">{{ $t('twofaccounts.icon') }}</label>
  11. <div class="file is-dark">
  12. <label class="file-label">
  13. <input class="file-input" type="file" accept="image/*" v-on:change="uploadIcon" ref="iconInput">
  14. <span class="file-cta">
  15. <span class="file-icon">
  16. <font-awesome-icon :icon="['fas', 'image']" />
  17. </span>
  18. <span class="file-label">{{ $t('twofaccounts.forms.choose_image') }}</span>
  19. </span>
  20. </label>
  21. <span class="tag is-black is-large" v-if="tempIcon">
  22. <img class="icon-preview" :src="'/storage/icons/' + tempIcon" >
  23. <button class="delete is-small" @click.prevent="deleteIcon"></button>
  24. </span>
  25. </div>
  26. </div>
  27. <field-error :form="form" field="icon" class="help-for-file" />
  28. <!-- otp type -->
  29. <form-toggle class="has-uppercased-button" :isDisabled="true" :form="form" :choices="otp_types" fieldName="otp_type" :label="$t('twofaccounts.forms.otp_type.label')" :help="$t('twofaccounts.forms.otp_type.help')" :hasOffset="true" />
  30. <div v-if="form.otp_type">
  31. <!-- secret -->
  32. <label class="label" v-html="$t('twofaccounts.forms.secret.label')"></label>
  33. <div class="field has-addons">
  34. <p v-if="!secretIsLocked" class="control">
  35. <span class="select">
  36. <select @change="form.secret=''" v-model="secretIsBase32Encoded">
  37. <option v-for="format in secretFormats" :value="format.value">{{ format.text }}</option>
  38. </select>
  39. </span>
  40. </p>
  41. <p class="control is-expanded">
  42. <input class="input" type="text" v-model="form.secret" :disabled="secretIsLocked">
  43. </p>
  44. <p class="control" v-if="secretIsLocked">
  45. <a class="button is-dark field-lock" @click="secretIsLocked = false" :title="$t('twofaccounts.forms.unlock.title')">
  46. <span class="icon">
  47. <font-awesome-icon :icon="['fas', 'lock']" />
  48. </span>
  49. </a>
  50. </p>
  51. <p class="control" v-else>
  52. <a class="button is-dark field-unlock" @click="secretIsLocked = true" :title="$t('twofaccounts.forms.lock.title')">
  53. <span class="icon has-text-danger">
  54. <font-awesome-icon :icon="['fas', 'lock-open']" />
  55. </span>
  56. </a>
  57. </p>
  58. </div>
  59. <div class="field">
  60. <field-error :form="form" field="secret" class="help-for-file" />
  61. <p class="help" v-html="$t('twofaccounts.forms.secret.help')"></p>
  62. </div>
  63. <div v-if="form.otp_type !== 'steamtotp'">
  64. <h2 class="title is-4 mt-5 mb-2">{{ $t('commons.options') }}</h2>
  65. <p class="help mb-4">
  66. {{ $t('twofaccounts.forms.options_help') }}
  67. </p>
  68. <!-- digits -->
  69. <form-toggle :form="form" :choices="digitsChoices" fieldName="digits" :label="$t('twofaccounts.forms.digits.label')" :help="$t('twofaccounts.forms.digits.help')" />
  70. <!-- algorithm -->
  71. <form-toggle :form="form" :choices="algorithms" fieldName="algorithm" :label="$t('twofaccounts.forms.algorithm.label')" :help="$t('twofaccounts.forms.algorithm.help')" />
  72. <!-- TOTP period -->
  73. <form-field v-if="form.otp_type === 'totp'" :form="form" fieldName="period" inputType="text" :label="$t('twofaccounts.forms.period.label')" :placeholder="$t('twofaccounts.forms.period.placeholder')" :help="$t('twofaccounts.forms.period.help')" />
  74. <!-- HOTP counter -->
  75. <div v-if="form.otp_type === 'hotp'">
  76. <div class="field" style="margin-bottom: 0.5rem;">
  77. <label class="label">{{ $t('twofaccounts.forms.counter.label') }}</label>
  78. </div>
  79. <div class="field has-addons">
  80. <div class="control is-expanded">
  81. <input class="input" type="text" placeholder="" v-model="form.counter" :disabled="counterIsLocked" />
  82. </div>
  83. <div class="control" v-if="counterIsLocked">
  84. <a class="button is-dark field-lock" @click="counterIsLocked = false" :title="$t('twofaccounts.forms.unlock.title')">
  85. <span class="icon">
  86. <font-awesome-icon :icon="['fas', 'lock']" />
  87. </span>
  88. </a>
  89. </div>
  90. <div class="control" v-else>
  91. <a class="button is-dark field-unlock" @click="counterIsLocked = true" :title="$t('twofaccounts.forms.lock.title')">
  92. <span class="icon has-text-danger">
  93. <font-awesome-icon :icon="['fas', 'lock-open']" />
  94. </span>
  95. </a>
  96. </div>
  97. </div>
  98. <field-error :form="form" field="counter" />
  99. <p class="help" v-html="$t('twofaccounts.forms.counter.help_lock')"></p>
  100. </div>
  101. </div>
  102. </div>
  103. <!-- form buttons -->
  104. <vue-footer :showButtons="true">
  105. <p class="control">
  106. <v-button :isLoading="form.isBusy" class="is-rounded" >{{ $t('commons.save') }}</v-button>
  107. </p>
  108. <p class="control" v-if="form.otp_type && form.secret">
  109. <button type="button" class="button is-success is-rounded" @click="previewAccount">{{ $t('twofaccounts.forms.test') }}</button>
  110. </p>
  111. <p class="control">
  112. <button type="button" class="button is-text is-rounded" @click="cancelCreation">{{ $t('commons.cancel') }}</button>
  113. </p>
  114. </vue-footer>
  115. </form>
  116. <!-- modal -->
  117. <modal v-model="ShowTwofaccountInModal">
  118. <otp-displayer ref="AdvancedFormOtpDisplayer" v-bind="form.data()" @increment-hotp="incrementHotp">
  119. </otp-displayer>
  120. </modal>
  121. </form-wrapper>
  122. </template>
  123. <script>
  124. import Modal from '../../components/Modal'
  125. import Form from './../../components/Form'
  126. import OtpDisplayer from '../../components/OtpDisplayer'
  127. import Base32 from "hi-base32"
  128. export default {
  129. data() {
  130. return {
  131. ShowTwofaccountInModal : false,
  132. counterIsLocked: true,
  133. twofaccountExists: false,
  134. tempIcon: '',
  135. secretIsBase32Encoded: null,
  136. form: new Form({
  137. service: '',
  138. account: '',
  139. otp_type: '',
  140. uri: '',
  141. icon: '',
  142. secret: '',
  143. algorithm: '',
  144. digits: null,
  145. counter: null,
  146. period: null,
  147. image: '',
  148. }),
  149. otp_types: [
  150. { text: 'TOTP', value: 'totp' },
  151. { text: 'HOTP', value: 'hotp' },
  152. { text: 'STEAM', value: 'steamtotp' },
  153. ],
  154. digitsChoices: [
  155. { text: 6, value: 6 },
  156. { text: 7, value: 7 },
  157. { text: 8, value: 8 },
  158. { text: 9, value: 9 },
  159. { text: 10, value: 10 },
  160. ],
  161. secretFormats: [
  162. { text: this.$t('twofaccounts.forms.plain_text'), value: 0 },
  163. { text: 'Base32', value: 1 }
  164. ],
  165. algorithms: [
  166. { text: 'sha1', value: 'sha1' },
  167. { text: 'sha256', value: 'sha256' },
  168. { text: 'sha512', value: 'sha512' },
  169. { text: 'md5', value: 'md5' },
  170. ],
  171. secretIsLocked: true,
  172. }
  173. },
  174. mounted: function () {
  175. // stop TOTP generation on modal close
  176. this.$on('modalClose', function() {
  177. this.$refs.AdvancedFormOtpDisplayer.stopLoop()
  178. });
  179. },
  180. created: function() {
  181. this.getAccount();
  182. },
  183. components: {
  184. Modal,
  185. OtpDisplayer,
  186. },
  187. methods: {
  188. async getAccount () {
  189. const { data } = await this.axios.get('/api/v1/twofaccounts/' + this.$route.params.twofaccountId)
  190. this.form.fill(data)
  191. this.secretIsBase32Encoded = 1
  192. this.twofaccountExists = true
  193. // set account icon as temp icon
  194. this.tempIcon = this.form.icon
  195. },
  196. async updateAccount() {
  197. // Set new icon and delete old one
  198. if( this.tempIcon !== this.form.icon ) {
  199. let oldIcon = ''
  200. oldIcon = this.form.icon
  201. this.form.icon = this.tempIcon
  202. this.tempIcon = oldIcon
  203. this.deleteIcon()
  204. }
  205. // Secret to base32 if necessary
  206. this.form.secret = this.secretIsBase32Encoded ? this.form.secret : Base32.encode(this.form.secret).toString();
  207. await this.form.put('/api/v1/twofaccounts/' + this.$route.params.twofaccountId)
  208. if( this.form.errors.any() === false ) {
  209. this.$router.push({name: 'accounts', params: { InitialEditMode: true, toRefresh: true }})
  210. }
  211. },
  212. previewAccount() {
  213. this.$refs.AdvancedFormOtpDisplayer.show()
  214. },
  215. cancelCreation: function() {
  216. // clean new temp icon
  217. this.deleteIcon()
  218. this.$router.push({name: 'accounts', params: { InitialEditMode: true }});
  219. },
  220. async uploadIcon(event) {
  221. // clean possible tempIcon but keep original one
  222. this.deleteIcon()
  223. let imgdata = new FormData();
  224. imgdata.append('icon', this.$refs.iconInput.files[0]);
  225. const { data } = await this.form.upload('/api/v1/icons', imgdata)
  226. this.tempIcon = data.filename;
  227. },
  228. deleteIcon(event) {
  229. if( this.tempIcon && this.tempIcon !== this.form.icon ) {
  230. this.axios.delete('/api/v1/icons/' + this.tempIcon)
  231. }
  232. this.tempIcon = ''
  233. },
  234. incrementHotp(payload) {
  235. // The quick form or the preview feature has incremented the HOTP counter so we get the new value from
  236. // the component.
  237. // This could desynchronized the HOTP verification server and our local counter if the user never verified the HOTP but this
  238. // is acceptable (and HOTP counter can be edited by the way)
  239. this.form.counter = payload.nextHotpCounter
  240. this.form.uri = payload.nextUri
  241. },
  242. },
  243. }
  244. </script>