Merge pull request #8175 from nextcloud/enhancement/alias-mapping-ui
improve certificate to alias mapping UI
This commit is contained in:
Коммит
3886273f67
|
@ -32,6 +32,11 @@
|
|||
:title="t('mail', 'Account settings')">
|
||||
<AliasSettings :account="account" @rename-primary-alias="scrollToAccountSettings" />
|
||||
</AppSettingsSection>
|
||||
<AppSettingsSection
|
||||
id="certificate-settings"
|
||||
:title="t('mail', 'Alias to S/MIME certificate mapping')">
|
||||
<CertificateSettings :account="account" />
|
||||
</AppSettingsSection>
|
||||
<AppSettingsSection id="signature" :title="t('mail', 'Signature')">
|
||||
<p class="settings-hint">
|
||||
{{ t('mail', 'A signature is added to the text of new messages and replies.') }}
|
||||
|
@ -113,6 +118,7 @@ import TrustedSenders from './TrustedSenders'
|
|||
import SieveAccountForm from './SieveAccountForm'
|
||||
import SieveFilterForm from './SieveFilterForm'
|
||||
import OutOfOfficeForm from './OutOfOfficeForm'
|
||||
import CertificateSettings from './CertificateSettings'
|
||||
|
||||
export default {
|
||||
name: 'AccountSettings',
|
||||
|
@ -128,6 +134,7 @@ export default {
|
|||
AppSettingsSection,
|
||||
AccountDefaultsSettings,
|
||||
OutOfOfficeForm,
|
||||
CertificateSettings,
|
||||
},
|
||||
props: {
|
||||
account: {
|
||||
|
|
|
@ -36,25 +36,12 @@
|
|||
class="alias-form__form__input"
|
||||
required>
|
||||
</form>
|
||||
<form v-else-if="showSmimeForm"
|
||||
:id="formId"
|
||||
class="alias-form__form"
|
||||
@submit.prevent="updateSmimeCertificate">
|
||||
<NcSelect v-model="changeSmimeCert"
|
||||
:options="smimeCertOptions"
|
||||
:placeholder="t('mail', 'Select a S/MIME certificate for signing and encrypting')"
|
||||
class="alias-form__form__input">
|
||||
<template #option="option">
|
||||
{{ option.label }}
|
||||
</template>
|
||||
</NcSelect>
|
||||
</form>
|
||||
<div v-else>
|
||||
<strong>{{ alias.name }}</strong> <{{ alias.alias }}>
|
||||
</div>
|
||||
|
||||
<div class="alias-form__actions">
|
||||
<template v-if="showForm || showSmimeForm">
|
||||
<template v-if="showForm">
|
||||
<NcButton type="tertiary-no-background"
|
||||
native-type="submit"
|
||||
:form="formId"
|
||||
|
@ -77,14 +64,6 @@
|
|||
<IconRename :size="20" />
|
||||
</template>
|
||||
</NcButton>
|
||||
<NcButton v-if="smimeCertOptions.length > 0"
|
||||
type="tertiary-no-background"
|
||||
:title="t('mail', 'Select S/MIME certificate')"
|
||||
@click.prevent="showSmimeForm = true">
|
||||
<template #icon>
|
||||
<IconCertificate :size="20" />
|
||||
</template>
|
||||
</NcButton>
|
||||
<NcButton v-if="enableDelete && !alias.provisioned"
|
||||
type="tertiary-no-background"
|
||||
:title="t('mail', 'Delete alias')"
|
||||
|
@ -100,25 +79,19 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { NcButton, NcLoadingIcon as IconLoading, NcSelect } from '@nextcloud/vue'
|
||||
import { mapGetters } from 'vuex'
|
||||
import moment from '@nextcloud/moment'
|
||||
import { NcButton, NcLoadingIcon as IconLoading } from '@nextcloud/vue'
|
||||
import IconDelete from 'vue-material-design-icons/Delete'
|
||||
import IconRename from 'vue-material-design-icons/Pencil'
|
||||
import IconCheck from 'vue-material-design-icons/Check'
|
||||
import IconCertificate from 'vue-material-design-icons/Certificate'
|
||||
import { compareSmimeCertificates } from '../util/smime'
|
||||
|
||||
export default {
|
||||
name: 'AliasForm',
|
||||
components: {
|
||||
NcButton,
|
||||
NcSelect,
|
||||
IconRename,
|
||||
IconLoading,
|
||||
IconDelete,
|
||||
IconCheck,
|
||||
IconCertificate,
|
||||
},
|
||||
props: {
|
||||
account: {
|
||||
|
@ -137,10 +110,6 @@ export default {
|
|||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
onUpdateSmimeCertificate: {
|
||||
type: Function,
|
||||
default: async (aliasId, smimeCertificateId) => {},
|
||||
},
|
||||
onUpdateAlias: {
|
||||
type: Function,
|
||||
default: async (aliasId, { alias, name }) => {},
|
||||
|
@ -154,55 +123,14 @@ export default {
|
|||
return {
|
||||
changeAlias: this.alias.alias,
|
||||
changeName: this.alias.name,
|
||||
changeSmimeCert: undefined,
|
||||
showForm: false,
|
||||
showSmimeForm: false,
|
||||
loading: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
smimeCertificates: 'getSmimeCertificates',
|
||||
}),
|
||||
formId() {
|
||||
return `alias-form-${this.alias.id}`
|
||||
},
|
||||
smimeCertOptions() {
|
||||
// Only show certificates that are at least valid until tomorrow
|
||||
const now = (new Date().getTime() / 1000) + 3600 * 24
|
||||
|
||||
return this.smimeCertificates
|
||||
.filter((cert) => {
|
||||
return cert.hasKey
|
||||
&& cert.emailAddress === this.alias.alias
|
||||
&& cert.info.notAfter >= now
|
||||
&& cert.purposes.sign
|
||||
&& cert.purposes.encrypt
|
||||
// TODO: select a separate certificate for encryption?!
|
||||
})
|
||||
.map(this.mapCertificateToOption)
|
||||
.sort(compareSmimeCertificates)
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
alias: {
|
||||
immediate: true,
|
||||
handler(newAlias) {
|
||||
if (!newAlias.smimeCertificateId) {
|
||||
return
|
||||
}
|
||||
|
||||
const cert = this.smimeCertificates.find((cert) => {
|
||||
return cert.id === newAlias.smimeCertificateId
|
||||
})
|
||||
|
||||
if (!cert) {
|
||||
return
|
||||
}
|
||||
|
||||
this.changeSmimeCert = this.mapCertificateToOption(cert)
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
|
@ -219,19 +147,6 @@ export default {
|
|||
this.showForm = false
|
||||
this.loading = false
|
||||
},
|
||||
|
||||
/**
|
||||
* Call S/MIME certificate update event handler of parent.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async updateSmimeCertificate() {
|
||||
this.loading = true
|
||||
await this.onUpdateSmimeCertificate(this.alias.id, this.changeSmimeCert?.id)
|
||||
this.showSmimeForm = false
|
||||
this.loading = false
|
||||
},
|
||||
|
||||
/**
|
||||
* Call alias deletion event handler of parent.
|
||||
*
|
||||
|
@ -242,20 +157,6 @@ export default {
|
|||
await this.onDelete(this.alias.id)
|
||||
this.loading = false
|
||||
},
|
||||
|
||||
/**
|
||||
* Map an S/MIME certificate from the db to a NcSelect option.
|
||||
*
|
||||
* @param {object} cert S/MIME certificate
|
||||
* @return {object} NcSelect option
|
||||
*/
|
||||
mapCertificateToOption(cert) {
|
||||
const label = this.t('mail', '{commonName} - Valid until {expiryDate}', {
|
||||
commonName: cert.info.commonName ?? cert.info.emailAddress,
|
||||
expiryDate: moment.unix(cert.info.notAfter).format('LL'),
|
||||
})
|
||||
return { ...cert, label }
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -27,8 +27,7 @@
|
|||
<AliasForm :account="account"
|
||||
:alias="accountAlias"
|
||||
:enable-update="false"
|
||||
:enable-delete="false"
|
||||
:on-update-smime-certificate="updateAccountSmimeCertificate">
|
||||
:enable-delete="false">
|
||||
<ButtonVue v-if="!account.provisioningId"
|
||||
type="tertiary-no-background"
|
||||
:title="t('mail', 'Change name')"
|
||||
|
@ -45,7 +44,6 @@
|
|||
<AliasForm :account="account"
|
||||
:alias="alias"
|
||||
:on-update-alias="updateAlias"
|
||||
:on-update-smime-certificate="updateAliasSmimeCertificate"
|
||||
:on-delete="deleteAlias" />
|
||||
</li>
|
||||
|
||||
|
@ -156,22 +154,7 @@ export default {
|
|||
this.newName = this.account.name
|
||||
this.showForm = false
|
||||
},
|
||||
async updateAccountSmimeCertificate(aliasId, smimeCertificateId) {
|
||||
await this.$store.dispatch('updateAccountSmimeCertificate', {
|
||||
account: this.account,
|
||||
smimeCertificateId,
|
||||
})
|
||||
},
|
||||
async updateAliasSmimeCertificate(aliasId, smimeCertificateId) {
|
||||
const alias = this.aliases.find((alias) => alias.id === aliasId)
|
||||
await this.$store.dispatch('updateAlias', {
|
||||
account: this.account,
|
||||
aliasId,
|
||||
alias: alias.alias,
|
||||
name: alias.name,
|
||||
smimeCertificateId,
|
||||
})
|
||||
},
|
||||
|
||||
async updateAlias(aliasId, newAlias) {
|
||||
const alias = this.aliases.find((alias) => alias.id === aliasId)
|
||||
await this.$store.dispatch('updateAlias', {
|
||||
|
|
|
@ -0,0 +1,196 @@
|
|||
<!--
|
||||
- @copyright 2023 Richard Steinmetz <richard@steinmetz.cloud>
|
||||
-
|
||||
- @author 2023 Richard Steinmetz <richard@steinmetz.cloud>
|
||||
- @author 2023 Hamza Mahjoubi <hamzamahjoubi221@gmail.com>
|
||||
-
|
||||
- @license AGPL-3.0-or-later
|
||||
-
|
||||
- This program is free software: you can redistribute it and/or modify
|
||||
- it under the terms of the GNU Affero General Public License as
|
||||
- published by the Free Software Foundation, either version 3 of the
|
||||
- License, or (at your option) any later version.
|
||||
-
|
||||
- This program is distributed in the hope that it will be useful,
|
||||
- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
- GNU Affero General Public License for more details.
|
||||
-
|
||||
- You should have received a copy of the GNU Affero General Public License
|
||||
- along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Multiselect
|
||||
:allow-empty="false"
|
||||
:options="aliases"
|
||||
:searchable="false"
|
||||
:value="alias"
|
||||
:placeholder="t('mail', 'Select an alias')"
|
||||
label="name"
|
||||
track-by="id"
|
||||
@select="handleAlias" />
|
||||
<Multiselect
|
||||
v-if="alias !== null"
|
||||
v-model="savedCertificate"
|
||||
:options="smimeCertOptions"
|
||||
:searchable="false"
|
||||
label="label"
|
||||
track-by="id"
|
||||
@select="selectCertificate" />
|
||||
<Button type="primary" :disabled="certificate === null" @click="updateSmimeCertificate">
|
||||
Update Certificate
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { NcMultiselect as Multiselect, NcButton as Button } from '@nextcloud/vue'
|
||||
import { compareSmimeCertificates } from '../util/smime'
|
||||
import { mapGetters } from 'vuex'
|
||||
import { showError, showSuccess } from '@nextcloud/dialogs'
|
||||
import Logger from '../logger'
|
||||
import moment from '@nextcloud/moment'
|
||||
|
||||
export default {
|
||||
name: 'CertificateSettings',
|
||||
components: {
|
||||
Multiselect,
|
||||
Button,
|
||||
},
|
||||
props: {
|
||||
account: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
alias: null,
|
||||
certificate: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
smimeCertificates: 'getSmimeCertificates',
|
||||
}),
|
||||
savedCertificate: {
|
||||
get() {
|
||||
if (this.certificate) {
|
||||
return this.certificate
|
||||
}
|
||||
const saved = this.smimeCertOptions.find(certificate => this.alias.smimeCertificateId === certificate.id)
|
||||
return saved || { label: t('mail', 'No certificate') }
|
||||
},
|
||||
set(newVal) {
|
||||
this.certificate = newVal
|
||||
},
|
||||
},
|
||||
accountSmimeCertificate() {
|
||||
return {
|
||||
id: -1,
|
||||
alias: this.account.emailAddress,
|
||||
name: this.account.name,
|
||||
provisioned: !!this.account.provisioningId,
|
||||
smimeCertificateId: this.account.smimeCertificateId,
|
||||
}
|
||||
},
|
||||
aliases() {
|
||||
const aliases = this.account.aliases.map((alias) => {
|
||||
return {
|
||||
id: alias.id,
|
||||
alias: alias.alias,
|
||||
name: alias.name,
|
||||
provisioned: !!alias.provisioningId,
|
||||
smimeCertificateId: alias.smimeCertificateId,
|
||||
isAccountCertificate: false,
|
||||
}
|
||||
})
|
||||
aliases.push({ ...this.accountSmimeCertificate, isAccountCertificate: true })
|
||||
return aliases
|
||||
},
|
||||
smimeCertOptions() {
|
||||
// Only show certificates that are at least valid until tomorrow
|
||||
const now = (new Date().getTime() / 1000) + 3600 * 24
|
||||
const certs = this.smimeCertificates
|
||||
.filter((cert) => {
|
||||
return cert.hasKey
|
||||
&& cert.emailAddress === this.alias.alias
|
||||
&& cert.info.notAfter >= now
|
||||
&& cert.purposes.sign
|
||||
&& cert.purposes.encrypt
|
||||
// TODO: select a separate certificate for encryption?!
|
||||
})
|
||||
.map(this.mapCertificateToOption)
|
||||
.sort(compareSmimeCertificates)
|
||||
certs.push({ label: t('mail', 'No certificate') })
|
||||
|
||||
return certs
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
selectCertificate(certificate) {
|
||||
this.certificate = certificate
|
||||
},
|
||||
handleAlias(alias) {
|
||||
this.alias = alias
|
||||
this.savedCertificate = null
|
||||
},
|
||||
async updateSmimeCertificate() {
|
||||
if (this.alias.isAccountCertificate) {
|
||||
await this.$store.dispatch('updateAccountSmimeCertificate', {
|
||||
account: this.account,
|
||||
smimeCertificateId: this.certificate.id,
|
||||
}).then(() => {
|
||||
showSuccess(t('mail', 'Certificate updated'))
|
||||
}).catch((error) => {
|
||||
Logger.error('could not update account Smime ceritificate', { error })
|
||||
showError(t('mail', 'Could not update certificate'))
|
||||
}
|
||||
)
|
||||
} else {
|
||||
await this.$store.dispatch('updateAlias', {
|
||||
account: this.account,
|
||||
aliasId: this.alias.id,
|
||||
alias: this.alias.alias,
|
||||
name: this.alias.name,
|
||||
smimeCertificateId: this.certificate.id,
|
||||
}).then(() => {
|
||||
showSuccess(t('mail', 'Certificate updated'))
|
||||
}).catch((error) => {
|
||||
Logger.error('could not update alias Smime ceritificate', { error })
|
||||
showError(t('mail', 'Could not update certificate'))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
},
|
||||
/**
|
||||
* Map an S/MIME certificate from the db to a NcSelect option.
|
||||
*
|
||||
* @param {object} cert S/MIME certificate
|
||||
* @return {object} NcSelect option
|
||||
*/
|
||||
mapCertificateToOption(cert) {
|
||||
const label = this.t('mail', '{commonName} - Valid until {expiryDate}', {
|
||||
commonName: cert.info.commonName ?? cert.info.emailAddress,
|
||||
expiryDate: moment.unix(cert.info.notAfter).format('LL'),
|
||||
})
|
||||
return { ...cert, label }
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.multiselect--single {
|
||||
width: 100%;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.button-vue {
|
||||
margin-top: 4px !important;
|
||||
}
|
||||
</style>
|
Загрузка…
Ссылка в новой задаче