123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746 |
- <template>
- <div>
- <div class="mb-6 flex flex-col md:flex-row justify-between md:items-center">
- <div class="relative">
- <input
- v-model="search"
- @keyup.esc="search = ''"
- tabindex="0"
- type="text"
- class="w-full md:w-64 appearance-none shadow bg-white text-grey-700 focus:outline-none rounded py-3 pl-3 pr-8"
- placeholder="Search Recipients"
- />
- <icon
- v-if="search"
- @click.native="search = ''"
- name="close-circle"
- class="absolute right-0 inset-y-0 w-5 h-full text-grey-300 fill-current mr-2 flex items-center cursor-pointer"
- />
- <icon
- v-else
- name="search"
- class="absolute right-0 inset-y-0 w-5 h-full text-grey-300 fill-current pointer-events-none mr-2 flex items-center"
- />
- </div>
- <div class="mt-4 md:mt-0">
- <button
- @click="addRecipientModalOpen = true"
- class="bg-cyan-400 hover:bg-cyan-300 text-cyan-900 font-bold py-3 px-4 rounded focus:outline-none ml-auto"
- >
- Add Recipient
- </button>
- </div>
- </div>
- <vue-good-table
- @on-search="debounceToolips"
- :columns="columns"
- :rows="rows"
- :search-options="{
- enabled: true,
- skipDiacritics: true,
- externalQuery: search,
- }"
- :sort-options="{
- enabled: true,
- initialSortBy: { field: 'created_at', type: 'desc' },
- }"
- styleClass="vgt-table"
- >
- <div slot="emptystate" class="flex items-center justify-center h-24 text-lg text-grey-700">
- No recipients found for that search!
- </div>
- <template slot="table-column" slot-scope="props">
- <span v-if="props.column.label == 'Key'">
- Key
- <span
- class="tooltip outline-none"
- :data-tippy-content="`Use this to attach recipients to new aliases as they are created e.g. alias+key@${user.username}.anonaddy.com. You can attach multiple recipients by doing alias+2.3.4@${user.username}.anonaddy.com. Separating each key by a full stop.`"
- >
- <icon name="info" class="inline-block w-4 h-4 text-grey-300 fill-current" />
- </span>
- </span>
- <span v-else>
- {{ props.column.label }}
- </span>
- </template>
- <template slot="table-row" slot-scope="props">
- <span
- v-if="props.column.field == 'created_at'"
- class="tooltip outline-none text-sm"
- :data-tippy-content="rows[props.row.originalIndex].created_at | formatDate"
- >{{ props.row.created_at | timeAgo }}
- </span>
- <span v-else-if="props.column.field == 'key'">
- {{ props.row.key }}
- </span>
- <span v-else-if="props.column.field == 'email'">
- <span
- class="tooltip cursor-pointer outline-none"
- data-tippy-content="Click to copy"
- v-clipboard="() => rows[props.row.originalIndex].email"
- v-clipboard:success="clipboardSuccess"
- v-clipboard:error="clipboardError"
- >{{ props.row.email | truncate(30) }}</span
- >
- <span
- v-if="isDefault(props.row.id)"
- class="ml-3 py-1 px-2 text-sm bg-yellow-200 text-yellow-900 rounded-full tooltip"
- data-tippy-content="The default recipient will be used for all aliases with no other recipients assigned"
- >
- default
- </span>
- </span>
- <span v-else-if="props.column.field === 'aliases'">
- <span
- v-if="props.row.aliases.length"
- class="tooltip outline-none"
- :data-tippy-content="aliasesTooltip(props.row.aliases, isDefault(props.row.id))"
- >{{ props.row.aliases[0].email | truncate(40) }}
- <span
- v-if="isDefault(props.row.id) && aliasesUsingDefaultCount > 1"
- class="block text-grey-500 text-sm"
- >
- + {{ aliasesUsingDefaultCount - 1 }}</span
- >
- <span v-else-if="props.row.aliases.length > 1" class="block text-grey-500 text-sm">
- + {{ props.row.aliases.length - 1 }}</span
- >
- </span>
- <span v-else class="block text-grey-500 text-sm">{{ props.row.aliases.length }}</span>
- </span>
- <span
- v-else-if="props.column.field === 'can_reply_send'"
- class="flex justify-center items-center"
- >
- <Toggle
- v-model="rows[props.row.originalIndex].can_reply_send"
- @on="allowRepliesSends(props.row.id)"
- @off="disallowRepliesSends(props.row.id)"
- />
- </span>
- <span v-else-if="props.column.field === 'should_encrypt'">
- <span v-if="props.row.fingerprint" class="flex">
- <Toggle
- v-model="rows[props.row.originalIndex].should_encrypt"
- @on="turnOnEncryption(props.row.id)"
- @off="turnOffEncryption(props.row.id)"
- />
- <icon
- name="fingerprint"
- class="tooltip outline-none cursor-pointer block w-6 h-6 text-grey-300 fill-current mx-2"
- :data-tippy-content="props.row.fingerprint"
- v-clipboard="() => props.row.fingerprint"
- v-clipboard:success="clipboardSuccess"
- v-clipboard:error="clipboardError"
- />
- <icon
- name="delete"
- class="tooltip outline-none cursor-pointer block w-6 h-6 text-grey-300 fill-current"
- @click.native="openDeleteRecipientKeyModal(props.row)"
- data-tippy-content="Remove public key"
- />
- </span>
- <button
- v-else
- @click="openRecipientKeyModal(props.row)"
- class="focus:outline-none text-sm"
- >
- Add public key
- </button>
- </span>
- <span v-else-if="props.column.field === 'email_verified_at'">
- <span
- name="check"
- v-if="props.row.email_verified_at"
- class="py-1 px-2 bg-green-200 text-green-900 rounded-full text-xs"
- >
- verified
- </span>
- <button
- v-else
- @click="resendVerification(props.row.id)"
- class="focus:outline-none text-sm"
- :class="resendVerificationLoading ? 'cursor-not-allowed' : ''"
- :disabled="resendVerificationLoading"
- >
- Resend email
- </button>
- </span>
- <span v-else class="flex items-center justify-center outline-none" tabindex="-1">
- <icon
- v-if="!isDefault(props.row.id)"
- name="trash"
- class="block w-6 h-6 text-grey-300 fill-current cursor-pointer"
- @click.native="openDeleteModal(props.row)"
- />
- </span>
- </template>
- </vue-good-table>
- <Modal :open="addRecipientModalOpen" @close="addRecipientModalOpen = false">
- <div class="max-w-lg w-full bg-white rounded-lg shadow-2xl p-6">
- <h2
- class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
- >
- Add new recipient
- </h2>
- <p class="mt-4 text-grey-700">
- Enter the individual email of the new recipient you'd like to add.
- </p>
- <p class="mt-4 text-grey-700">
- You will receive an email with a verification link that will expire in one hour, you can
- click "Resend email" to get a new one.
- </p>
- <div class="mt-6">
- <p v-show="errors.newRecipient" class="mb-3 text-red-500 text-sm">
- {{ errors.newRecipient }}
- </p>
- <input
- v-model="newRecipient"
- type="email"
- class="w-full appearance-none bg-grey-100 border border-transparent text-grey-700 focus:outline-none rounded p-3 mb-6"
- :class="errors.newRecipient ? 'border-red-500' : ''"
- placeholder="johndoe@example.com"
- autofocus
- />
- <button
- @click="validateNewRecipient"
- class="bg-cyan-400 hover:bg-cyan-300 text-cyan-900 font-bold py-3 px-4 rounded focus:outline-none"
- :class="addRecipientLoading ? 'cursor-not-allowed' : ''"
- :disabled="addRecipientLoading"
- >
- Add Recipient
- <loader v-if="addRecipientLoading" />
- </button>
- <button
- @click="addRecipientModalOpen = false"
- 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"
- >
- Cancel
- </button>
- </div>
- </div>
- </Modal>
- <Modal :open="addRecipientKeyModalOpen" @close="closeRecipientKeyModal">
- <div class="max-w-lg w-full bg-white rounded-lg shadow-2xl p-6">
- <h2
- class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
- >
- Add Public GPG Key
- </h2>
- <p class="mt-4 text-grey-700">Enter your <b>PUBLIC</b> key data in the text area below.</p>
- <p class="mt-4 text-grey-700">Make sure to remove <b>Comment:</b> and <b>Version:</b></p>
- <div class="mt-6">
- <p v-show="errors.recipientKey" class="mb-3 text-red-500 text-sm">
- {{ errors.recipientKey }}
- </p>
- <textarea
- v-model="recipientKey"
- class="w-full appearance-none bg-grey-100 border border-transparent text-grey-700 focus:outline-none rounded p-3 mb-6"
- :class="errors.recipientKey ? 'border-red-500' : ''"
- placeholder="Begins with '-----BEGIN PGP PUBLIC KEY BLOCK-----'"
- rows="10"
- autofocus
- >
- </textarea>
- <button
- type="button"
- @click="validateRecipientKey"
- class="bg-cyan-400 hover:bg-cyan-300 text-cyan-900 font-bold py-3 px-4 rounded focus:outline-none"
- :class="addRecipientKeyLoading ? 'cursor-not-allowed' : ''"
- :disabled="addRecipientKeyLoading"
- >
- Add Key
- <loader v-if="addRecipientKeyLoading" />
- </button>
- <button
- @click="closeRecipientKeyModal"
- 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"
- >
- Cancel
- </button>
- </div>
- </div>
- </Modal>
- <Modal :open="deleteRecipientKeyModalOpen" @close="closeDeleteRecipientKeyModal">
- <div class="max-w-lg w-full bg-white rounded-lg shadow-2xl p-6">
- <h2
- class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
- >
- Remove recipient public key
- </h2>
- <p class="mt-4 text-grey-700">
- Are you sure you want to remove the public key for this recipient? It will also be removed
- from any other recipients using the same key.
- </p>
- <div class="mt-6">
- <button
- type="button"
- @click="deleteRecipientKey(recipientKeyToDelete)"
- class="px-4 py-3 text-white font-semibold bg-red-500 hover:bg-red-600 border border-transparent rounded focus:outline-none"
- :class="deleteRecipientKeyLoading ? 'cursor-not-allowed' : ''"
- :disabled="deleteRecipientKeyLoading"
- >
- Remove public key
- <loader v-if="deleteRecipientLoading" />
- </button>
- <button
- @click="closeDeleteRecipientKeyModal"
- 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"
- >
- Cancel
- </button>
- </div>
- </div>
- </Modal>
- <Modal :open="deleteRecipientModalOpen" @close="closeDeleteModal">
- <div class="max-w-lg w-full bg-white rounded-lg shadow-2xl p-6">
- <h2
- class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
- >
- Delete recipient
- </h2>
- <p class="mt-4 text-grey-700">Are you sure you want to delete this recipient?</p>
- <div class="mt-6">
- <button
- type="button"
- @click="deleteRecipient(recipientToDelete)"
- class="px-4 py-3 text-white font-semibold bg-red-500 hover:bg-red-600 border border-transparent rounded focus:outline-none"
- :class="deleteRecipientLoading ? 'cursor-not-allowed' : ''"
- :disabled="deleteRecipientLoading"
- >
- Delete recipient
- <loader v-if="deleteRecipientLoading" />
- </button>
- <button
- @click="closeDeleteModal"
- 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"
- >
- Cancel
- </button>
- </div>
- </div>
- </Modal>
- </div>
- </template>
- <script>
- import Modal from './../components/Modal.vue'
- import Toggle from './../components/Toggle.vue'
- import { roundArrow } from 'tippy.js'
- import 'tippy.js/dist/svg-arrow.css'
- import 'tippy.js/dist/tippy.css'
- import tippy from 'tippy.js'
- export default {
- props: {
- user: {
- type: Object,
- required: true,
- },
- initialRecipients: {
- type: Array,
- required: true,
- },
- aliasesUsingDefault: {
- type: Array,
- required: true,
- },
- aliasesUsingDefaultCount: {
- type: Number,
- required: true,
- },
- domain: {
- type: String,
- required: true,
- },
- },
- components: {
- Modal,
- Toggle,
- },
- created() {
- this.defaultRecipient = _.find(this.rows, ['id', this.user.default_recipient_id])
- this.defaultRecipient.aliases = this.defaultRecipient.aliases.concat(this.aliasesUsingDefault)
- },
- data() {
- return {
- defaultRecipient: {},
- newRecipient: '',
- recipientKey: '',
- search: '',
- addRecipientLoading: false,
- addRecipientModalOpen: false,
- recipientToDelete: null,
- recipientKeyToDelete: null,
- deleteRecipientLoading: false,
- deleteRecipientModalOpen: false,
- deleteRecipientKeyLoading: false,
- deleteRecipientKeyModalOpen: false,
- addRecipientKeyLoading: false,
- addRecipientKeyModalOpen: false,
- recipientToAddKey: {},
- resendVerificationLoading: false,
- errors: {},
- columns: [
- {
- label: 'Created',
- field: 'created_at',
- globalSearchDisabled: true,
- },
- {
- label: 'Key',
- field: 'key',
- type: 'number',
- },
- {
- label: 'Email',
- field: 'email',
- },
- {
- label: 'Recipient Aliases',
- field: 'aliases',
- sortable: true,
- sortFn: this.sortRecipientAliases,
- globalSearchDisabled: true,
- },
- {
- label: 'Can Reply/Send',
- field: 'can_reply_send',
- type: 'boolean',
- globalSearchDisabled: true,
- },
- {
- label: 'Encryption',
- field: 'should_encrypt',
- type: 'boolean',
- globalSearchDisabled: true,
- },
- {
- label: 'Verified',
- field: 'email_verified_at',
- globalSearchDisabled: true,
- },
- {
- label: '',
- field: 'actions',
- sortable: false,
- globalSearchDisabled: true,
- },
- ],
- rows: this.initialRecipients,
- tippyInstance: null,
- }
- },
- watch: {
- addRecipientKeyModalOpen: _.debounce(function () {
- this.addTooltips()
- }, 50),
- },
- methods: {
- addTooltips() {
- if (this.tippyInstance) {
- _.each(this.tippyInstance, instance => instance.destroy())
- }
- this.tippyInstance = tippy('.tooltip', {
- arrow: roundArrow,
- allowHTML: true,
- })
- },
- debounceToolips: _.debounce(function () {
- this.addTooltips()
- }, 50),
- aliasesTooltip(aliases, isDefault) {
- let ellipses =
- aliases.length > 5 || (isDefault && this.aliasesUsingDefaultCount > 5) ? '...' : ''
- return (
- _.reduce(_.take(aliases, 5), (list, alias) => list + `${alias.email}<br>`, '') + ellipses
- )
- },
- isDefault(id) {
- return this.user.default_recipient_id === id
- },
- validateNewRecipient(e) {
- this.errors = {}
- if (!this.newRecipient) {
- this.errors.newRecipient = 'Email required'
- } else if (!this.validEmail(this.newRecipient)) {
- this.errors.newRecipient = 'Valid Email required'
- }
- if (!this.errors.newRecipient) {
- this.addNewRecipient()
- }
- e.preventDefault()
- },
- addNewRecipient() {
- this.addRecipientLoading = true
- axios
- .post(
- '/api/v1/recipients',
- JSON.stringify({
- email: this.newRecipient,
- }),
- {
- headers: { 'Content-Type': 'application/json' },
- }
- )
- .then(({ data }) => {
- this.addRecipientLoading = false
- data.data.key = this.rows.length + 1
- this.rows.push(data.data)
- this.newRecipient = ''
- this.addRecipientModalOpen = false
- this.success('Recipient created and verification email sent')
- })
- .catch(error => {
- this.addRecipientLoading = false
- if (error.response.status === 422) {
- this.error(error.response.data.errors.email[0])
- } else if (error.response.status === 429) {
- this.error('You are making too many requests')
- } else {
- this.error()
- }
- })
- },
- resendVerification(id) {
- this.resendVerificationLoading = true
- axios
- .post(
- '/recipients/email/resend',
- JSON.stringify({
- recipient_id: id,
- }),
- {
- headers: { 'Content-Type': 'application/json' },
- }
- )
- .then(({ data }) => {
- this.resendVerificationLoading = false
- this.success('Verification email resent')
- })
- .catch(error => {
- this.resendVerificationLoading = false
- if (error.response.status === 429) {
- this.error('You can only resend the email once per minute')
- } else {
- this.error()
- }
- })
- },
- openDeleteModal(recipient) {
- this.deleteRecipientModalOpen = true
- this.recipientToDelete = recipient
- },
- closeDeleteModal() {
- this.deleteRecipientModalOpen = false
- this.recipientToDelete = null
- },
- deleteRecipient(recipient) {
- this.deleteRecipientLoading = true
- axios
- .delete(`/api/v1/recipients/${recipient.id}`)
- .then(response => {
- recipient.should_encrypt = false
- recipient.fingerprint = null
- this.rows = _.reject(this.rows, row => row.id === recipient.id)
- this.deleteRecipientModalOpen = false
- this.deleteRecipientLoading = false
- })
- .catch(error => {
- this.error()
- this.deleteRecipientLoading = false
- this.deleteRecipientModalOpen = false
- })
- },
- openDeleteRecipientKeyModal(recipient) {
- this.deleteRecipientKeyModalOpen = true
- this.recipientKeyToDelete = recipient
- },
- closeDeleteRecipientKeyModal() {
- this.deleteRecipientKeyModalOpen = false
- this.recipientKeyIdToDelete = null
- },
- deleteRecipientKey(recipient) {
- this.deleteRecipientKeyLoading = true
- axios
- .delete(`/api/v1/recipient-keys/${recipient.id}`)
- .then(response => {
- recipient.should_encrypt = false
- recipient.fingerprint = null
- this.deleteRecipientKeyModalOpen = false
- this.deleteRecipientKeyLoading = false
- })
- .catch(error => {
- if (error.response !== undefined) {
- this.error(error.response.data)
- } else {
- this.error()
- }
- this.deleteRecipientKeyLoading = false
- this.deleteRecipientKeyModalOpen = false
- })
- },
- validateRecipientKey(e) {
- this.errors = {}
- if (!this.recipientKey) {
- this.errors.recipientKey = 'Key required'
- } else if (!this.validKey(this.recipientKey)) {
- this.errors.recipientKey = 'Valid Key required'
- }
- if (!this.errors.recipientKey) {
- this.addRecipientKey()
- }
- e.preventDefault()
- },
- addRecipientKey() {
- this.addRecipientKeyLoading = true
- axios
- .patch(
- `/api/v1/recipient-keys/${this.recipientToAddKey.id}`,
- JSON.stringify({
- key_data: this.recipientKey,
- }),
- {
- headers: { 'Content-Type': 'application/json' },
- }
- )
- .then(({ data }) => {
- this.addRecipientKeyLoading = false
- let recipient = _.find(this.rows, ['id', this.recipientToAddKey.id])
- recipient.should_encrypt = data.data.should_encrypt
- recipient.fingerprint = data.data.fingerprint
- this.recipientKey = ''
- this.addRecipientKeyModalOpen = false
- this.success(
- `Key Successfully Added for ${this.recipientToAddKey.email}. Make sure to check the fingerprint is correct!`
- )
- })
- .catch(error => {
- this.addRecipientKeyLoading = false
- if (error.response !== undefined) {
- this.error(error.response.data)
- } else {
- this.error()
- }
- })
- },
- turnOnEncryption(id) {
- axios
- .post(
- `/api/v1/encrypted-recipients`,
- JSON.stringify({
- id: id,
- }),
- {
- headers: { 'Content-Type': 'application/json' },
- }
- )
- .then(response => {
- //
- })
- .catch(error => {
- this.error()
- })
- },
- turnOffEncryption(id) {
- axios
- .delete(`/api/v1/encrypted-recipients/${id}`)
- .then(response => {
- //
- })
- .catch(error => {
- this.error()
- })
- },
- allowRepliesSends(id) {
- axios
- .post(
- `/api/v1/allowed-recipients`,
- JSON.stringify({
- id: id,
- }),
- {
- headers: { 'Content-Type': 'application/json' },
- }
- )
- .then(response => {
- //
- })
- .catch(error => {
- this.error()
- })
- },
- disallowRepliesSends(id) {
- axios
- .delete(`/api/v1/allowed-recipients/${id}`)
- .then(response => {
- //
- })
- .catch(error => {
- this.error()
- })
- },
- openRecipientKeyModal(recipient) {
- this.addRecipientKeyModalOpen = true
- this.recipientToAddKey = recipient
- },
- closeRecipientKeyModal() {
- this.addRecipientKeyModalOpen = false
- this.recipientToAddKey = {}
- },
- validEmail(email) {
- 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,}))$/
- return re.test(email)
- },
- validKey(key) {
- let re =
- /-----BEGIN PGP PUBLIC KEY BLOCK-----([A-Za-z0-9+=\/\n]+)-----END PGP PUBLIC KEY BLOCK-----/i
- return re.test(key)
- },
- clipboardSuccess() {
- this.success('Copied to clipboard')
- },
- clipboardError() {
- this.error('Could not copy to clipboard')
- },
- success(text = '') {
- this.$notify({
- title: 'Success',
- text: text,
- type: 'success',
- })
- },
- error(text = 'An error has occurred, please try again later') {
- this.$notify({
- title: 'Error',
- text: text,
- type: 'error',
- })
- },
- },
- }
- </script>
|