Recipients.vue 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677
  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.id)"
  116. @off="turnOffEncryption(props.row.id)"
  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 text-sm">
  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 text-sm">
  223. {{ errors.recipientKey }}
  224. </p>
  225. <textarea
  226. v-model="recipientKey"
  227. class="w-full appearance-none bg-grey-100 border border-transparent text-grey-700 focus:outline-none rounded p-3 mb-6"
  228. :class="errors.recipientKey ? 'border-red-500' : ''"
  229. placeholder="Begins with '-----BEGIN PGP PUBLIC KEY BLOCK-----'"
  230. rows="10"
  231. autofocus
  232. >
  233. </textarea>
  234. <button
  235. type="button"
  236. @click="validateRecipientKey"
  237. class="bg-cyan-400 hover:bg-cyan-300 text-cyan-900 font-bold py-3 px-4 rounded focus:outline-none"
  238. :class="addRecipientKeyLoading ? 'cursor-not-allowed' : ''"
  239. :disabled="addRecipientKeyLoading"
  240. >
  241. Add Key
  242. <loader v-if="addRecipientKeyLoading" />
  243. </button>
  244. <button
  245. @click="closeRecipientKeyModal"
  246. 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"
  247. >
  248. Cancel
  249. </button>
  250. </div>
  251. </div>
  252. </Modal>
  253. <Modal :open="deleteRecipientKeyModalOpen" @close="closeDeleteRecipientKeyModal">
  254. <div class="max-w-lg w-full bg-white rounded-lg shadow-2xl p-6">
  255. <h2
  256. class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
  257. >
  258. Remove recipient public key
  259. </h2>
  260. <p class="mt-4 text-grey-700">
  261. Are you sure you want to remove the public key for this recipient?
  262. </p>
  263. <div class="mt-6">
  264. <button
  265. type="button"
  266. @click="deleteRecipientKey(recipientKeyIdToDelete)"
  267. class="px-4 py-3 text-white font-semibold bg-red-500 hover:bg-red-600 border border-transparent rounded focus:outline-none"
  268. :class="deleteRecipientKeyLoading ? 'cursor-not-allowed' : ''"
  269. :disabled="deleteRecipientKeyLoading"
  270. >
  271. Remove public key
  272. <loader v-if="deleteRecipientLoading" />
  273. </button>
  274. <button
  275. @click="closeDeleteRecipientKeyModal"
  276. 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"
  277. >
  278. Cancel
  279. </button>
  280. </div>
  281. </div>
  282. </Modal>
  283. <Modal :open="deleteRecipientModalOpen" @close="closeDeleteModal">
  284. <div class="max-w-lg w-full bg-white rounded-lg shadow-2xl p-6">
  285. <h2
  286. class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
  287. >
  288. Delete recipient
  289. </h2>
  290. <p class="mt-4 text-grey-700">Are you sure you want to delete this recipient?</p>
  291. <div class="mt-6">
  292. <button
  293. type="button"
  294. @click="deleteRecipient(recipientIdToDelete)"
  295. class="px-4 py-3 text-white font-semibold bg-red-500 hover:bg-red-600 border border-transparent rounded focus:outline-none"
  296. :class="deleteRecipientLoading ? 'cursor-not-allowed' : ''"
  297. :disabled="deleteRecipientLoading"
  298. >
  299. Delete recipient
  300. <loader v-if="deleteRecipientLoading" />
  301. </button>
  302. <button
  303. @click="closeDeleteModal"
  304. 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"
  305. >
  306. Cancel
  307. </button>
  308. </div>
  309. </div>
  310. </Modal>
  311. </div>
  312. </template>
  313. <script>
  314. import Modal from './../components/Modal.vue'
  315. import Toggle from './../components/Toggle.vue'
  316. import tippy from 'tippy.js'
  317. export default {
  318. props: {
  319. user: {
  320. type: Object,
  321. required: true,
  322. },
  323. initialRecipients: {
  324. type: Array,
  325. required: true,
  326. },
  327. aliasesUsingDefault: {
  328. type: Array,
  329. required: true,
  330. },
  331. domain: {
  332. type: String,
  333. required: true,
  334. },
  335. },
  336. components: {
  337. Modal,
  338. Toggle,
  339. },
  340. created() {
  341. this.defaultRecipient = _.find(this.rows, ['id', this.user.default_recipient_id])
  342. this.defaultRecipient.aliases = this.defaultRecipient.aliases.concat(this.aliasesUsingDefault)
  343. },
  344. mounted() {
  345. this.addTooltips()
  346. },
  347. data() {
  348. return {
  349. defaultRecipient: {},
  350. newRecipient: '',
  351. recipientKey: '',
  352. search: '',
  353. addRecipientLoading: false,
  354. addRecipientModalOpen: false,
  355. recipientIdToDelete: null,
  356. recipientKeyIdToDelete: null,
  357. deleteRecipientLoading: false,
  358. deleteRecipientModalOpen: false,
  359. deleteRecipientKeyLoading: false,
  360. deleteRecipientKeyModalOpen: false,
  361. addRecipientKeyLoading: false,
  362. addRecipientKeyModalOpen: false,
  363. recipientToAddKey: {},
  364. resendVerificationLoading: false,
  365. errors: {},
  366. columns: [
  367. {
  368. label: 'Created',
  369. field: 'created_at',
  370. globalSearchDisabled: true,
  371. },
  372. {
  373. label: 'Key',
  374. field: 'key',
  375. type: 'number',
  376. },
  377. {
  378. label: 'Email',
  379. field: 'email',
  380. },
  381. {
  382. label: 'Recipient Aliases',
  383. field: 'aliases',
  384. sortable: true,
  385. sortFn: this.sortRecipientAliases,
  386. globalSearchDisabled: true,
  387. },
  388. {
  389. label: 'Encryption',
  390. field: 'should_encrypt',
  391. type: 'boolean',
  392. globalSearchDisabled: true,
  393. },
  394. {
  395. label: 'Verified',
  396. field: 'email_verified_at',
  397. globalSearchDisabled: true,
  398. },
  399. {
  400. label: '',
  401. field: 'actions',
  402. sortable: false,
  403. globalSearchDisabled: true,
  404. },
  405. ],
  406. rows: this.initialRecipients,
  407. }
  408. },
  409. watch: {
  410. addRecipientKeyModalOpen: _.debounce(function() {
  411. this.addTooltips()
  412. }, 50),
  413. },
  414. methods: {
  415. addTooltips() {
  416. tippy('.tooltip', {
  417. arrow: true,
  418. arrowType: 'round',
  419. })
  420. },
  421. debounceToolips: _.debounce(function() {
  422. this.addTooltips()
  423. }, 50),
  424. aliasesTooltip(aliases) {
  425. return _.reduce(aliases, (list, alias) => list + `${alias.email}<br>`, '')
  426. },
  427. isDefault(id) {
  428. return this.user.default_recipient_id === id
  429. },
  430. validateNewRecipient(e) {
  431. this.errors = {}
  432. if (!this.newRecipient) {
  433. this.errors.newRecipient = 'Email required'
  434. } else if (!this.validEmail(this.newRecipient)) {
  435. this.errors.newRecipient = 'Valid Email required'
  436. }
  437. if (!this.errors.newRecipient) {
  438. this.addNewRecipient()
  439. }
  440. e.preventDefault()
  441. },
  442. addNewRecipient() {
  443. this.addRecipientLoading = true
  444. axios
  445. .post(
  446. '/api/v1/recipients',
  447. JSON.stringify({
  448. email: this.newRecipient,
  449. }),
  450. {
  451. headers: { 'Content-Type': 'application/json' },
  452. }
  453. )
  454. .then(({ data }) => {
  455. this.addRecipientLoading = false
  456. data.data.key = this.rows.length + 1
  457. this.rows.push(data.data)
  458. this.newRecipient = ''
  459. this.addRecipientModalOpen = false
  460. this.success('Recipient created and verification email sent')
  461. })
  462. .catch(error => {
  463. this.addRecipientLoading = false
  464. if (error.response.status === 422) {
  465. this.error(error.response.data.errors.email[0])
  466. } else {
  467. this.error()
  468. }
  469. })
  470. },
  471. resendVerification(id) {
  472. this.resendVerificationLoading = true
  473. axios
  474. .post(
  475. '/recipients/email/resend',
  476. JSON.stringify({
  477. recipient_id: id,
  478. }),
  479. {
  480. headers: { 'Content-Type': 'application/json' },
  481. }
  482. )
  483. .then(({ data }) => {
  484. this.resendVerificationLoading = false
  485. this.success('Verification email resent')
  486. })
  487. .catch(error => {
  488. this.resendVerificationLoading = false
  489. if (error.response.status === 429) {
  490. this.error('You can only resend the email once per minute')
  491. } else {
  492. this.error()
  493. }
  494. })
  495. },
  496. openDeleteModal(id) {
  497. this.deleteRecipientModalOpen = true
  498. this.recipientIdToDelete = id
  499. },
  500. closeDeleteModal() {
  501. this.deleteRecipientModalOpen = false
  502. this.recipientIdToDelete = null
  503. },
  504. deleteRecipient(id) {
  505. this.deleteRecipientLoading = true
  506. axios
  507. .delete(`/api/v1/recipients/${id}`)
  508. .then(response => {
  509. this.rows = _.reject(this.rows, recipient => recipient.id === id)
  510. this.deleteRecipientModalOpen = false
  511. this.deleteRecipientLoading = false
  512. })
  513. .catch(error => {
  514. this.error()
  515. this.deleteRecipientLoading = false
  516. this.deleteRecipientModalOpen = false
  517. })
  518. },
  519. openDeleteRecipientKeyModal(id) {
  520. this.deleteRecipientKeyModalOpen = true
  521. this.recipientKeyIdToDelete = id
  522. },
  523. closeDeleteRecipientKeyModal() {
  524. this.deleteRecipientKeyModalOpen = false
  525. this.recipientKeyIdToDelete = null
  526. },
  527. deleteRecipientKey(id) {
  528. this.deleteRecipientKeyLoading = true
  529. axios
  530. .delete(`/api/v1/recipient-keys/${id}`)
  531. .then(response => {
  532. let recipient = _.find(this.rows, ['id', this.recipientKeyIdToDelete])
  533. recipient.should_encrypt = false
  534. recipient.fingerprint = null
  535. this.deleteRecipientKeyModalOpen = false
  536. this.deleteRecipientKeyLoading = false
  537. })
  538. .catch(error => {
  539. this.error()
  540. this.deleteRecipientKeyLoading = false
  541. this.deleteRecipientKeyModalOpen = false
  542. })
  543. },
  544. validateRecipientKey(e) {
  545. this.errors = {}
  546. if (!this.recipientKey) {
  547. this.errors.recipientKey = 'Key required'
  548. } else if (!this.validKey(this.recipientKey)) {
  549. this.errors.recipientKey = 'Valid Key required'
  550. }
  551. if (!this.errors.recipientKey) {
  552. this.addRecipientKey()
  553. }
  554. e.preventDefault()
  555. },
  556. addRecipientKey() {
  557. this.addRecipientKeyLoading = true
  558. axios
  559. .patch(
  560. `/api/v1/recipient-keys/${this.recipientToAddKey.id}`,
  561. JSON.stringify({
  562. key_data: this.recipientKey,
  563. }),
  564. {
  565. headers: { 'Content-Type': 'application/json' },
  566. }
  567. )
  568. .then(({ data }) => {
  569. this.addRecipientKeyLoading = false
  570. let recipient = _.find(this.rows, ['id', this.recipientToAddKey.id])
  571. recipient.should_encrypt = data.data.should_encrypt
  572. recipient.fingerprint = data.data.fingerprint
  573. this.recipientKey = ''
  574. this.addRecipientKeyModalOpen = false
  575. this.success(
  576. `Key Successfully Added for ${
  577. this.recipientToAddKey.email
  578. }. Make sure to check the fingerprint is correct!`
  579. )
  580. })
  581. .catch(error => {
  582. this.addRecipientKeyLoading = false
  583. if (error.response !== undefined) {
  584. this.error(error.response.data)
  585. } else {
  586. this.error()
  587. }
  588. })
  589. },
  590. turnOnEncryption(id) {
  591. axios
  592. .post(
  593. `/api/v1/encrypted-recipients`,
  594. JSON.stringify({
  595. id: id,
  596. }),
  597. {
  598. headers: { 'Content-Type': 'application/json' },
  599. }
  600. )
  601. .then(response => {
  602. //
  603. })
  604. .catch(error => {
  605. this.error()
  606. })
  607. },
  608. turnOffEncryption(id) {
  609. axios
  610. .delete(`/api/v1/encrypted-recipients/${id}`)
  611. .then(response => {
  612. //
  613. })
  614. .catch(error => {
  615. this.error()
  616. })
  617. },
  618. openRecipientKeyModal(recipient) {
  619. this.addRecipientKeyModalOpen = true
  620. this.recipientToAddKey = recipient
  621. },
  622. closeRecipientKeyModal() {
  623. this.addRecipientKeyModalOpen = false
  624. this.recipientToAddKey = {}
  625. },
  626. validEmail(email) {
  627. 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,}))$/
  628. return re.test(email)
  629. },
  630. validKey(key) {
  631. let re = /-----BEGIN PGP PUBLIC KEY BLOCK-----([A-Za-z0-9+=\/\n]+)-----END PGP PUBLIC KEY BLOCK-----/i
  632. return re.test(key)
  633. },
  634. clipboardSuccess() {
  635. this.success('Copied to clipboard')
  636. },
  637. clipboardError() {
  638. this.error('Could not copy to clipboard')
  639. },
  640. success(text = '') {
  641. this.$notify({
  642. title: 'Success',
  643. text: text,
  644. type: 'success',
  645. })
  646. },
  647. error(text = 'An error has occurred, please try again later') {
  648. this.$notify({
  649. title: 'Error',
  650. text: text,
  651. type: 'error',
  652. })
  653. },
  654. },
  655. }
  656. </script>