Recipients.vue 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670
  1. <template>
  2. <div>
  3. <div class="mb-6 flex flex-col md:flex-row justify-between md:items-center">
  4. <div class="relative">
  5. <input
  6. v-model="search"
  7. @keyup.esc="search = ''"
  8. tabindex="0"
  9. type="text"
  10. class="w-full md:w-64 appearance-none shadow bg-white text-grey-700 focus:outline-none rounded py-3 pl-3 pr-8"
  11. placeholder="Search Recipients"
  12. />
  13. <icon
  14. v-if="search"
  15. @click.native="search = ''"
  16. name="close-circle"
  17. class="absolute right-0 inset-y-0 w-5 h-full text-grey-300 fill-current mr-2 flex items-center cursor-pointer"
  18. />
  19. <icon
  20. v-else
  21. name="search"
  22. class="absolute right-0 inset-y-0 w-5 h-full text-grey-300 fill-current pointer-events-none mr-2 flex items-center"
  23. />
  24. </div>
  25. <div class="mt-4 md:mt-0">
  26. <button
  27. @click="addRecipientModalOpen = true"
  28. class="bg-cyan-400 hover:bg-cyan-300 text-cyan-900 font-bold py-3 px-4 rounded focus:outline-none ml-auto"
  29. >
  30. Add Recipient
  31. </button>
  32. </div>
  33. </div>
  34. <vue-good-table
  35. @on-search="debounceToolips"
  36. :columns="columns"
  37. :rows="rows"
  38. :search-options="{
  39. enabled: true,
  40. skipDiacritics: true,
  41. externalQuery: search,
  42. }"
  43. :sort-options="{
  44. enabled: true,
  45. initialSortBy: { field: 'created_at', type: 'desc' },
  46. }"
  47. styleClass="vgt-table"
  48. >
  49. <div slot="emptystate" class="flex items-center justify-center h-24 text-lg text-grey-700">
  50. No recipients found for that search!
  51. </div>
  52. <template slot="table-column" slot-scope="props">
  53. <span v-if="props.column.label == 'Key'">
  54. Key
  55. <span
  56. class="tooltip outline-none"
  57. :data-tippy-content="
  58. `Use this to attach recipients to new aliases as they are created e.g. alias+key@${
  59. user.username
  60. }.anonaddy.com. You can attach multiple recipients by doing alias+2.3.4@${
  61. user.username
  62. }.anonaddy.com. Separating each key by a full stop.`
  63. "
  64. >
  65. <icon name="info" class="inline-block w-4 h-4 text-grey-200 fill-current" />
  66. </span>
  67. </span>
  68. <span v-else>
  69. {{ props.column.label }}
  70. </span>
  71. </template>
  72. <template slot="table-row" slot-scope="props">
  73. <span
  74. v-if="props.column.field == 'created_at'"
  75. class="tooltip outline-none text-sm"
  76. :data-tippy-content="props.row.created_at | formatDate"
  77. >{{ props.row.created_at | timeAgo }}
  78. </span>
  79. <span v-else-if="props.column.field == 'key'">
  80. {{ props.row.key }}
  81. </span>
  82. <span v-else-if="props.column.field == 'email'">
  83. <span
  84. class="tooltip cursor-pointer outline-none"
  85. data-tippy-content="Click to copy"
  86. v-clipboard="() => props.row.email"
  87. v-clipboard:success="clipboardSuccess"
  88. v-clipboard:error="clipboardError"
  89. >{{ props.row.email | truncate(30) }}</span
  90. >
  91. <span
  92. v-if="isDefault(props.row.id)"
  93. class="ml-3 py-1 px-2 text-sm bg-yellow-200 text-yellow-900 rounded-full tooltip"
  94. data-tippy-content="The default recipient will be used for all aliases with no other recipients assigned"
  95. >
  96. default
  97. </span>
  98. </span>
  99. <span v-else-if="props.column.field === 'aliases'">
  100. <span
  101. v-if="props.row.aliases.length"
  102. class="tooltip outline-none"
  103. :data-tippy-content="aliasesTooltip(props.row.aliases)"
  104. >{{ props.row.aliases[0].email | truncate(40) }}
  105. <span v-if="props.row.aliases.length > 1" class="block text-grey-500 text-sm">
  106. + {{ props.row.aliases.length - 1 }}</span
  107. >
  108. </span>
  109. <span v-else class="block text-grey-500 text-sm">{{ props.row.aliases.length }}</span>
  110. </span>
  111. <span v-else-if="props.column.field === 'should_encrypt'">
  112. <span v-if="props.row.fingerprint" class="flex">
  113. <Toggle
  114. v-model="rows[props.row.originalIndex].should_encrypt"
  115. @on="turnOnEncryption(props.row)"
  116. @off="turnOffEncryption(props.row)"
  117. />
  118. <icon
  119. name="fingerprint"
  120. class="tooltip outline-none cursor-pointer block w-6 h-6 text-grey-200 fill-current mx-2"
  121. :data-tippy-content="props.row.fingerprint"
  122. v-clipboard="() => props.row.fingerprint"
  123. v-clipboard:success="clipboardSuccess"
  124. v-clipboard:error="clipboardError"
  125. />
  126. <icon
  127. name="delete"
  128. class="tooltip outline-none cursor-pointer block w-6 h-6 text-grey-200 fill-current"
  129. @click.native="openDeleteRecipientKeyModal(props.row.id)"
  130. data-tippy-content="Remove public key"
  131. />
  132. </span>
  133. <button
  134. v-else
  135. @click="openRecipientKeyModal(props.row)"
  136. class="focus:outline-none text-sm"
  137. >
  138. Add public key
  139. </button>
  140. </span>
  141. <span v-else-if="props.column.field === 'email_verified_at'">
  142. <span
  143. name="check"
  144. v-if="props.row.email_verified_at"
  145. class="py-1 px-2 bg-green-200 text-green-900 rounded-full text-sm"
  146. >
  147. verified
  148. </span>
  149. <button
  150. v-else
  151. @click="resendVerification(props.row.id)"
  152. class="focus:outline-none text-sm"
  153. :class="resendVerificationLoading ? 'cursor-not-allowed' : ''"
  154. :disabled="resendVerificationLoading"
  155. >
  156. Resend email
  157. </button>
  158. </span>
  159. <span v-else class="flex items-center justify-center outline-none" tabindex="-1">
  160. <icon
  161. name="trash"
  162. class="block w-6 h-6 text-grey-200 fill-current cursor-pointer"
  163. @click.native="openDeleteModal(props.row.id)"
  164. />
  165. </span>
  166. </template>
  167. </vue-good-table>
  168. <Modal :open="addRecipientModalOpen" @close="addRecipientModalOpen = false">
  169. <div class="max-w-lg w-full bg-white rounded-lg shadow-2xl p-6">
  170. <h2
  171. class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
  172. >
  173. Add new recipient
  174. </h2>
  175. <p class="mt-4 text-grey-700">
  176. Enter the individual email of the new recipient you'd like to add.
  177. </p>
  178. <p class="mt-4 text-grey-700">
  179. You will receive an email with a verification link that will expire in one hour, you can
  180. click "Resend email" to get a new one.
  181. </p>
  182. <div class="mt-6">
  183. <p v-show="errors.newRecipient" class="mb-3 text-red-500">
  184. {{ errors.newRecipient }}
  185. </p>
  186. <input
  187. v-model="newRecipient"
  188. type="email"
  189. class="w-full appearance-none bg-grey-100 border border-transparent text-grey-700 focus:outline-none rounded p-3 mb-6"
  190. :class="errors.newRecipient ? 'border-red-500' : ''"
  191. placeholder="johndoe@example.com"
  192. autofocus
  193. />
  194. <button
  195. @click="validateNewRecipient"
  196. class="bg-cyan-400 hover:bg-cyan-300 text-cyan-900 font-bold py-3 px-4 rounded focus:outline-none"
  197. :class="addRecipientLoading ? 'cursor-not-allowed' : ''"
  198. :disabled="addRecipientLoading"
  199. >
  200. Add Recipient
  201. <loader v-if="addRecipientLoading" />
  202. </button>
  203. <button
  204. @click="addRecipientModalOpen = false"
  205. class="ml-4 px-4 py-3 text-grey-800 font-semibold bg-white hover:bg-grey-50 border border-grey-100 rounded focus:outline-none"
  206. >
  207. Cancel
  208. </button>
  209. </div>
  210. </div>
  211. </Modal>
  212. <Modal :open="addRecipientKeyModalOpen" @close="closeRecipientKeyModal">
  213. <div class="max-w-lg w-full bg-white rounded-lg shadow-2xl p-6">
  214. <h2
  215. class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
  216. >
  217. Add Public GPG Key
  218. </h2>
  219. <p class="mt-4 text-grey-700">Enter your <b>PUBLIC</b> key data in the text area below.</p>
  220. <p class="mt-4 text-grey-700">Make sure to remove <b>Comment:</b> and <b>Version:</b></p>
  221. <div class="mt-6">
  222. <p v-show="errors.recipientKey" class="mb-3 text-red-500">
  223. {{ errors.recipientKey }}
  224. </p>
  225. <textarea
  226. v-model="recipientKey"
  227. type="email"
  228. class="w-full appearance-none bg-grey-100 border border-transparent text-grey-700 focus:outline-none rounded p-3 mb-6"
  229. :class="errors.recipientKey ? 'border-red-500' : ''"
  230. placeholder="Begins with '-----BEGIN PGP PUBLIC KEY BLOCK-----'"
  231. rows="10"
  232. autofocus
  233. >
  234. </textarea>
  235. <button
  236. type="button"
  237. @click="validateRecipientKey"
  238. class="bg-cyan-400 hover:bg-cyan-300 text-cyan-900 font-bold py-3 px-4 rounded focus:outline-none"
  239. :class="addRecipientKeyLoading ? 'cursor-not-allowed' : ''"
  240. :disabled="addRecipientKeyLoading"
  241. >
  242. Add Key
  243. <loader v-if="addRecipientKeyLoading" />
  244. </button>
  245. <button
  246. @click="closeRecipientKeyModal"
  247. class="ml-4 px-4 py-3 text-grey-800 font-semibold bg-white hover:bg-grey-50 border border-grey-100 rounded focus:outline-none"
  248. >
  249. Cancel
  250. </button>
  251. </div>
  252. </div>
  253. </Modal>
  254. <Modal :open="deleteRecipientKeyModalOpen" @close="closeDeleteRecipientKeyModal">
  255. <div class="max-w-lg w-full bg-white rounded-lg shadow-2xl p-6">
  256. <h2
  257. class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
  258. >
  259. Remove recipient public key
  260. </h2>
  261. <p class="mt-4 text-grey-700">
  262. Are you sure you want to remove the public key for this recipient?
  263. </p>
  264. <div class="mt-6">
  265. <button
  266. type="button"
  267. @click="deleteRecipientKey(recipientKeyIdToDelete)"
  268. class="px-4 py-3 text-white font-semibold bg-red-500 hover:bg-red-600 border border-transparent rounded focus:outline-none"
  269. :class="deleteRecipientKeyLoading ? 'cursor-not-allowed' : ''"
  270. :disabled="deleteRecipientKeyLoading"
  271. >
  272. Remove public key
  273. <loader v-if="deleteRecipientLoading" />
  274. </button>
  275. <button
  276. @click="closeDeleteRecipientKeyModal"
  277. class="ml-4 px-4 py-3 text-grey-800 font-semibold bg-white hover:bg-grey-50 border border-grey-100 rounded focus:outline-none"
  278. >
  279. Cancel
  280. </button>
  281. </div>
  282. </div>
  283. </Modal>
  284. <Modal :open="deleteRecipientModalOpen" @close="closeDeleteModal">
  285. <div class="max-w-lg w-full bg-white rounded-lg shadow-2xl p-6">
  286. <h2
  287. class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
  288. >
  289. Delete recipient
  290. </h2>
  291. <p class="mt-4 text-grey-700">Are you sure you want to delete this recipient?</p>
  292. <div class="mt-6">
  293. <button
  294. type="button"
  295. @click="deleteRecipient(recipientIdToDelete)"
  296. class="px-4 py-3 text-white font-semibold bg-red-500 hover:bg-red-600 border border-transparent rounded focus:outline-none"
  297. :class="deleteRecipientLoading ? 'cursor-not-allowed' : ''"
  298. :disabled="deleteRecipientLoading"
  299. >
  300. Delete recipient
  301. <loader v-if="deleteRecipientLoading" />
  302. </button>
  303. <button
  304. @click="closeDeleteModal"
  305. class="ml-4 px-4 py-3 text-grey-800 font-semibold bg-white hover:bg-grey-50 border border-grey-100 rounded focus:outline-none"
  306. >
  307. Cancel
  308. </button>
  309. </div>
  310. </div>
  311. </Modal>
  312. </div>
  313. </template>
  314. <script>
  315. import Modal from './../components/Modal.vue'
  316. import Toggle from './../components/Toggle.vue'
  317. import tippy from 'tippy.js'
  318. export default {
  319. props: {
  320. user: {
  321. type: Object,
  322. required: true,
  323. },
  324. initialRecipients: {
  325. type: Array,
  326. required: true,
  327. },
  328. aliasesUsingDefault: {
  329. type: Array,
  330. required: true,
  331. },
  332. domain: {
  333. type: String,
  334. required: true,
  335. },
  336. },
  337. components: {
  338. Modal,
  339. Toggle,
  340. },
  341. created() {
  342. this.defaultRecipient = _.find(this.rows, ['id', this.user.default_recipient_id])
  343. this.defaultRecipient.aliases = this.defaultRecipient.aliases.concat(this.aliasesUsingDefault)
  344. },
  345. mounted() {
  346. this.addTooltips()
  347. },
  348. data() {
  349. return {
  350. defaultRecipient: {},
  351. newRecipient: '',
  352. recipientKey: '',
  353. search: '',
  354. addRecipientLoading: false,
  355. addRecipientModalOpen: false,
  356. recipientIdToDelete: null,
  357. recipientKeyIdToDelete: null,
  358. deleteRecipientLoading: false,
  359. deleteRecipientModalOpen: false,
  360. deleteRecipientKeyLoading: false,
  361. deleteRecipientKeyModalOpen: false,
  362. addRecipientKeyLoading: false,
  363. addRecipientKeyModalOpen: false,
  364. recipientToAddKey: {},
  365. resendVerificationLoading: false,
  366. errors: {},
  367. columns: [
  368. {
  369. label: 'Created',
  370. field: 'created_at',
  371. globalSearchDisabled: true,
  372. },
  373. {
  374. label: 'Key',
  375. field: 'key',
  376. type: 'number',
  377. },
  378. {
  379. label: 'Email',
  380. field: 'email',
  381. },
  382. {
  383. label: 'Recipient Aliases',
  384. field: 'aliases',
  385. sortable: true,
  386. sortFn: this.sortRecipientAliases,
  387. globalSearchDisabled: true,
  388. },
  389. {
  390. label: 'Encryption',
  391. field: 'should_encrypt',
  392. type: 'boolean',
  393. globalSearchDisabled: true,
  394. },
  395. {
  396. label: 'Verified',
  397. field: 'email_verified_at',
  398. globalSearchDisabled: true,
  399. },
  400. {
  401. label: '',
  402. field: 'actions',
  403. sortable: false,
  404. globalSearchDisabled: true,
  405. },
  406. ],
  407. rows: this.initialRecipients,
  408. }
  409. },
  410. watch: {
  411. addRecipientKeyModalOpen: _.debounce(function() {
  412. this.addTooltips()
  413. }, 50),
  414. },
  415. methods: {
  416. addTooltips() {
  417. tippy('.tooltip', {
  418. arrow: true,
  419. arrowType: 'round',
  420. })
  421. },
  422. debounceToolips: _.debounce(function() {
  423. this.addTooltips()
  424. }, 50),
  425. aliasesTooltip(aliases) {
  426. return _.reduce(aliases, (list, alias) => list + `${alias.email}<br>`, '')
  427. },
  428. isDefault(id) {
  429. return this.user.default_recipient_id === id
  430. },
  431. validateNewRecipient(e) {
  432. this.errors = {}
  433. if (!this.newRecipient) {
  434. this.errors.newRecipient = 'Email required'
  435. } else if (!this.validEmail(this.newRecipient)) {
  436. this.errors.newRecipient = 'Valid Email required'
  437. }
  438. if (!this.errors.newRecipient) {
  439. this.addNewRecipient()
  440. }
  441. e.preventDefault()
  442. },
  443. addNewRecipient() {
  444. this.addRecipientLoading = true
  445. axios
  446. .post(
  447. '/recipients',
  448. JSON.stringify({
  449. email: this.newRecipient,
  450. }),
  451. {
  452. headers: { 'Content-Type': 'application/json' },
  453. }
  454. )
  455. .then(({ data }) => {
  456. this.addRecipientLoading = false
  457. data.data.key = this.rows.length + 1
  458. this.rows.push(data.data)
  459. this.newRecipient = ''
  460. this.addRecipientModalOpen = false
  461. this.success('Recipient created and verification email sent')
  462. })
  463. .catch(error => {
  464. this.addRecipientLoading = false
  465. if (error.response.status === 422) {
  466. this.error(error.response.data.errors.email[0])
  467. } else {
  468. this.error()
  469. }
  470. })
  471. },
  472. resendVerification(id) {
  473. this.resendVerificationLoading = true
  474. axios
  475. .get(`/recipients/${id}/email/resend`)
  476. .then(({ data }) => {
  477. this.resendVerificationLoading = false
  478. this.success('Verification email resent')
  479. })
  480. .catch(error => {
  481. this.resendVerificationLoading = false
  482. if (error.response.status === 429) {
  483. this.error('You can only resend the email once every 5 minutes')
  484. } else {
  485. this.error()
  486. }
  487. })
  488. },
  489. openDeleteModal(id) {
  490. this.deleteRecipientModalOpen = true
  491. this.recipientIdToDelete = id
  492. },
  493. closeDeleteModal() {
  494. this.deleteRecipientModalOpen = false
  495. this.recipientIdToDelete = null
  496. },
  497. deleteRecipient(id) {
  498. this.deleteRecipientLoading = true
  499. axios
  500. .delete(`/recipients/${id}`)
  501. .then(response => {
  502. this.recipients = _.reject(this.rows, recipient => recipient.id === id)
  503. this.deleteRecipientModalOpen = false
  504. this.deleteRecipientLoading = false
  505. })
  506. .catch(error => {
  507. this.error()
  508. this.deleteRecipientLoading = false
  509. this.deleteRecipientModalOpen = false
  510. })
  511. },
  512. openDeleteRecipientKeyModal(id) {
  513. this.deleteRecipientKeyModalOpen = true
  514. this.recipientKeyIdToDelete = id
  515. },
  516. closeDeleteRecipientKeyModal() {
  517. this.deleteRecipientKeyModalOpen = false
  518. this.recipientKeyIdToDelete = null
  519. },
  520. deleteRecipientKey(id) {
  521. this.deleteRecipientKeyLoading = true
  522. axios
  523. .delete(`/recipient-keys/${id}`)
  524. .then(response => {
  525. let recipient = _.find(this.rows, ['id', this.recipientKeyIdToDelete])
  526. recipient.should_encrypt = false
  527. recipient.fingerprint = null
  528. this.deleteRecipientKeyModalOpen = false
  529. this.deleteRecipientKeyLoading = false
  530. })
  531. .catch(error => {
  532. this.error()
  533. this.deleteRecipientKeyLoading = false
  534. this.deleteRecipientKeyModalOpen = false
  535. })
  536. },
  537. validateRecipientKey(e) {
  538. this.errors = {}
  539. if (!this.recipientKey) {
  540. this.errors.recipientKey = 'Key required'
  541. } else if (!this.validKey(this.recipientKey)) {
  542. this.errors.recipientKey = 'Valid Key required'
  543. }
  544. if (!this.errors.recipientKey) {
  545. this.addRecipientKey()
  546. }
  547. e.preventDefault()
  548. },
  549. addRecipientKey() {
  550. this.addRecipientKeyLoading = true
  551. axios
  552. .patch(
  553. `/recipient-keys/${this.recipientToAddKey.id}`,
  554. JSON.stringify({
  555. key_data: this.recipientKey,
  556. }),
  557. {
  558. headers: { 'Content-Type': 'application/json' },
  559. }
  560. )
  561. .then(({ data }) => {
  562. this.addRecipientKeyLoading = false
  563. let recipient = _.find(this.rows, ['id', this.recipientToAddKey.id])
  564. recipient.should_encrypt = data.data.should_encrypt
  565. recipient.fingerprint = data.data.fingerprint
  566. this.recipientKey = ''
  567. this.addRecipientKeyModalOpen = false
  568. this.success(
  569. `Key Successfully Added for ${
  570. this.recipientToAddKey.email
  571. }. Make sure to check the fingerprint is correct!`
  572. )
  573. })
  574. .catch(error => {
  575. this.addRecipientKeyLoading = false
  576. if (error.response !== undefined) {
  577. this.error(error.response.data)
  578. } else {
  579. this.error()
  580. }
  581. })
  582. },
  583. turnOnEncryption(recipient) {
  584. axios
  585. .post(
  586. `/encrypted-recipients`,
  587. JSON.stringify({
  588. id: recipient.id,
  589. }),
  590. {
  591. headers: { 'Content-Type': 'application/json' },
  592. }
  593. )
  594. .then(response => {
  595. //
  596. })
  597. .catch(error => {
  598. this.error()
  599. })
  600. },
  601. turnOffEncryption(recipient) {
  602. axios
  603. .delete(`/encrypted-recipients/${recipient.id}`)
  604. .then(response => {
  605. //
  606. })
  607. .catch(error => {
  608. this.error()
  609. })
  610. },
  611. openRecipientKeyModal(recipient) {
  612. this.addRecipientKeyModalOpen = true
  613. this.recipientToAddKey = recipient
  614. },
  615. closeRecipientKeyModal() {
  616. this.addRecipientKeyModalOpen = false
  617. this.recipientToAddKey = {}
  618. },
  619. validEmail(email) {
  620. let re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
  621. return re.test(email)
  622. },
  623. validKey(key) {
  624. let re = /-----BEGIN PGP PUBLIC KEY BLOCK-----([A-Za-z0-9+=\/\n]+)-----END PGP PUBLIC KEY BLOCK-----/i
  625. return re.test(key)
  626. },
  627. clipboardSuccess() {
  628. this.success('Copied to clipboard')
  629. },
  630. clipboardError() {
  631. this.error('Could not copy to clipboard')
  632. },
  633. success(text = '') {
  634. this.$notify({
  635. title: 'Success',
  636. text: text,
  637. type: 'success',
  638. })
  639. },
  640. error(text = 'An error has occurred, please try again later') {
  641. this.$notify({
  642. title: 'Error',
  643. text: text,
  644. type: 'error',
  645. })
  646. },
  647. },
  648. }
  649. </script>