Merge pull request #352 from YunoHost/enh-permissions

Enh permissions (new selectize + confirmation for ssh/sftp)
This commit is contained in:
Alexandre Aubin 2021-04-17 02:00:39 +02:00 committed by GitHub
commit 9e70259899
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 321 additions and 417 deletions

View file

@ -14,7 +14,6 @@ import { openWebSocket, getResponseData, handleError } from './handlers'
* @property {Boolean} wait - If `true`, will display the waiting modal. * @property {Boolean} wait - If `true`, will display the waiting modal.
* @property {Boolean} websocket - if `true`, will open a websocket connection. * @property {Boolean} websocket - if `true`, will open a websocket connection.
* @property {Boolean} initial - if `true` and an error occurs, the dismiss button will trigger a go back in history. * @property {Boolean} initial - if `true` and an error occurs, the dismiss button will trigger a go back in history.
* @property {Boolean} noCache - if `true`, will disable the cache mecanism for this call.
* @property {Boolean} asFormData - if `true`, will send the data with a body encoded as `"multipart/form-data"` instead of `"x-www-form-urlencoded"`). * @property {Boolean} asFormData - if `true`, will send the data with a body encoded as `"multipart/form-data"` instead of `"x-www-form-urlencoded"`).
*/ */

View file

@ -1,151 +0,0 @@
<template>
<div class="selectize-base">
<b-input-group>
<b-input-group-prepend is-text>
<icon iname="search-plus" />
<span class="ml-1">{{ label }}</span>
</b-input-group-prepend>
<b-form-input
:class="visible ? null : 'collapsed'"
aria-controls="collapse" :aria-expanded="visible ? 'true' : 'false'"
@focus="onInputFocus" @blur="onInputBlur" @keydown="onInputKeydown"
v-model="search" ref="input"
/>
</b-input-group>
<b-collapse ref="collapse" v-model="visible">
<b-list-group tabindex="-1" @mouseover="onChoiceListOver" v-if="visible">
<b-list-group-item
v-for="(item, index) in filteredChoices" :key="item"
tabindex="-1" :active="index === focusedIndex" ref="choiceList"
@mousedown.prevent @mouseup.prevent="onSelect(item)"
>
{{ item | filter(format) }}
</b-list-group-item>
</b-list-group>
</b-collapse>
</div>
</template>
<script>
// FIXME add accessibility to ChoiceList
export default {
name: 'BaseSelectize',
props: {
choices: { type: Array, required: true },
label: { type: String, default: null },
// FIXME find a better way to pass filters
format: { type: Function, default: null }
},
data: () => ({
visible: false,
search: '',
focusedIndex: 0
}),
computed: {
filteredChoices () {
const search = this.search.toLowerCase()
return this.choices.filter(item => {
return item.toLowerCase().includes(search)
}).sort()
}
},
methods: {
onInputFocus ({ relatedTarget }) {
this.visible = true
this.focusedIndex = 0
// timeout needed else scrollIntoView won't work
if (!this.$refs.choiceList) return
setTimeout(() => {
this.$refs.choiceList[0].scrollIntoView({ behavior: 'instant', block: 'nearest', inline: 'nearest' })
}, 50)
},
onInputBlur ({ relatedTarget }) {
if (!this.$refs.collapse.$el.contains(relatedTarget)) {
this.visible = false
}
},
onInputKeydown (e) {
const { key } = e
const choicesLen = this.filteredChoices.length
if (choicesLen < 1) return
if (key === 'ArrowDown') {
e.preventDefault()
if (this.focusedIndex <= choicesLen) {
this.focusedIndex++
}
} else if (key === 'ArrowUp') {
e.preventDefault()
if (this.focusedIndex > 0) {
this.focusedIndex--
}
} else if (key === 'Enter') {
this.onSelect(this.filteredChoices[this.focusedIndex])
this.focusedIndex = 0
} else {
this.focusedIndex = 0
}
const elemToFocus = this.$refs.choiceList[this.focusedIndex]
if (elemToFocus) {
elemToFocus.scrollIntoView({ behavior: 'instant', block: 'nearest', inline: 'nearest' })
}
},
onChoiceListOver ({ target }) {
const index = this.$refs.choiceList.indexOf(target)
if (index > -1) {
this.focusedIndex = index
}
},
onSelect (item) {
this.$emit('selected', { item, index: this.choices.indexOf(item) })
}
},
filters: {
filter: function (text, func) {
if (func) return func(text)
else return text
}
}
}
</script>
<style lang="scss" scoped>
.collapse {
position: relative;
width: 100%;
}
// disable collapse animation
.collapsing {
-webkit-transition: none;
transition: none;
display: none;
}
.list-group {
margin-top: .5rem;
max-height: 10rem;
overflow-y: auto;
position: absolute;
z-index: 10;
width: 100%;
}
.list-group-item {
padding-top: 0;
padding-bottom: 0;
min-height: 2rem;
line-height: 1.75rem;
cursor: pointer;
}
</style>

View file

@ -0,0 +1,154 @@
<template>
<div class="tags-selectize">
<b-form-tags
v-bind="$attrs" v-on="$listeners"
:value="value" :id="id"
size="lg" class="p-0 border-0" no-outer-focus
>
<template v-slot="{ tags, disabled, addTag, removeTag }">
<ul v-if="!noTags && tags.length > 0" class="list-inline d-inline-block mb-2">
<li v-for="tag in tags" :key="id + '-' + tag" class="list-inline-item">
<b-form-tag
@remove="onRemoveTag({ option: tag, removeTag })"
:title="tag"
:disabled="disabled || disabledItems.includes(tag)"
variant="light"
class="border border-dark mb-2"
>
<icon v-if="tagIcon" :iname="tagIcon" /> {{ tag }}
</b-form-tag>
</li>
</ul>
<b-dropdown
ref="dropdown"
variant="outline-dark" block menu-class="w-100"
@keydown.native="onDropdownKeydown"
>
<template #button-content>
<icon iname="search-plus" /> {{ label }}
</template>
<b-dropdown-group class="search-group">
<b-dropdown-form @submit.stop.prevent="() => {}">
<b-form-group
:label="$t('search.for', { items: itemsName })"
label-cols-md="auto" label-size="sm" :label-for="id + '-search-input'"
:invalid-feedback="$t('search.not_found', { items: $tc('items.' + itemsName, 0) })"
:state="searchState" :disabled="disabled"
class="mb-0"
>
<b-form-input
ref="search-input" v-model="search"
:id="id + '-search-input'"
type="search" size="sm" autocomplete="off"
/>
</b-form-group>
</b-dropdown-form>
<b-dropdown-divider />
</b-dropdown-group>
<b-dropdown-item-button
v-for="option in availableOptions"
:key="option"
@click="onAddTag({ option, addTag })"
>
{{ option }}
</b-dropdown-item-button>
<b-dropdown-text v-if="!criteria && availableOptions.length === 0">
<icon iname="exclamation-triangle" />
{{ $t('items_verbose_items_left', { items: $tc('items.' + itemsName, 0) }) }}
</b-dropdown-text>
</b-dropdown>
</template>
</b-form-tags>
</div>
</template>
<script>
export default {
name: 'TagsSelectize',
props: {
value: { type: Array, required: true },
options: { type: Array, required: true },
id: { type: String, required: true },
itemsName: { type: String, required: true },
disabledItems: { type: Array, default: () => ([]) },
// By default `addTag` and `removeTag` have to be executed manually by listening to 'tag-update'.
auto: { type: Boolean, default: false },
noTags: { type: Boolean, default: false },
label: { type: String, default: null },
tagIcon: { type: String, default: null }
},
data () {
return {
search: ''
}
},
computed: {
criteria () {
return this.search.trim().toLowerCase()
},
availableOptions () {
const criteria = this.criteria
const options = this.options.filter(opt => {
return this.value.indexOf(opt) === -1 && !this.disabledItems.includes(opt)
})
if (criteria) {
return options.filter(opt => opt.toLowerCase().indexOf(criteria) > -1)
}
return options
},
searchState () {
return this.criteria && this.availableOptions.length === 0 ? false : null
}
},
methods: {
onAddTag ({ option, addTag }) {
this.$emit('tag-update', { action: 'add', option, applyMethod: addTag })
this.search = ''
if (this.auto) {
addTag(option)
}
},
onRemoveTag ({ option, removeTag }) {
this.$emit('tag-update', { action: 'remove', option, applyMethod: removeTag })
if (this.auto) {
removeTag(option)
}
},
onDropdownKeydown (e) {
// Allow to start searching after dropdown opening
if (
!['Tab', 'Space'].includes(e.code) &&
e.target === this.$refs.dropdown.$el.lastElementChild
) {
this.$refs['search-input'].focus()
}
}
}
}
</script>
<style lang="scss" scoped>
::v-deep .dropdown-menu {
max-height: 300px;
overflow-y: auto;
padding-top: 0;
.search-group {
padding-top: .5rem;
position: sticky;
top: 0;
background-color: white;
}
}
</style>

View file

@ -1,98 +0,0 @@
<template lang="html">
<div class="selectize-zone">
<div id="selected-items" v-if="selected.length > 0">
<b-button-group size="sm" v-for="item in filteredSelected" :key="item">
<b-button :to="itemRoute ? {name: itemRoute, params: {name: item}} : null" class="item-btn btn-light btn-outline-dark">
<icon :iname="itemIcon" /> {{ item | filter(format) }}
</b-button>
<b-button
v-if="!removable || removable(item)"
class="remove-btn btn-outline-dark" variant="warning"
@click="onRemove(item)"
>
<icon :title="$t('delete')" iname="minus" />
</b-button>
</b-button-group>
</div>
<base-selectize
v-if="choices.length"
:choices="choices"
:format="format"
:label="label"
@selected="$emit('change', { ...$event, action: 'add' })"
/>
</div>
</template>
<script>
import BaseSelectize from '@/components/BaseSelectize'
export default {
name: 'ZoneSelectize',
props: {
itemIcon: { type: String, default: null },
itemRoute: { type: String, default: null },
selected: { type: Array, required: true },
// needed by SelectizeBase
choices: { type: Array, required: true },
label: { type: String, default: null },
format: { type: Function, default: null },
removable: { type: Function, default: null }
},
data: () => ({
visible: false,
search: '',
focusedIndex: 0
}),
computed: {
filteredSelected () {
return [...this.selected].sort()
}
},
methods: {
onRemove (item) {
this.$emit('change', { item, index: this.selected.indexOf(item), action: 'remove' })
}
},
filters: {
filter: function (text, func) {
if (func) return func(text)
else return text
}
},
components: {
BaseSelectize
}
}
</script>
<style lang="scss" scoped>
#selected-items {
margin-bottom: .75rem;
display: flex;
flex-wrap: wrap;
.btn-group {
margin-right: .5rem;
margin-bottom: .5rem;
.item-btn {
.icon {
margin-right: .25rem;
}
}
}
}
.fa-minus {
position: relative;
top: 1px;
}
</style>

View file

@ -28,8 +28,8 @@
"confirm_app_default": "أمتأكد مِن أنك تود تعيين هذا التطبيق كبرنامج إفتراضي ؟", "confirm_app_default": "أمتأكد مِن أنك تود تعيين هذا التطبيق كبرنامج إفتراضي ؟",
"confirm_change_maindomain": "متأكد من أنك تريد تغيير النطاق الرئيسي ؟", "confirm_change_maindomain": "متأكد من أنك تريد تغيير النطاق الرئيسي ؟",
"confirm_delete": "هل تود حقًا حذف {name} ؟", "confirm_delete": "هل تود حقًا حذف {name} ؟",
"confirm_firewall_open": "متأكد مِن أنك تود فتح منفذ {port} ؟ (بروتوكول : {protocol}، إتصال : {connection})", "confirm_firewall_allow": "متأكد مِن أنك تود فتح منفذ {port} ؟ (بروتوكول : {protocol}، إتصال : {connection})",
"confirm_firewall_close": "متأكد مِن أنك تود إغلاق منفذ {port} ؟ (بروتوكول : {protocol}، إتصال : {connection})", "confirm_firewall_disallow": "متأكد مِن أنك تود إغلاق منفذ {port} ؟ (بروتوكول : {protocol}، إتصال : {connection})",
"confirm_install_custom_app": "إنّ خيار تنصيب تطبيقات خارجية قد يؤثر على أمان نظامكم. ربما وجب عليكم ألا تقوموا بالتنصيب إلا إن كنتم حقا مدركون بما أنتم فاعلين. هل أنتم مستعدون للمخاطرة؟", "confirm_install_custom_app": "إنّ خيار تنصيب تطبيقات خارجية قد يؤثر على أمان نظامكم. ربما وجب عليكم ألا تقوموا بالتنصيب إلا إن كنتم حقا مدركون بما أنتم فاعلين. هل أنتم مستعدون للمخاطرة؟",
"confirm_install_domain_root": "لن يكون بإمكانك تنصيب أي برنامج آخر على {domain}. هل تريد المواصلة ؟", "confirm_install_domain_root": "لن يكون بإمكانك تنصيب أي برنامج آخر على {domain}. هل تريد المواصلة ؟",
"confirm_postinstall": "إنك بصدد إطلاق خطوة ما بعد التنصيب على النطاق {domain}. سوف تستغرق العملية بضع دقائق، لذلك *يُرجى عدم إيقاف العملية*.", "confirm_postinstall": "إنك بصدد إطلاق خطوة ما بعد التنصيب على النطاق {domain}. سوف تستغرق العملية بضع دقائق، لذلك *يُرجى عدم إيقاف العملية*.",

View file

@ -28,8 +28,8 @@
"confirm_app_default": "Està segur de voler fer aquesta aplicació predeterminada?", "confirm_app_default": "Està segur de voler fer aquesta aplicació predeterminada?",
"confirm_change_maindomain": "Està segur de voler canviar el domini principal?", "confirm_change_maindomain": "Està segur de voler canviar el domini principal?",
"confirm_delete": "Està segur de voler eliminar {name}?", "confirm_delete": "Està segur de voler eliminar {name}?",
"confirm_firewall_open": "Està segur de voler obrir el port {port}? (protocol: {protocol}, connexió: {connection})", "confirm_firewall_allow": "Està segur de voler obrir el port {port}? (protocol: {protocol}, connexió: {connection})",
"confirm_firewall_close": "Està segur de voler tancar el port {port}? (protocol: {protocol}, connexió: {connection})", "confirm_firewall_disallow": "Està segur de voler tancar el port {port}? (protocol: {protocol}, connexió: {connection})",
"confirm_install_custom_app": "ATENCIÓ! La instal·lació d'aplicacions de terceres parts pot comprometre la integritat i seguretat del seu sistema. No hauríeu d'instal·lar-ne a no ser que sapigueu el que feu. Esteu segurs de voler córrer aquest risc?", "confirm_install_custom_app": "ATENCIÓ! La instal·lació d'aplicacions de terceres parts pot comprometre la integritat i seguretat del seu sistema. No hauríeu d'instal·lar-ne a no ser que sapigueu el que feu. Esteu segurs de voler córrer aquest risc?",
"confirm_install_domain_root": "No podrà instal·lar cap altra aplicació {domain}. Vol continuar?", "confirm_install_domain_root": "No podrà instal·lar cap altra aplicació {domain}. Vol continuar?",
"confirm_migrations_skip": "Saltar-se les migracions no està recomanat. Està segur de voler continuar?", "confirm_migrations_skip": "Saltar-se les migracions no està recomanat. Està segur de voler continuar?",

View file

@ -185,8 +185,8 @@
"confirm_update_apps": "Möchtest du wirklich alle Anwendungen aktualisieren?", "confirm_update_apps": "Möchtest du wirklich alle Anwendungen aktualisieren?",
"confirm_upnp_enable": "Möchtest du wirklich UPnP aktivieren?", "confirm_upnp_enable": "Möchtest du wirklich UPnP aktivieren?",
"confirm_upnp_disable": "Möchtest du wirklich UPnP deaktivieren?", "confirm_upnp_disable": "Möchtest du wirklich UPnP deaktivieren?",
"confirm_firewall_open": "Möchtest du wirklich Port {port}1 öffnen? (Protokoll: {protocol}2, Verbindung: {connection}3)", "confirm_firewall_allow": "Möchtest du wirklich Port {port}1 öffnen? (Protokoll: {protocol}2, Verbindung: {connection}3)",
"confirm_firewall_close": "Möchtest du wirklich Port {port}1 schließen? (Protokoll: {protocol}2, Verbindung: {connection}3)", "confirm_firewall_disallow": "Möchtest du wirklich Port {port}1 schließen? (Protokoll: {protocol}2, Verbindung: {connection}3)",
"confirm_update_specific_app": "Möchtest du wirklich {app} aktualisieren?", "confirm_update_specific_app": "Möchtest du wirklich {app} aktualisieren?",
"confirm_reboot_action_reboot": "Möchtest du wirklich den Server neustarten?", "confirm_reboot_action_reboot": "Möchtest du wirklich den Server neustarten?",
"confirm_reboot_action_shutdown": "Möchtest du wirklich den Server herunterfahren?", "confirm_reboot_action_shutdown": "Möchtest du wirklich den Server herunterfahren?",

View file

@ -94,6 +94,7 @@
"confirm_delete": "Are you sure you want to delete {name}?", "confirm_delete": "Are you sure you want to delete {name}?",
"confirm_firewall_allow": "Are you sure you want to open port {port} (protocol: {protocol}, connection: {connection})", "confirm_firewall_allow": "Are you sure you want to open port {port} (protocol: {protocol}, connection: {connection})",
"confirm_firewall_disallow": "Are you sure you want to close port {port} (protocol: {protocol}, connection: {connection})", "confirm_firewall_disallow": "Are you sure you want to close port {port} (protocol: {protocol}, connection: {connection})",
"confirm_group_add_access_permission": "Are you sure you want to grant {perm} access to {name}? Such access significantly increases the attack surface if {name} happens to be a malicious person. You should only do so if you TRUST this person/group.",
"confirm_install_custom_app": "WARNING! Installing 3rd party applications may compromise the integrity and security of your system. You should probably NOT install it unless you know what you are doing. Are you willing to take that risk?", "confirm_install_custom_app": "WARNING! Installing 3rd party applications may compromise the integrity and security of your system. You should probably NOT install it unless you know what you are doing. Are you willing to take that risk?",
"confirm_install_domain_root": "Are you sure you want to install this application on '/'? You will not be able to install any other app on {domain}", "confirm_install_domain_root": "Are you sure you want to install this application on '/'? You will not be able to install any other app on {domain}",
"confirm_app_install": "Are you sure you want to install this application?", "confirm_app_install": "Are you sure you want to install this application?",
@ -242,10 +243,12 @@
"groups": "no groups | group | {c} groups", "groups": "no groups | group | {c} groups",
"installed_apps": "no installed apps | installed app | {c} installed apps", "installed_apps": "no installed apps | installed app | {c} installed apps",
"logs": "no logs | log | {c} logs", "logs": "no logs | log | {c} logs",
"permissions": "no permissions | permission | {c} permissions",
"services": "no services | service | {c} services", "services": "no services | service | {c} services",
"users": "no users | user | {c} users" "users": "no users | user | {c} users"
}, },
"items_verbose_count": "There is {items}.", "items_verbose_count": "There are {items}.",
"items_verbose_items_left": "There are {items} left.",
"label": "Label", "label": "Label",
"label_for_manifestname": "Label for {name}", "label_for_manifestname": "Label for {name}",
"last_ran": "Last time ran:", "last_ran": "Last time ran:",
@ -412,7 +415,7 @@
"save": "Save", "save": "Save",
"search": { "search": {
"for": "Search for {items}...", "for": "Search for {items}...",
"not_found": "There is {items} matching your criteria." "not_found": "There are {items} matching your criteria."
}, },
"select_all": "Select all", "select_all": "Select all",
"select_none": "Select none", "select_none": "Select none",

View file

@ -32,7 +32,7 @@
"hook_conf_ssh": "SSH", "hook_conf_ssh": "SSH",
"confirm_update_system": "Ĉu vi certas, ke vi volas ĝisdatigi ĉiujn sistemajn pakaĵojn ?", "confirm_update_system": "Ĉu vi certas, ke vi volas ĝisdatigi ĉiujn sistemajn pakaĵojn ?",
"installation_complete": "Kompleta instalado", "installation_complete": "Kompleta instalado",
"confirm_firewall_open": "Ĉu vi certas, ke vi volas malfermi havenojn {port} ? (Protokoloj {protocol}, konekto: {connection})", "confirm_firewall_allow": "Ĉu vi certas, ke vi volas malfermi havenojn {port} ? (Protokoloj {protocol}, konekto: {connection})",
"confirm_postinstall": "Vi tuj lanĉos la postinstalaran procezon sur la domajno {domain}. Eble daŭras kelkajn minutojn, *ne interrompu la operacion*.", "confirm_postinstall": "Vi tuj lanĉos la postinstalaran procezon sur la domajno {domain}. Eble daŭras kelkajn minutojn, *ne interrompu la operacion*.",
"description": "priskribo", "description": "priskribo",
"hook_conf_ynh_mysql": "MySQL pasvorto", "hook_conf_ynh_mysql": "MySQL pasvorto",
@ -84,7 +84,7 @@
"hook_data_mail": "Poŝto", "hook_data_mail": "Poŝto",
"backup_create": "Krei sekurkopion", "backup_create": "Krei sekurkopion",
"confirm_uninstall": "Ĉu vi certas, ke vi volas malinstali {name} ?", "confirm_uninstall": "Ĉu vi certas, ke vi volas malinstali {name} ?",
"confirm_firewall_close": "Ĉu vi certas, ke vi volas fermi havenon {port} ? (protokolo: {protocol}, rilato: {connection})", "confirm_firewall_disallow": "Ĉu vi certas, ke vi volas fermi havenon {port} ? (protokolo: {protocol}, rilato: {connection})",
"created_at": "Kreita ĉe", "created_at": "Kreita ĉe",
"confirm_app_change_url": "Ĉu vi certas, ke vi volas ŝanĝi la URL-aliron de la aplikaĵo?", "confirm_app_change_url": "Ĉu vi certas, ke vi volas ŝanĝi la URL-aliron de la aplikaĵo?",
"ipv6": "IPv6", "ipv6": "IPv6",

View file

@ -180,8 +180,8 @@
"revert_to_selfsigned_cert_message": "Si realmente lo desea, puede reinstalar un certificado autofirmado. (No recomendado)", "revert_to_selfsigned_cert_message": "Si realmente lo desea, puede reinstalar un certificado autofirmado. (No recomendado)",
"revert_to_selfsigned_cert": "Volver a un certificado autofirmado", "revert_to_selfsigned_cert": "Volver a un certificado autofirmado",
"user_mailbox_use": "Espacio utilizado", "user_mailbox_use": "Espacio utilizado",
"confirm_firewall_open": "¿Está seguro de que desea abrir el puerto {port}? (protocolo: {protocol}, conexión: {connection})", "confirm_firewall_allow": "¿Está seguro de que desea abrir el puerto {port}? (protocolo: {protocol}, conexión: {connection})",
"confirm_firewall_close": "¿Está seguro de que desea cerrar el puerto {port}? (protocolo: {protocol}, conexión: {connection})", "confirm_firewall_disallow": "¿Está seguro de que desea cerrar el puerto {port}? (protocolo: {protocol}, conexión: {connection})",
"confirm_service_start": "¿Está seguro de que desea iniciar {name}?", "confirm_service_start": "¿Está seguro de que desea iniciar {name}?",
"confirm_service_stop": "¿Está seguro de que desea parar {name}?", "confirm_service_stop": "¿Está seguro de que desea parar {name}?",
"confirm_update_apps": "¿Está seguro de que desea actualizar todas las aplicaciones?", "confirm_update_apps": "¿Está seguro de que desea actualizar todas las aplicaciones?",

View file

@ -180,8 +180,8 @@
"revert_to_selfsigned_cert_message": "Si vous le souhaitez vraiment, vous pouvez réinstaller un certificat auto-signé (non recommandé).", "revert_to_selfsigned_cert_message": "Si vous le souhaitez vraiment, vous pouvez réinstaller un certificat auto-signé (non recommandé).",
"revert_to_selfsigned_cert": "Retourner à un certificat auto-signé", "revert_to_selfsigned_cert": "Retourner à un certificat auto-signé",
"user_mailbox_use": "Espace utilisé de la boite aux lettres", "user_mailbox_use": "Espace utilisé de la boite aux lettres",
"confirm_firewall_open": "Voulez-vous vraiment ouvrir le port {port} ? (protocole : {protocol}, connexion : {connection})", "confirm_firewall_allow": "Voulez-vous vraiment ouvrir le port {port} ? (protocole : {protocol}, connexion : {connection})",
"confirm_firewall_close": "Voulez-vous vraiment fermer le port {port} ? (protocole : {protocol}, connexion : {connection})", "confirm_firewall_disallow": "Voulez-vous vraiment fermer le port {port} ? (protocole : {protocol}, connexion : {connection})",
"confirm_service_start": "Voulez-vous vraiment démarrer {name} ?", "confirm_service_start": "Voulez-vous vraiment démarrer {name} ?",
"confirm_service_stop": "Voulez-vous vraiment arrêter {name} ?", "confirm_service_stop": "Voulez-vous vraiment arrêter {name} ?",
"confirm_update_apps": "Voulez-vous vraiment mettre à jour toutes les applications ?", "confirm_update_apps": "Voulez-vous vraiment mettre à jour toutes les applications ?",

View file

@ -160,8 +160,8 @@
"app_info_changeurl_desc": "Cambia l'URL di accesso di questa applicazione (dominio e/o percorso).", "app_info_changeurl_desc": "Cambia l'URL di accesso di questa applicazione (dominio e/o percorso).",
"app_info_change_url_disabled_tooltip": "Questa funzionalità non è ancora stata implementata in questa applicazione", "app_info_change_url_disabled_tooltip": "Questa funzionalità non è ancora stata implementata in questa applicazione",
"confirm_app_change_url": "Sei sicuro di voler cambiare l'URL di accesso all'applicazione ?", "confirm_app_change_url": "Sei sicuro di voler cambiare l'URL di accesso all'applicazione ?",
"confirm_firewall_open": "Sei sicuro di voler aprire la porta {port}? (protocollo: {protocol}, connessione: {connection})", "confirm_firewall_allow": "Sei sicuro di voler aprire la porta {port}? (protocollo: {protocol}, connessione: {connection})",
"confirm_firewall_close": "Sei sicuro di voler chiudere la porta {port}? (protocollo: {protocol}, connessione: {connection})", "confirm_firewall_disallow": "Sei sicuro di voler chiudere la porta {port}? (protocollo: {protocol}, connessione: {connection})",
"confirm_migrations_skip": "Saltare le migrazioni è sconsigliato. Sei sicuro di volerlo fare?", "confirm_migrations_skip": "Saltare le migrazioni è sconsigliato. Sei sicuro di volerlo fare?",
"confirm_service_start": "Sei sicuro di voler eseguire {name}?", "confirm_service_start": "Sei sicuro di voler eseguire {name}?",
"confirm_service_stop": "Sei sicuro di voler fermare {name}?", "confirm_service_stop": "Sei sicuro di voler fermare {name}?",

View file

@ -28,8 +28,8 @@
"confirm_app_default": "Volètz vertadièrament definir aquesta aplicacion coma aplicacion per defaut?", "confirm_app_default": "Volètz vertadièrament definir aquesta aplicacion coma aplicacion per defaut?",
"confirm_change_maindomain": "Volètz vertadièrament cambiar lo domeni màger?", "confirm_change_maindomain": "Volètz vertadièrament cambiar lo domeni màger?",
"confirm_delete": "Volètz vertadièrament escafar {name}?", "confirm_delete": "Volètz vertadièrament escafar {name}?",
"confirm_firewall_open": "Volètz vertadièrament dobrir lo pòrt {port}? (protocòl: {protocol}, connexion: {connection})", "confirm_firewall_allow": "Volètz vertadièrament dobrir lo pòrt {port}? (protocòl: {protocol}, connexion: {connection})",
"confirm_firewall_close": "Volètz vertadièrament tampar lo pòrt {port}? (protocòl: {protocol}, connexion: {connection})", "confirm_firewall_disallow": "Volètz vertadièrament tampar lo pòrt {port}? (protocòl: {protocol}, connexion: {connection})",
"confirm_install_custom_app": "Atencion! Linstallacion daplicacions tèrças pòt perilhar lintegritat e la seguretat del sistèma. Auriatz PAS de ninstallar levat que saupèssetz çò que fasètz. Volètz vertadièrament córrer aqueste risc?", "confirm_install_custom_app": "Atencion! Linstallacion daplicacions tèrças pòt perilhar lintegritat e la seguretat del sistèma. Auriatz PAS de ninstallar levat que saupèssetz çò que fasètz. Volètz vertadièrament córrer aqueste risc?",
"confirm_install_domain_root": "Poiretz pas installar mai aplicacions sus {domain}. Contunhar?", "confirm_install_domain_root": "Poiretz pas installar mai aplicacions sus {domain}. Contunhar?",
"confirm_migrations_skip": "Passar las migracions es pas recomandat. Volètz vertadièrament o far?", "confirm_migrations_skip": "Passar las migracions es pas recomandat. Volètz vertadièrament o far?",

View file

@ -30,8 +30,8 @@
"logout": "Wyloguj", "logout": "Wyloguj",
"ok": "OK", "ok": "OK",
"confirm_app_install": "Czy na pewno chcesz zainstalować tę aplikację?", "confirm_app_install": "Czy na pewno chcesz zainstalować tę aplikację?",
"confirm_firewall_close": "Czy na pewno chcesz zamknąć port {port} (protocol:{protocol}, connection: {connection})", "confirm_firewall_disallow": "Czy na pewno chcesz zamknąć port {port} (protocol:{protocol}, connection: {connection})",
"confirm_firewall_open": "Czy na pewno chcesz otworzyć port {port} (protocol:{protocol}, connection: {connection})", "confirm_firewall_allow": "Czy na pewno chcesz otworzyć port {port} (protocol:{protocol}, connection: {connection})",
"confirm_delete": "Czy na pewno chcesz usunąć {name}?", "confirm_delete": "Czy na pewno chcesz usunąć {name}?",
"confirm_change_maindomain": "Czy na pewno chcesz zmienić domenę podstawową?", "confirm_change_maindomain": "Czy na pewno chcesz zmienić domenę podstawową?",
"confirm_app_default": "Czy na pewno chcesz ustawić tę aplikację jako domyślną?", "confirm_app_default": "Czy na pewno chcesz ustawić tę aplikację jako domyślną?",

View file

@ -137,8 +137,8 @@
"backup_new": "Nova cópia de segurança", "backup_new": "Nova cópia de segurança",
"check": "Verificação", "check": "Verificação",
"confirm_app_change_url": "Tem certeza que quer mudar o endereço URL para acessar esta aplicação?", "confirm_app_change_url": "Tem certeza que quer mudar o endereço URL para acessar esta aplicação?",
"confirm_firewall_open": "Tem certeza que quer abrir a porta {port}? (protocolo: {protocol}, conexão: {connection})", "confirm_firewall_allow": "Tem certeza que quer abrir a porta {port}? (protocolo: {protocol}, conexão: {connection})",
"confirm_firewall_close": "Tem certeza que quer fechar a porta {port}? (protocolo: {protocol}, conexão: {connection})", "confirm_firewall_disallow": "Tem certeza que quer fechar a porta {port}? (protocolo: {protocol}, conexão: {connection})",
"confirm_install_custom_app": "CUIDADO! Instalar aplicações de terceiros pode comprometer a integridade e a segurança do seu sistema. Provavelmente NÃO deveria instalar esta aplicação se não tiver certeza do que está fazendo. Quer correr esses riscos?", "confirm_install_custom_app": "CUIDADO! Instalar aplicações de terceiros pode comprometer a integridade e a segurança do seu sistema. Provavelmente NÃO deveria instalar esta aplicação se não tiver certeza do que está fazendo. Quer correr esses riscos?",
"confirm_install_domain_root": "Não será mas capaz de instalar outras aplicações em {domain}. Quer continuar?", "confirm_install_domain_root": "Não será mas capaz de instalar outras aplicações em {domain}. Quer continuar?",
"confirm_install_app_lowquality": "Aviso: esta aplicação pode funcionar mais não está bem integrada em Yunohost. Algumas funcionalidades como logon único e/ou cópia de segurança/restauro pode não ser disponível.", "confirm_install_app_lowquality": "Aviso: esta aplicação pode funcionar mais não está bem integrada em Yunohost. Algumas funcionalidades como logon único e/ou cópia de segurança/restauro pode não ser disponível.",

View file

@ -33,8 +33,8 @@
"confirm_app_default": "Вы хотите сделать это приложение приложением по умолчанию?", "confirm_app_default": "Вы хотите сделать это приложение приложением по умолчанию?",
"confirm_change_maindomain": "Вы хотите изменить главный домен?", "confirm_change_maindomain": "Вы хотите изменить главный домен?",
"confirm_delete": "Вы хотите удалить {name}1 ?", "confirm_delete": "Вы хотите удалить {name}1 ?",
"confirm_firewall_open": "Вы хотите открыть порт {port}1 ? (протокол {protocol}2, соединение {connection}3)", "confirm_firewall_allow": "Вы хотите открыть порт {port}1 ? (протокол {protocol}2, соединение {connection}3)",
"confirm_firewall_close": "Вы хотите закрыть порт {port}1 ? (протокол {protocol}2, соединение {connection}3)", "confirm_firewall_disallow": "Вы хотите закрыть порт {port}1 ? (протокол {protocol}2, соединение {connection}3)",
"confirm_install_custom_app": "Установка сторонних приложений может повредить безопасности вашей системы. Установка на вашу ответственность.", "confirm_install_custom_app": "Установка сторонних приложений может повредить безопасности вашей системы. Установка на вашу ответственность.",
"confirm_install_domain_root": "Вы больше не сможете устанавливать приложения на {domain}1. Продолжить?", "confirm_install_domain_root": "Вы больше не сможете устанавливать приложения на {domain}1. Продолжить?",
"confirm_restore": "Вы хотите восстановить {name}1 ?", "confirm_restore": "Вы хотите восстановить {name}1 ?",

View file

@ -1,6 +1,7 @@
import Vue from 'vue' import Vue from 'vue'
import api from '@/api' import api from '@/api'
import { isEmptyValue } from '@/helpers/commons'
export default { export default {
@ -14,31 +15,31 @@ export default {
}), }),
mutations: { mutations: {
'SET_DOMAINS' (state, domains) { 'SET_DOMAINS' (state, [domains]) {
state.domains = domains state.domains = domains
}, },
'ADD_DOMAINS' (state, { domain }) { 'ADD_DOMAINS' (state, [{ domain }]) {
state.domains.push(domain) state.domains.push(domain)
}, },
'DEL_DOMAINS' (state, domain) { 'DEL_DOMAINS' (state, [domain]) {
state.domains.splice(state.domains.indexOf(domain), 1) state.domains.splice(state.domains.indexOf(domain), 1)
}, },
'SET_MAIN_DOMAIN' (state, response) { 'SET_MAIN_DOMAIN' (state, [response]) {
state.main_domain = response.current_main_domain state.main_domain = response.current_main_domain
}, },
'UPDATE_MAIN_DOMAIN' (state, domain) { 'UPDATE_MAIN_DOMAIN' (state, [domain]) {
state.main_domain = domain state.main_domain = domain
}, },
'SET_USERS' (state, users) { 'SET_USERS' (state, [users]) {
state.users = Object.keys(users).length === 0 ? null : users state.users = Object.keys(users).length === 0 ? null : users
}, },
'ADD_USERS' (state, user) { 'ADD_USERS' (state, [user]) {
if (!state.users) state.users = {} if (!state.users) state.users = {}
Vue.set(state.users, user.username, user) Vue.set(state.users, user.username, user)
}, },
@ -60,7 +61,7 @@ export default {
this.commit('SET_USERS_DETAILS', payload) this.commit('SET_USERS_DETAILS', payload)
}, },
'DEL_USERS_DETAILS' (state, username) { 'DEL_USERS_DETAILS' (state, [username]) {
Vue.delete(state.users_details, username) Vue.delete(state.users_details, username)
if (state.users) { if (state.users) {
Vue.delete(state.users, username) Vue.delete(state.users, username)
@ -70,60 +71,80 @@ export default {
} }
}, },
'SET_GROUPS' (state, groups) { 'SET_GROUPS' (state, [groups]) {
state.groups = groups state.groups = groups
}, },
'ADD_GROUPS' (state, { name }) { 'ADD_GROUPS' (state, [{ name }]) {
if (state.groups !== undefined) { if (state.groups !== undefined) {
Vue.set(state.groups, name, { members: [], permissions: [] }) Vue.set(state.groups, name, { members: [], permissions: [] })
} }
}, },
'DEL_GROUPS' (state, groupname) { 'UPDATE_GROUPS' (state, [data, { groupName }]) {
Vue.set(state.groups, groupName, data)
},
'DEL_GROUPS' (state, [groupname]) {
Vue.delete(state.groups, groupname) Vue.delete(state.groups, groupname)
}, },
'SET_PERMISSIONS' (state, permissions) { 'SET_PERMISSIONS' (state, [permissions]) {
state.permissions = permissions state.permissions = permissions
},
'UPDATE_PERMISSIONS' (state, [_, { groupName, action, permId }]) {
// FIXME hacky way to update the store
const permissions = state.groups[groupName].permissions
if (action === 'add') {
permissions.push(permId)
} else if (action === 'remove') {
const index = permissions.indexOf(permId)
if (index > -1) permissions.splice(index, 1)
}
} }
}, },
actions: { actions: {
'GET' ({ state, commit, rootState }, { uri, param, humanKey, storeKey = uri, options = {} }) { 'GET' (
const noCache = !rootState.cache || options.noCache || false { state, commit, rootState },
{ uri, param, storeKey = uri, humanKey, noCache, options, ...extraParams }
) {
const currentState = param ? state[storeKey][param] : state[storeKey] const currentState = param ? state[storeKey][param] : state[storeKey]
// if data has already been queried, simply return // if data has already been queried, simply return
if (currentState !== undefined && !noCache) return currentState const ignoreCache = !rootState.cache || noCache || false
if (currentState !== undefined && !ignoreCache) return currentState
return api.fetch('GET', param ? `${uri}/${param}` : uri, null, humanKey, options).then(responseData => { return api.fetch('GET', param ? `${uri}/${param}` : uri, null, humanKey, options).then(responseData => {
const data = responseData[storeKey] ? responseData[storeKey] : responseData const data = responseData[storeKey] ? responseData[storeKey] : responseData
commit('SET_' + storeKey.toUpperCase(), param ? [param, data] : data) commit(
'SET_' + storeKey.toUpperCase(),
[param, data, extraParams].filter(item => !isEmptyValue(item))
)
return param ? state[storeKey][param] : state[storeKey] return param ? state[storeKey][param] : state[storeKey]
}) })
}, },
'POST' ({ state, commit }, { uri, storeKey = uri, data, humanKey, options }) { 'POST' ({ state, commit }, { uri, storeKey = uri, data, humanKey, options, ...extraParams }) {
return api.fetch('POST', uri, data, humanKey, options).then(responseData => { return api.fetch('POST', uri, data, humanKey, options).then(responseData => {
// FIXME api/domains returns null // FIXME api/domains returns null
if (responseData === null) responseData = data if (responseData === null) responseData = data
responseData = responseData[storeKey] ? responseData[storeKey] : responseData responseData = responseData[storeKey] ? responseData[storeKey] : responseData
commit('ADD_' + storeKey.toUpperCase(), responseData) commit('ADD_' + storeKey.toUpperCase(), [responseData, extraParams].filter(item => !isEmptyValue(item)))
return state[storeKey] return state[storeKey]
}) })
}, },
'PUT' ({ state, commit }, { uri, param, storeKey = uri, data, humanKey, options }) { 'PUT' ({ state, commit }, { uri, param, storeKey = uri, data, humanKey, options, ...extraParams }) {
return api.fetch('PUT', param ? `${uri}/${param}` : uri, data, humanKey, options).then(responseData => { return api.fetch('PUT', param ? `${uri}/${param}` : uri, data, humanKey, options).then(responseData => {
const data = responseData[storeKey] ? responseData[storeKey] : responseData const data = responseData[storeKey] ? responseData[storeKey] : responseData
commit('UPDATE_' + storeKey.toUpperCase(), param ? [param, data] : data) commit('UPDATE_' + storeKey.toUpperCase(), [param, data, extraParams].filter(item => !isEmptyValue(item)))
return param ? state[storeKey][param] : state[storeKey] return param ? state[storeKey][param] : state[storeKey]
}) })
}, },
'DELETE' ({ commit }, { uri, param, storeKey = uri, data, humanKey, options }) { 'DELETE' ({ commit }, { uri, param, storeKey = uri, data, humanKey, options, ...extraParams }) {
return api.fetch('DELETE', param ? `${uri}/${param}` : uri, data, humanKey, options).then(() => { return api.fetch('DELETE', param ? `${uri}/${param}` : uri, data, humanKey, options).then(() => {
commit('DEL_' + storeKey.toUpperCase(), param) commit('DEL_' + storeKey.toUpperCase(), [param, extraParams].filter(item => !isEmptyValue(item)))
}) })
} }
}, },

View file

@ -2,7 +2,7 @@
<view-search <view-search
items-name="groups" items-name="groups"
:search.sync="search" :search.sync="search"
:items="normalGroups" :items="primaryGroups"
:filtered-items="filteredGroups" :filtered-items="filteredGroups"
:queries="queries" :queries="queries"
@queries-response="onQueriesResponse" @queries-response="onQueriesResponse"
@ -16,13 +16,13 @@
<!-- PRIMARY GROUPS CARDS --> <!-- PRIMARY GROUPS CARDS -->
<card <card
v-for="(group, name) in filteredGroups" :key="name" collapsable v-for="(group, groupName) in filteredGroups" :key="groupName" collapsable
:title="group.isSpecial ? $t('group_' + name) : `${$t('group')} '${name}'`" icon="group" :title="group.isSpecial ? $t('group_' + groupName) : `${$t('group')} '${groupName}'`" icon="group"
> >
<template #header-buttons> <template #header-buttons>
<!-- DELETE GROUP --> <!-- DELETE GROUP -->
<b-button <b-button
v-if="!group.isSpecial" @click="deleteGroup(name)" v-if="!group.isSpecial" @click="deleteGroup(groupName)"
size="sm" variant="danger" size="sm" variant="danger"
> >
<icon iname="trash-o" /> {{ $t('delete') }} <icon iname="trash-o" /> {{ $t('delete') }}
@ -37,18 +37,18 @@
<b-col> <b-col>
<template v-if="group.isSpecial"> <template v-if="group.isSpecial">
<p class="text-primary"> <p class="text-primary">
<icon iname="info-circle" /> {{ $t('group_explain_' + name) }} <icon iname="info-circle" /> {{ $t('group_explain_' + groupName) }}
</p> </p>
<p class="text-primary" v-if="name === 'visitors'"> <p class="text-primary" v-if="groupName === 'visitors'">
<em>{{ $t('group_explain_visitors_needed_for_external_client') }}</em> <em>{{ $t('group_explain_visitors_needed_for_external_client') }}</em>
</p> </p>
</template> </template>
<template v-else> <template v-else>
<zone-selectize <tags-selectize
:choices="group.availableMembers" :selected="group.members" v-model="group.members" :options="usersOptions"
item-icon="user" :id="groupName + '-users'" :label="$t('group_add_member')"
:label="$t('group_add_member')" tag-icon="user" items-name="users"
@change="onUserChanged({ ...$event, name })" @tag-update="onUserChanged({ ...$event, groupName })"
/> />
</template> </template>
</b-col> </b-col>
@ -60,50 +60,45 @@
<strong>{{ $t('permissions') }}</strong> <strong>{{ $t('permissions') }}</strong>
</b-col> </b-col>
<b-col> <b-col>
<zone-selectize <tags-selectize
item-icon="key-modern" v-model="group.permissions" :options="permissionsOptions"
:choices="group.availablePermissions" :id="groupName + '-perms'" :label="$t('group_add_permission')"
:selected="group.permissions" tag-icon="key-modern" items-name="permissions"
:label="$t('group_add_permission')" @tag-update="onPermissionChanged({ ...$event, groupName })"
:format="formatPermission" :disabled-items="group.disabledItems"
:removable="name === 'visitors' ? removable : null"
@change="onPermissionChanged({ ...$event, name, groupType: 'normal' })"
/> />
</b-col> </b-col>
</b-row> </b-row>
</card> </card>
<!-- GROUP SPECIFIC CARD --> <!-- USER GROUPS CARD -->
<card <card
v-if="userGroups" collapsable v-if="userGroups" collapsable
:title="$t('group_specific_permissions')" icon="group" :title="$t('group_specific_permissions')" icon="group"
> >
<template v-for="(name, index) in userGroupsNames"> <template v-for="(userName, index) in activeUserGroups">
<b-row :key="name"> <b-row :key="userName">
<b-col md="3" lg="2"> <b-col md="3" lg="2">
<icon iname="user" /> <strong>{{ name }}</strong> <icon iname="user" /> <strong>{{ userName }}</strong>
</b-col> </b-col>
<b-col> <b-col>
<zone-selectize <tags-selectize
item-icon="key-modern" item-variant="dark" v-model="userGroups[userName].permissions" :options="permissionsOptions"
:choices="userGroups[name].availablePermissions" :id="userName + '-perms'" :label="$t('group_add_permission')"
:selected="userGroups[name].permissions" tag-icon="key-modern" items-name="permissions"
:label="$t('group_add_permission')" @tag-update="onPermissionChanged({ ...$event, groupName: userName })"
:format="formatPermission"
@change="onPermissionChanged({ ...$event, name, groupType: 'user' })"
/> />
</b-col> </b-col>
</b-row> </b-row>
<hr :key="index"> <hr :key="index">
</template> </template>
<base-selectize <tags-selectize
v-if="availableMembers.length" v-model="activeUserGroups" :options="usersOptions"
:label="$t('group_add_member')" id="user-groups" :label="$t('group_add_member')"
:choices="availableMembers" no-tags items-name="users"
:selected="userGroupsNames" @tag-update="onSpecificUserAdded"
@selected="onSpecificUserAdded"
/> />
</card> </card>
</view-search> </view-search>
@ -114,8 +109,7 @@ import Vue from 'vue'
import api from '@/api' import api from '@/api'
import { isEmptyValue } from '@/helpers/commons' import { isEmptyValue } from '@/helpers/commons'
import ZoneSelectize from '@/components/ZoneSelectize' import TagsSelectize from '@/components/TagsSelectize'
import BaseSelectize from '@/components/BaseSelectize'
// TODO add global search with type (search by: group, user, permission) // TODO add global search with type (search by: group, user, permission)
// TODO add vuex store update on inputs ? // TODO add vuex store update on inputs ?
@ -123,8 +117,7 @@ export default {
name: 'GroupList', name: 'GroupList',
components: { components: {
ZoneSelectize, TagsSelectize
BaseSelectize
}, },
data () { data () {
@ -136,134 +129,117 @@ export default {
], ],
search: '', search: '',
permissions: undefined, permissions: undefined,
normalGroups: undefined, permissionsOptions: undefined,
userGroups: undefined primaryGroups: undefined,
userGroups: undefined,
usersOptions: undefined,
activeUserGroups: undefined
} }
}, },
computed: { computed: {
filteredGroups () { filteredGroups () {
const groups = this.normalGroups const groups = this.primaryGroups
if (!groups) return if (!groups) return
const search = this.search.toLowerCase() const search = this.search.toLowerCase()
const filtered = {} const filtered = {}
for (const name in groups) { for (const groupName in groups) {
if (name.toLowerCase().includes(search)) { if (groupName.toLowerCase().includes(search)) {
filtered[name] = groups[name] filtered[groupName] = groups[groupName]
} }
} }
return isEmptyValue(filtered) ? null : filtered return isEmptyValue(filtered) ? null : filtered
},
userGroupsNames () {
const groups = this.userGroups
if (!groups) return
return Object.keys(groups).filter(name => {
return groups[name].permissions !== null
})
},
availableMembers () {
const groups = this.userGroups
if (!groups) return
return Object.keys(groups).filter(name => {
return groups[name].permissions === null
})
} }
}, },
methods: { methods: {
onQueriesResponse (users, allGroups, permissions) { onQueriesResponse (users, allGroups, permsDict) {
// Do not use computed properties to get values from the store here to avoid auto // Do not use computed properties to get values from the store here to avoid auto
// updates while modifying values. // updates while modifying values.
const normalGroups = {} const permissions = Object.entries(permsDict).map(([id, value]) => ({ id, ...value }))
const userGroups = {}
const userNames = users ? Object.keys(users) : [] const userNames = users ? Object.keys(users) : []
const primaryGroups = {}
const userGroups = {}
for (const groupName in allGroups) { for (const groupName in allGroups) {
// copy the group to unlink it from the store // copy the group to unlink it from the store
const group = { ...allGroups[groupName] } const group = { ...allGroups[groupName] }
group.availablePermissions = Object.keys(permissions).filter(perm => { group.permissions = group.permissions.map((perm) => {
// Remove 'email', 'xmpp' and protected permissions in visitors's permission choice list return permsDict[perm].label
if (groupName === 'visitors' && (['mail.main', 'xmpp.main'].includes(perm) || permissions[perm].protected)) {
return false
}
return !group.permissions.includes(perm)
}) })
if (userNames.includes(groupName)) { if (userNames.includes(groupName)) {
if (group.permissions.length === 0) {
// This forbid the user to appear in the displayed user list
group.permissions = null
}
userGroups[groupName] = group userGroups[groupName] = group
continue continue
} }
if (['visitors', 'all_users'].includes(groupName)) { group.isSpecial = ['visitors', 'all_users'].includes(groupName)
group.isSpecial = true
} else { if (groupName === 'visitors') {
group.availableMembers = userNames.filter(name => { // Forbid to add or remove a protected permission on group `visitors`
return !group.members.includes(name) group.disabledItems = permissions.filter(({ id }) => {
}) return ['mail.main', 'xmpp.main'].includes(id) || permsDict[id].protected
}).map(({ id }) => permsDict[id].label)
} }
normalGroups[groupName] = group
primaryGroups[groupName] = group
} }
this.permissions = permissions const activeUserGroups = Object.entries(userGroups).filter(([_, group]) => {
this.normalGroups = normalGroups return group.permissions.length > 0
this.userGroups = isEmptyValue(userGroups) ? null : userGroups }).map(([name]) => name)
},
onPermissionChanged ({ item, index, name, groupType, action }) { Object.assign(this, {
// const uri = 'users/permissions/' + item permissions,
// const data = { [action]: name } permissionsOptions: permissions.map(perm => perm.label),
const from = action === 'add' ? 'availablePermissions' : 'permissions' primaryGroups,
const to = action === 'add' ? 'permissions' : 'availablePermissions' userGroups: isEmptyValue(userGroups) ? null : userGroups,
api.put( usersOptions: userNames,
`users/permissions/${item}/${action}/${name}`, activeUserGroups
{},
{ key: 'permissions.' + action, perm: item.replace('.main', ''), name }
).then(() => {
this[groupType + 'Groups'][name][from].splice(index, 1)
this[groupType + 'Groups'][name][to].push(item)
}) })
}, },
onUserChanged ({ item, index, name, action }) { async onPermissionChanged ({ option, groupName, action, applyMethod }) {
const from = action === 'add' ? 'availableMembers' : 'members' const permId = this.permissions.find(perm => perm.label === option).id
const to = action === 'add' ? 'members' : 'availableMembers' if (action === 'add' && ['sftp.main', 'ssh.main'].includes(permId)) {
const confirmed = await this.$askConfirmation(
this.$i18n.t('confirm_group_add_access_permission', { name: groupName, perm: option })
)
if (!confirmed) return
}
api.put( api.put(
`users/groups/${name}/${action}/${item}`, // FIXME hacky way to update the store
{ uri: `users/permissions/${permId}/${action}/${groupName}`, storeKey: 'permissions', groupName, action, permId },
{}, {},
{ key: 'groups.' + action, user: item, name } { key: 'permissions.' + action, perm: option, name: groupName }
).then(() => { ).then(() => applyMethod(option))
this.normalGroups[name][from].splice(index, 1)
this.normalGroups[name][to].push(item)
})
}, },
onSpecificUserAdded ({ item }) { onUserChanged ({ option, groupName, action, applyMethod }) {
this.userGroups[item].permissions = [] api.put(
{ uri: `users/groups/${groupName}/${action}/${option}`, storeKey: 'groups', groupName },
{},
{ key: 'groups.' + action, user: option, name: groupName }
).then(() => applyMethod(option))
}, },
// FIXME Find a way to pass a filter to a component onSpecificUserAdded ({ option: userName, action, applyMethod }) {
formatPermission (name) { if (action === 'add') {
return this.permissions[name].label this.userGroups[userName].permissions = []
applyMethod(userName)
}
}, },
removable (name) { async deleteGroup (groupName) {
return this.permissions[name].protected === false const confirmed = await this.$askConfirmation(this.$i18n.t('confirm_delete', { name: groupName }))
},
async deleteGroup (name) {
const confirmed = await this.$askConfirmation(this.$i18n.t('confirm_delete', { name }))
if (!confirmed) return if (!confirmed) return
api.delete( api.delete(
{ uri: 'users/groups', param: name, storeKey: 'groups' }, {}, { key: 'groups.delete', name } { uri: 'users/groups', param: groupName, storeKey: 'groups' },
{},
{ key: 'groups.delete', name: groupName }
).then(() => { ).then(() => {
Vue.delete(this.normalGroups, name) Vue.delete(this.primaryGroups, groupName)
}) })
} }
} }