anonaddy/resources/js/pages/Recipients.vue
2020-01-02 10:10:03 +00:00

693 lines
22 KiB
Vue

<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-200 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)"
>{{ props.row.aliases[0].email | truncate(40) }}
<span v-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 === '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-200 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-200 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-sm"
>
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-200 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 tippy from 'tippy.js'
export default {
props: {
user: {
type: Object,
required: true,
},
initialRecipients: {
type: Array,
required: true,
},
aliasesUsingDefault: {
type: Array,
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)
},
mounted() {
this.addTooltips()
},
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: '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,
}
},
watch: {
addRecipientKeyModalOpen: _.debounce(function() {
this.addTooltips()
}, 50),
},
methods: {
addTooltips() {
tippy('.tooltip', {
arrow: true,
arrowType: 'round',
})
},
debounceToolips: _.debounce(function() {
this.addTooltips()
}, 50),
aliasesTooltip(aliases) {
return _.reduce(aliases, (list, alias) => list + `${alias.email}<br>`, '')
},
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 {
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 => {
let recipients = _.filter(this.rows, ['fingerprint', recipient.fingerprint])
_.forEach(recipients, function(recipient) {
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 => {
let recipients = _.filter(this.rows, ['fingerprint', recipient.fingerprint])
_.forEach(recipients, function(recipient) {
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()
})
},
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>