Merge pull request #8175 from nextcloud/enhancement/alias-mapping-ui

improve certificate to alias mapping UI
This commit is contained in:
Christoph Wurst 2023-04-04 07:02:08 +02:00 коммит произвёл GitHub
Родитель 4da06df6d1 dbbb1e050f
Коммит 3886273f67
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
4 изменённых файлов: 207 добавлений и 120 удалений

Просмотреть файл

@ -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> &lt;{{ alias.alias }}&gt;
</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>