Recipients.vue 22 KB

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