CreateUpdate.vue 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576
  1. <script setup>
  2. import Form from '@/components/formElements/Form'
  3. import OtpDisplay from '@/components/OtpDisplay.vue'
  4. import QrContentDisplay from '@/components/QrContentDisplay.vue'
  5. import FormLockField from '@/components/formElements/FormLockField.vue'
  6. import twofaccountService from '@/services/twofaccountService'
  7. import { useUserStore } from '@/stores/user'
  8. import { useTwofaccounts } from '@/stores/twofaccounts'
  9. import { useGroups } from '@/stores/groups'
  10. import { useBusStore } from '@/stores/bus'
  11. import { useNotifyStore } from '@/stores/notify'
  12. import { UseColorMode } from '@vueuse/components'
  13. const $2fauth = inject('2fauth')
  14. const router = useRouter()
  15. const route = useRoute()
  16. const user = useUserStore()
  17. const twofaccounts = useTwofaccounts()
  18. const bus = useBusStore()
  19. const notify = useNotifyStore()
  20. const form = reactive(new Form({
  21. service: '',
  22. account: '',
  23. otp_type: '',
  24. icon: '',
  25. group_id: user.preferences.defaultGroup == -1 ? user.preferences.activeGroup : user.preferences.defaultGroup,
  26. secret: '',
  27. algorithm: '',
  28. digits: null,
  29. counter: null,
  30. period: null,
  31. image: '',
  32. }))
  33. const qrcodeForm = reactive(new Form({
  34. qrcode: null
  35. }))
  36. const iconForm = reactive(new Form({
  37. icon: null
  38. }))
  39. const otpDisplayProps = ref({
  40. otp_type: '',
  41. account : '',
  42. service : '',
  43. icon : '',
  44. })
  45. const otp_types = [
  46. { text: 'TOTP', value: 'totp' },
  47. { text: 'HOTP', value: 'hotp' },
  48. { text: 'STEAM', value: 'steamtotp' },
  49. ]
  50. const digitsChoices = [
  51. { text: '6', value: 6 },
  52. { text: '7', value: 7 },
  53. { text: '8', value: 8 },
  54. { text: '9', value: 9 },
  55. { text: '10', value: 10 },
  56. ]
  57. const algorithms = [
  58. { text: 'sha1', value: 'sha1' },
  59. { text: 'sha256', value: 'sha256' },
  60. { text: 'sha512', value: 'sha512' },
  61. { text: 'md5', value: 'md5' },
  62. ]
  63. const uri = ref()
  64. const tempIcon = ref('')
  65. const showQuickForm = ref(false)
  66. const showAlternatives = ref(false)
  67. const showOtpInModal = ref(false)
  68. const showAdvancedForm = ref(false)
  69. const ShowTwofaccountInModal = ref(false)
  70. const fetchingLogo = ref(false)
  71. // $refs
  72. const iconInput = ref(null)
  73. const OtpDisplayForAutoSave = ref(null)
  74. const OtpDisplayForQuickForm = ref(null)
  75. const OtpDisplayForAdvancedForm = ref(null)
  76. const qrcodeInputLabel = ref(null)
  77. const qrcodeInput = ref(null)
  78. const iconInputLabel = ref(null)
  79. const props = defineProps({
  80. twofaccountId: [Number, String]
  81. })
  82. const isEditMode = computed(() => {
  83. return props.twofaccountId != undefined
  84. })
  85. const groups = computed(() => {
  86. return useGroups().items.map((item) => {
  87. return { text: item.id > 0 ? item.name : '- ' + trans('groups.no_group') + ' -', value: item.id }
  88. })
  89. })
  90. onMounted(() => {
  91. if (route.name == 'editAccount') {
  92. twofaccountService.get(props.twofaccountId).then(response => {
  93. form.fill(response.data)
  94. if (form.group_id == null) {
  95. form.group_id = 0
  96. }
  97. form.setOriginal()
  98. // set account icon as temp icon
  99. tempIcon.value = form.icon
  100. showAdvancedForm.value = true
  101. })
  102. }
  103. else if( bus.decodedUri ) {
  104. // The Start|Capture view provided an uri via the bus store.
  105. uri.value = bus.decodedUri
  106. bus.decodedUri = null
  107. if (user.preferences.AutoSaveQrcodedAccount) {
  108. // The user wants the account to be saved automatically.
  109. // We save it now and we show him a fresh otp
  110. twofaccountService.storeFromUri(uri.value).then(response => {
  111. showOTP(response.data)
  112. })
  113. .catch(error => {
  114. if( error.response.data.errors.uri ) {
  115. showAlternatives.value = true
  116. showAdvancedForm.value = true
  117. }
  118. })
  119. }
  120. else {
  121. // We prefill and show the quick form
  122. twofaccountService.preview(uri.value).then(response => {
  123. form.fill(response.data)
  124. tempIcon.value = response.data.icon ? response.data.icon : ''
  125. showQuickForm.value = true
  126. nextTick().then(() => {
  127. OtpDisplayForQuickForm.value.show()
  128. })
  129. })
  130. .catch(error => {
  131. if( error.response.data.errors.uri ) {
  132. showAlternatives.value = true
  133. showAdvancedForm.value = true
  134. }
  135. })
  136. }
  137. } else {
  138. showAdvancedForm.value = true
  139. }
  140. })
  141. watch(tempIcon, (val) => {
  142. if( showQuickForm.value ) {
  143. nextTick().then(() => {
  144. OtpDisplayForQuickForm.value.icon = val
  145. })
  146. }
  147. })
  148. watch(ShowTwofaccountInModal, (val) => {
  149. if (val == false) {
  150. OtpDisplayForAdvancedForm.value?.clearOTP()
  151. OtpDisplayForQuickForm.value?.clearOTP()
  152. }
  153. })
  154. watch(showOtpInModal, (val) => {
  155. if (val == false) {
  156. OtpDisplayForAutoSave.value?.clearOTP()
  157. router.push({ name: 'accounts' })
  158. }
  159. })
  160. watch(
  161. () => form.otp_type,
  162. (to, from) => {
  163. if (to === 'steamtotp') {
  164. form.service = 'Steam'
  165. fetchLogo()
  166. }
  167. else if (from === 'steamtotp') {
  168. form.service = ''
  169. deleteTempIcon()
  170. }
  171. }
  172. )
  173. /**
  174. * Wrapper to call the appropriate function at form submit
  175. */
  176. function handleSubmit() {
  177. isEditMode.value ? updateAccount() : createAccount()
  178. }
  179. /**
  180. * Submits the form to the backend to store the new account
  181. */
  182. async function createAccount() {
  183. // set current temp icon as account icon
  184. form.icon = tempIcon.value
  185. const { data } = await form.post('/api/v1/twofaccounts')
  186. if (form.errors.any() === false) {
  187. twofaccounts.items.push(data)
  188. notify.success({ text: trans('twofaccounts.account_created') })
  189. router.push({ name: 'accounts' });
  190. }
  191. }
  192. /**
  193. * Submits the form to the backend to save the edited account
  194. */
  195. async function updateAccount() {
  196. // Set new icon and delete old one
  197. if( tempIcon.value !== form.icon ) {
  198. let oldIcon = ''
  199. oldIcon = form.icon
  200. form.icon = tempIcon.value
  201. tempIcon.value = oldIcon
  202. deleteTempIcon()
  203. }
  204. const { data } = await form.put('/api/v1/twofaccounts/' + props.twofaccountId)
  205. if( form.errors.any() === false ) {
  206. const index = twofaccounts.items.findIndex(acc => acc.id === data.id)
  207. twofaccounts.items.splice(index, 1, data)
  208. notify.success({ text: trans('twofaccounts.account_updated') })
  209. router.push({ name: 'accounts' })
  210. }
  211. }
  212. /**
  213. * Shows an OTP generated with the infos filled in the form
  214. * in order to preview or validated the password/the form data
  215. */
  216. function previewOTP() {
  217. form.clear()
  218. ShowTwofaccountInModal.value = true
  219. OtpDisplayForAdvancedForm.value.show()
  220. }
  221. /**
  222. * Shows rotating OTP for the provided account
  223. */
  224. function showOTP(otp) {
  225. // Data that should be displayed quickly by the OtpDisplay
  226. // component are passed using props.
  227. otpDisplayProps.value.otp_type = otp.otp_type
  228. otpDisplayProps.value.service = otp.service
  229. otpDisplayProps.value.account = otp.account
  230. otpDisplayProps.value.icon = otp.icon
  231. nextTick().then(() => {
  232. showOtpInModal.value = true
  233. OtpDisplayForAutoSave.value.show(otp.id);
  234. })
  235. }
  236. /**
  237. * Exits the view with user confirmation
  238. */
  239. function cancelCreation() {
  240. if (form.hasChanged() || tempIcon.value != form.icon) {
  241. if (confirm(trans('twofaccounts.confirm.cancel')) === true) {
  242. if (!isEditMode.value || tempIcon.value != form.icon) {
  243. deleteTempIcon()
  244. }
  245. router.push({name: 'accounts'})
  246. }
  247. }
  248. else router.push({name: 'accounts'})
  249. }
  250. /**
  251. * Uploads the submited image resource to the backend
  252. */
  253. function uploadIcon() {
  254. // clean possible already uploaded temp icon
  255. deleteTempIcon()
  256. iconForm.icon = iconInput.value.files[0]
  257. iconForm.upload('/api/v1/icons', { returnError: true })
  258. .then(response => {
  259. tempIcon.value = response.data.filename
  260. if (showQuickForm.value) {
  261. form.icon = tempIcon.value
  262. }
  263. })
  264. .catch(error => {
  265. if (error.response.status !== 422) {
  266. notify.alert({ text: error.response.data.message})
  267. }
  268. })
  269. }
  270. /**
  271. * Deletes the temp icon from backend
  272. */
  273. function deleteTempIcon() {
  274. if (isEditMode.value) {
  275. if (tempIcon.value) {
  276. if (tempIcon.value !== form.icon) {
  277. twofaccountService.deleteIcon(tempIcon.value)
  278. }
  279. tempIcon.value = ''
  280. }
  281. }
  282. else if (tempIcon.value) {
  283. twofaccountService.deleteIcon(tempIcon.value)
  284. tempIcon.value = ''
  285. if (showQuickForm.value) {
  286. form.icon = ''
  287. }
  288. }
  289. }
  290. /**
  291. * Increments the HOTP counter of the form after a preview
  292. *
  293. * @param {object} payload
  294. */
  295. function incrementHotp(payload) {
  296. // The quick form or the preview feature has incremented the HOTP counter so we get the new value from
  297. // the OtpDisplay component.
  298. // This could desynchronized the HOTP verification server and our local counter if the user never verified the HOTP but this
  299. // is acceptable (and HOTP counter can be edited by the way)
  300. form.counter = payload.nextHotpCounter
  301. //form.uri = payload.nextUri
  302. }
  303. /**
  304. * Maps errors received by the OtpDisplay to the form errors instance
  305. *
  306. * @param {object} errorResponse
  307. */
  308. function mapDisplayerErrors(errorResponse) {
  309. form.errors.set(form.extractErrors(errorResponse))
  310. }
  311. /**
  312. * Sends a QR code to backend for decoding and prefill the form with the qr data
  313. */
  314. function uploadQrcode() {
  315. qrcodeForm.qrcode = qrcodeInput.value.files[0]
  316. // First we get the uri encoded in the qrcode
  317. qrcodeForm.upload('/api/v1/qrcode/decode', { returnError: true })
  318. .then(response => {
  319. uri.value = response.data.data
  320. // Then the otp described by the uri
  321. twofaccountService.preview(uri.value, { returnError: true }).then(response => {
  322. form.fill(response.data)
  323. tempIcon.value = response.data.icon ? response.data.icon : null
  324. })
  325. .catch(error => {
  326. if( error.response.status === 422 ) {
  327. if( error.response.data.errors.uri ) {
  328. showAlternatives.value = true
  329. }
  330. else notify.alert({ text: trans(error.response.data.message) })
  331. } else {
  332. notify.error(error)
  333. }
  334. })
  335. })
  336. .catch(error => {
  337. if (error.response.status !== 422) {
  338. notify.alert({ text: error.response.data.message})
  339. }
  340. })
  341. }
  342. /**
  343. * Tries to get the official logo/icon of the Service filled in the form
  344. */
  345. function fetchLogo() {
  346. if (user.preferences.getOfficialIcons) {
  347. fetchingLogo.value = true
  348. twofaccountService.getLogo(form.service, { returnError: true })
  349. .then(response => {
  350. if (response.status === 201) {
  351. // clean possible already uploaded temp icon
  352. deleteTempIcon()
  353. tempIcon.value = response.data.filename;
  354. }
  355. else notify.warn( {text: trans('errors.no_logo_found_for_x', {service: strip_tags(form.service)}) })
  356. })
  357. .catch(() => {
  358. notify.warn({ text: trans('errors.no_logo_found_for_x', {service: strip_tags(form.service)}) })
  359. })
  360. .finally(() => {
  361. fetchingLogo.value = false
  362. })
  363. }
  364. }
  365. /**
  366. * Strips html tags to prevent code injection
  367. *
  368. * @param {*} str
  369. */
  370. function strip_tags(str) {
  371. return str.replace(/(<([^> ]+)>)/ig, "")
  372. }
  373. </script>
  374. <template>
  375. <div>
  376. <!-- otp display modal -->
  377. <Modal v-if="user.preferences.AutoSaveQrcodedAccount" v-model="showOtpInModal">
  378. <OtpDisplay
  379. ref="OtpDisplayForAutoSave"
  380. v-bind="otpDisplayProps"
  381. @please-close-me="router.push({ name: 'accounts' })">
  382. </OtpDisplay>
  383. </Modal>
  384. <!-- Quick form -->
  385. <form @submit.prevent="createAccount" @keydown="form.onKeydown($event)" v-if="!isEditMode && showQuickForm">
  386. <div class="container preview has-text-centered">
  387. <div class="columns is-mobile">
  388. <div class="column">
  389. <FieldError v-if="iconForm.errors.hasAny('icon')" :error="iconForm.errors.get('icon')" :field="'icon'" class="help-for-file" />
  390. <label class="add-icon-button" v-if="!tempIcon">
  391. <input inert class="file-input" type="file" accept="image/*" v-on:change="uploadIcon" ref="iconInput">
  392. <FontAwesomeIcon :icon="['fas', 'image']" size="2x" />
  393. </label>
  394. <button class="delete delete-icon-button is-medium" v-if="tempIcon" @click.prevent="deleteTempIcon"></button>
  395. <OtpDisplay
  396. ref="OtpDisplayForQuickForm"
  397. v-bind="form.data()"
  398. @increment-hotp="incrementHotp"
  399. @validation-error="mapDisplayerErrors"
  400. @please-close-me="ShowTwofaccountInModal = false">
  401. </OtpDisplay>
  402. </div>
  403. </div>
  404. <div class="columns is-mobile" role="alert">
  405. <div v-if="form.errors.any()" class="column">
  406. <p v-for="(field, index) in form.errors.errors" :key="index" class="help is-danger">
  407. <ul>
  408. <li v-for="(error, index) in field" :key="index">{{ error }}</li>
  409. </ul>
  410. </p>
  411. </div>
  412. </div>
  413. <div class="columns is-mobile">
  414. <div class="column quickform-footer">
  415. <div class="field is-grouped is-grouped-centered">
  416. <div class="control">
  417. <VueButton :isLoading="form.isBusy" >{{ $t('commons.save') }}</VueButton>
  418. </div>
  419. <ButtonBackCloseCancel action="cancel" :isText="true" :isRounded="false" :useLinkTag="false" @canceled="cancelCreation" />
  420. </div>
  421. </div>
  422. </div>
  423. </div>
  424. </form>
  425. <!-- Full form -->
  426. <FormWrapper :title="$t(isEditMode ? 'twofaccounts.forms.edit_account' : 'twofaccounts.forms.new_account')" v-if="showAdvancedForm">
  427. <form @submit.prevent="handleSubmit" @keydown="form.onKeydown($event)">
  428. <!-- qcode fileupload -->
  429. <div v-if="!isEditMode" class="field is-grouped">
  430. <div class="control">
  431. <UseColorMode v-slot="{ mode }">
  432. <div role="button" tabindex="0" class="file is-small" :class="{ 'is-black': mode == 'dark' }" @keyup.enter="qrcodeInputLabel.click()">
  433. <label class="file-label" :title="$t('twofaccounts.forms.use_qrcode.title')" ref="qrcodeInputLabel">
  434. <input inert tabindex="-1" class="file-input" type="file" accept="image/*" v-on:change="uploadQrcode" ref="qrcodeInput">
  435. <span class="file-cta">
  436. <span class="file-icon">
  437. <FontAwesomeIcon :icon="['fas', 'qrcode']" size="lg" />
  438. </span>
  439. <span class="file-label">{{ $t('twofaccounts.forms.prefill_using_qrcode') }}</span>
  440. </span>
  441. </label>
  442. </div>
  443. </UseColorMode>
  444. </div>
  445. </div>
  446. <FieldError v-if="qrcodeForm.errors.hasAny('qrcode')" :error="qrcodeForm.errors.get('qrcode')" :field="'qrcode'" class="help-for-file" />
  447. <!-- service -->
  448. <FormField v-model="form.service" fieldName="service" :fieldError="form.errors.get('email')" :isDisabled="form.otp_type === 'steamtotp'" label="twofaccounts.service" :placeholder="$t('twofaccounts.forms.service.placeholder')" autofocus />
  449. <!-- account -->
  450. <FormField v-model="form.account" fieldName="account" :fieldError="form.errors.get('account')" label="twofaccounts.account" :placeholder="$t('twofaccounts.forms.account.placeholder')" />
  451. <!-- icon upload -->
  452. <label for="filUploadIcon" class="label">{{ $t('twofaccounts.icon') }}</label>
  453. <div class="field is-grouped">
  454. <!-- Try my luck button -->
  455. <div class="control" v-if="user.preferences.getOfficialIcons">
  456. <UseColorMode v-slot="{ mode }">
  457. <VueButton @click="fetchLogo" :color="mode == 'dark' ? 'is-dark' : ''" :nativeType="'button'" :is-loading="fetchingLogo" :isDisabled="!form.service" aria-describedby="lgdTryMyLuck">
  458. <span class="icon is-small">
  459. <FontAwesomeIcon :icon="['fas', 'globe']" />
  460. </span>
  461. <span>{{ $t('twofaccounts.forms.i_m_lucky') }}</span>
  462. </VueButton>
  463. </UseColorMode>
  464. </div>
  465. <!-- upload icon button -->
  466. <div class="control is-flex">
  467. <UseColorMode v-slot="{ mode }">
  468. <div role="button" tabindex="0" class="file mr-3" :class="mode == 'dark' ? 'is-dark' : 'is-white'" @keyup.enter="iconInputLabel.click()">
  469. <label for="filUploadIcon" class="file-label" ref="iconInputLabel">
  470. <input id="filUploadIcon" tabindex="-1" class="file-input" type="file" accept="image/*" v-on:change="uploadIcon" ref="iconInput">
  471. <span class="file-cta">
  472. <span class="file-icon">
  473. <FontAwesomeIcon :icon="['fas', 'upload']" />
  474. </span>
  475. <span class="file-label">{{ $t('twofaccounts.forms.choose_image') }}</span>
  476. </span>
  477. </label>
  478. </div>
  479. <span class="tag is-large" :class="mode =='dark' ? 'is-dark' : 'is-white'" v-if="tempIcon">
  480. <img class="icon-preview" :src="$2fauth.config.subdirectory + '/storage/icons/' + tempIcon" :alt="$t('twofaccounts.icon_to_illustrate_the_account')">
  481. <button class="clear-selection delete is-small" @click.prevent="deleteTempIcon" :aria-label="$t('twofaccounts.remove_icon')"></button>
  482. </span>
  483. </UseColorMode>
  484. </div>
  485. </div>
  486. <div class="field">
  487. <FieldError v-if="iconForm.errors.hasAny('icon')" :error="iconForm.errors.get('icon')" :field="'icon'" class="help-for-file" />
  488. <p id="lgdTryMyLuck" v-if="user.preferences.getOfficialIcons" class="help" v-html="$t('twofaccounts.forms.i_m_lucky_legend')"></p>
  489. </div>
  490. <!-- group -->
  491. <FormSelect v-if="groups.length > 0" v-model="form.group_id" :options="groups" fieldName="group_id" label="twofaccounts.forms.group.label" help="twofaccounts.forms.group.help" />
  492. <!-- otp type -->
  493. <FormToggle v-model="form.otp_type" :isDisabled="isEditMode" :choices="otp_types" fieldName="otp_type" :fieldError="form.errors.get('otp_type')" label="twofaccounts.forms.otp_type.label" help="twofaccounts.forms.otp_type.help" :hasOffset="true" />
  494. <div v-if="form.otp_type != ''">
  495. <!-- secret -->
  496. <FormLockField :isEditMode="isEditMode" v-model.trimAll="form.secret" fieldName="secret" :fieldError="form.errors.get('secret')" label="twofaccounts.forms.secret.label" help="twofaccounts.forms.secret.help" />
  497. <!-- Options -->
  498. <div v-if="form.otp_type !== 'steamtotp'">
  499. <h2 class="title is-4 mt-5 mb-2">{{ $t('commons.options') }}</h2>
  500. <p class="help mb-4">
  501. {{ $t('twofaccounts.forms.options_help') }}
  502. </p>
  503. <!-- digits -->
  504. <FormToggle v-model="form.digits" :choices="digitsChoices" fieldName="digits" :fieldError="form.errors.get('digits')" label="twofaccounts.forms.digits.label" help="twofaccounts.forms.digits.help" />
  505. <!-- algorithm -->
  506. <FormToggle v-model="form.algorithm" :choices="algorithms" fieldName="algorithm" :fieldError="form.errors.get('algorithm')" label="twofaccounts.forms.algorithm.label" help="twofaccounts.forms.algorithm.help" />
  507. <!-- TOTP period -->
  508. <FormField v-if="form.otp_type === 'totp'" pattern="[0-9]{1,4}" :class="'is-third-width-field'" v-model="form.period" fieldName="period" :fieldError="form.errors.get('period')" label="twofaccounts.forms.period.label" help="twofaccounts.forms.period.help" :placeholder="$t('twofaccounts.forms.period.placeholder')" />
  509. <!-- HOTP counter -->
  510. <FormLockField v-if="form.otp_type === 'hotp'" pattern="[0-9]{1,4}" :isEditMode="isEditMode" :isExpanded="false" v-model="form.counter" fieldName="counter" :fieldError="form.errors.get('counter')" label="twofaccounts.forms.counter.label" :placeholder="$t('twofaccounts.forms.counter.placeholder')" :help="isEditMode ? 'twofaccounts.forms.counter.help_lock' : 'twofaccounts.forms.counter.help'" />
  511. </div>
  512. </div>
  513. <VueFooter :showButtons="true">
  514. <p class="control">
  515. <VueButton :id="isEditMode ? 'btnUpdate' : 'btnCreate'" :isLoading="form.isBusy" class="is-rounded" >{{ isEditMode ? $t('commons.save') : $t('commons.create') }}</VueButton>
  516. </p>
  517. <p class="control" v-if="form.otp_type && form.secret">
  518. <button id="btnPreview" type="button" class="button is-success is-rounded" @click="previewOTP">{{ $t('twofaccounts.forms.test') }}</button>
  519. </p>
  520. <ButtonBackCloseCancel action="cancel" :useLinkTag="false" @canceled="cancelCreation" />
  521. </VueFooter>
  522. </form>
  523. <!-- modal -->
  524. <modal v-model="ShowTwofaccountInModal">
  525. <OtpDisplay
  526. ref="OtpDisplayForAdvancedForm"
  527. v-bind="form.data()"
  528. @increment-hotp="incrementHotp"
  529. @validation-error="mapDisplayerErrors"
  530. @please-close-me="ShowTwofaccountInModal = false">
  531. </OtpDisplay>
  532. </modal>
  533. </FormWrapper>
  534. <!-- alternatives -->
  535. <modal v-model="showAlternatives">
  536. <QrContentDisplay :qrContent="uri" />
  537. </modal>
  538. </div>
  539. </template>