Files
cloudron-box/dashboard/src/views/UsersView.vue
T

352 lines
12 KiB
Vue
Raw Normal View History

2025-02-11 18:50:10 +01:00
<script setup>
import { useI18n } from 'vue-i18n';
const i18n = useI18n();
const t = i18n.t;
import { ref, onMounted, computed, useTemplateRef, inject } from 'vue';
import { Button, Menu, TextInput, SingleSelect, TableView, InputDialog } from '@cloudron/pankow';
2025-02-11 18:50:10 +01:00
import { ROLES } from '../constants.js';
import Section from '../components/Section.vue';
2025-02-16 17:04:58 +01:00
import UserDialog from '../components/UserDialog.vue';
2025-02-12 13:09:07 +01:00
import GroupDialog from '../components/GroupDialog.vue';
2025-02-12 15:54:04 +01:00
import ImpersonateDialog from '../components/ImpersonateDialog.vue';
2025-03-28 20:39:54 +01:00
import InvitationDialog from '../components/InvitationDialog.vue';
2025-02-13 17:15:48 +01:00
import PasswordResetDialog from '../components/PasswordResetDialog.vue';
2025-02-11 18:50:10 +01:00
import UsersModel from '../models/UsersModel.js';
import GroupsModel from '../models/GroupsModel.js';
import ProfileModel from '../models/ProfileModel.js';
const usersModel = UsersModel.create();
const groupsModel = GroupsModel.create();
const profileModel = ProfileModel.create();
const usersColumns = {
2025-03-26 16:04:58 +01:00
role: {
width: '33.5px'
},
user: {
label: t('users.users.user'),
sort: true
},
groups: {
label: t('users.users.groups'),
sort: true,
hideMobile: true,
},
2025-02-11 18:50:10 +01:00
actions: {}
};
const actionMenuModel = ref([]);
const actionMenuElement = useTemplateRef('actionMenuElement');
function onUserActionMenu(user, event) {
actionMenuModel.value = [{
icon: 'fa-solid fa-paper-plane',
label: t('users.users.invitationTooltip'),
visible: !user.inviteAccepted && !isMe(user) && !user.source,
disabled: !canEdit(user),
action: onInvitation.bind(null, user),
}, {
icon: 'fa-solid fa-key',
label: t('users.users.resetPasswordTooltip'),
visible: user.inviteAccepted && !user.source,
disabled: !canEdit(user),
action: onPasswordReset.bind(null, user),
}, {
icon: 'fa-solid fa-user-secret',
label: t('users.users.setGhostTooltip'),
visible: canImpersonate(user),
action: onImpersonate.bind(null, user),
}, {
icon: 'fa fa-pencil-alt',
label: t('main.action.edit'),
disabled: !canEdit(user),
action: onEditOrAddUser.bind(null, user),
}, {
separator: true,
}, {
icon: 'fa-solid fa-trash-alt',
label: t('main.action.remove'),
disabled: !canEdit(user),
action: onRemoveUser.bind(null, user),
}];
actionMenuElement.value.open(event, event.currentTarget);
}
2025-02-11 18:50:10 +01:00
const groupsColumns = {
2025-03-26 16:04:58 +01:00
name: {
label: t('users.groups.name'),
sort: true
},
users: {
label: t('users.groups.users'),
sort: true,
hideMobile: true,
},
2025-02-11 18:50:10 +01:00
actions: {}
};
function onGroupActionMenu(group, event) {
actionMenuModel.value = [{
icon: 'fa-solid fa-pencil-alt',
label: t('main.action.edit'),
action: onEditOrAddGroup.bind(null, group),
}, {
separator: true,
}, {
icon: 'fa-solid fa-trash-alt',
label: t('main.action.remove'),
action: onRemoveGroup.bind(null, group),
}];
actionMenuElement.value.open(event, event.currentTarget);
}
2025-02-11 18:50:10 +01:00
const profile = ref({});
2025-02-11 20:21:35 +01:00
const busy = ref(true);
2025-02-11 18:50:10 +01:00
const filterOptions = ref([
{ id: 'all', name: 'All Users' },
{ id: 'active', name: 'Active Users' },
{ id: 'inactive', name: 'Inactive Users' }
]);
const users = ref([]);
const usersById = ref({});
const search = ref('');
const filter = ref(filterOptions.value[0].id);
const groups = ref([]);
const groupsById = ref({});
const roles = ref([]);
const features = inject('features');
const subscriptionRequiredDialog = inject('subscriptionRequiredDialog');
2025-02-12 13:09:07 +01:00
const inputDialog = useTemplateRef('inputDialog');
2025-02-16 17:04:58 +01:00
const userDialog = useTemplateRef('userDialog');
2025-02-12 13:09:07 +01:00
const groupDialog = useTemplateRef('groupDialog');
2025-02-12 15:54:04 +01:00
const impersonateDialog = useTemplateRef('impersonateDialog');
2025-02-13 17:15:48 +01:00
const passwordResetDialog = useTemplateRef('passwordResetDialog');
2025-02-12 13:09:07 +01:00
2025-02-11 20:21:35 +01:00
const filteredUsers = computed(() => {
return users.value.filter(u => {
const username = u.username || ''; // username is null if not yet set
return username.indexOf(search.value) !== -1 || u.email.indexOf(search.value) !== -1 || u.displayName.indexOf(search.value) !== -1;
2025-02-11 20:21:35 +01:00
}).filter(u => {
if (filter.value === 'active') {
return u.active;
} else if (filter.value === 'inactive') {
return !u.active;
} else {
return true;
}
});
});
2025-02-11 18:50:10 +01:00
async function refreshUsers() {
const [error, result] = await usersModel.list();
if (error) return console.error(error);
usersById.value = {};
users.value = result;
result.forEach(user => {
usersById.value[user.id] = user;
});
}
async function refreshGroups() {
const [error, result] = await groupsModel.list();
if (error) return console.error(error);
groupsById.value = {};
groups.value = result;
result.forEach(group => {
groupsById.value[group.id] = group;
});
}
function canEdit(user) {
const roleInt1 = roles.value.findIndex(function (role) { return role.id === profile.value.role; });
const roleInt2 = roles.value.findIndex(function (role) { return role.id === user.role; });
return (roleInt1 - roleInt2) >= 0;
}
function canImpersonate(user) {
// only admins can impersonate
if (!profile.value.isAtLeastAdmin) return false;
// only users with username can be impersonated
if (!user.username) return false;
// normal admins cannot impersonate owners
if (!profile.value.isAtLeastOwner && [ ROLES.OWNER ].indexOf(user.role) !== -1) return false;
return true;
}
function isMe(user) {
return user.username === profile.value.username;
}
function groupMembers(group) {
return group.userIds.filter(function (uid) { return !!usersById.value[uid]; }).map(function (uid) { return usersById.value[uid].username || usersById.value[uid].email; }).join(' ');
}
2025-02-12 15:54:04 +01:00
function onImpersonate(user) {
impersonateDialog.value.open(user);
}
2025-02-13 17:15:48 +01:00
function onPasswordReset(user) {
passwordResetDialog.value.open(user);
}
2025-02-16 17:04:58 +01:00
function onEditOrAddUser(user = null) {
2025-05-07 15:51:22 +02:00
if (user || features.value.userMaxCount > users.value.length) userDialog.value.open(user);
else subscriptionRequiredDialog.value.open();
2025-02-16 17:04:58 +01:00
}
2025-03-28 20:39:54 +01:00
const invitationDialog = useTemplateRef('invitationDialog');
2025-02-20 11:43:57 +01:00
function onInvitation(user) {
2025-03-28 20:39:54 +01:00
invitationDialog.value.open(user);
2025-02-20 11:43:57 +01:00
}
2025-02-12 13:09:07 +01:00
function onEditOrAddGroup(group = null) {
if (group || features.value.userGroups) groupDialog.value.open(group);
else subscriptionRequiredDialog.value.open();
2025-02-12 13:09:07 +01:00
}
async function onRemoveGroup(group) {
const yes = await inputDialog.value.confirm({
title: t('users.deleteGroupDialog.title', { name: group.name }),
message: t('users.deleteGroupDialog.description', { memberCount: group.memberCount }),
confirmStyle: 'danger',
confirmLabel: t('users.deleteGroupDialog.deleteAction'),
rejectLabel: t('main.dialog.cancel')
});
if (!yes) return;
const [error] = await groupsModel.remove(group.id);
if (error) console.error(error);
await refreshGroups();
}
2025-02-12 15:18:45 +01:00
async function onRemoveUser(user) {
const yes = await inputDialog.value.confirm({
title: t('users.deleteUserDialog.title', { username: (user.username || user.email) }),
message: t('users.deleteUserDialog.description'),
confirmStyle: 'danger',
confirmLabel: t('users.deleteUserDialog.deleteAction'),
rejectLabel: t('main.dialog.cancel')
});
if (!yes) return;
const [error] = await usersModel.remove(user.id);
if (error) console.error(error);
await refreshUsers();
}
2025-02-11 18:50:10 +01:00
onMounted(async () => {
const [error, result] = await profileModel.get();
if (error) return console.error(error);
profile.value = result;
await refreshUsers();
await refreshGroups();
// Order matters for permissions used in canEdit
roles.value = [
{ id: 'user', name: t('users.role.user'), disabled: false },
{ id: 'usermanager', name: t('users.role.usermanager'), disabled: false },
{ id: 'mailmanager', name: t('users.role.mailmanager'), disabled: false },
{ id: 'admin', name: t('users.role.admin'), disabled: !profile.value.isAtLeastAdmin },
{ id: 'owner', name: t('users.role.owner'), disabled: !profile.value.isAtLeastOwner }
];
2025-02-11 20:21:35 +01:00
busy.value = false;
2025-02-11 18:50:10 +01:00
});
</script>
<template>
<div class="content">
<Menu ref="actionMenuElement" :model="actionMenuModel" />
<InputDialog ref="inputDialog" />
<UserDialog ref="userDialog" @success="refreshUsers()"/>
<GroupDialog ref="groupDialog" @success="refreshGroups()"/>
<ImpersonateDialog ref="impersonateDialog" />
<PasswordResetDialog ref="passwordResetDialog" />
2025-08-07 13:41:50 +02:00
<InvitationDialog ref="invitationDialog" @refresh-required="refreshUsers()" />
2025-02-11 18:50:10 +01:00
<Section :title="$t('main.navbar.users')">
<template #header-buttons>
<TextInput v-model="search" placeholder="Search ..." />
<SingleSelect :options="filterOptions" option-key="id" option-label="name" v-model="filter" />
2025-02-16 17:04:58 +01:00
<Button icon="fa-solid fa-user-plus" @click="onEditOrAddUser()">{{ $t('users.newUserAction') }}</Button>
2025-02-11 18:50:10 +01:00
</template>
2025-07-16 12:49:22 +02:00
<TableView :columns="usersColumns" :model="filteredUsers" :busy="busy" style="max-height: 400px;">
2025-02-11 18:50:10 +01:00
<template #role="user">
<i class="fas fa-crown arrow" v-if="user.active && user.role === 'owner'" v-tooltip="$t('users.users.superadminTooltip')"></i>
<i class="fa fa-user-tie arrow" v-if="user.active && user.role === 'admin'" v-tooltip="$t('users.users.adminTooltip')"></i>
<i class="fas fa-users-cog arrow" v-if="user.active && user.role === 'usermanager'" v-tooltip="$t('users.users.usermanagerTooltip')"></i>
<i class="fas fa-mail-bulk arrow" v-if="user.active && user.role === 'mailmanager'" v-tooltip="$t('users.users.mailmanagerTooltip')"></i>
<i class="fa fa-ban" v-if="!user.active" v-tooltip="$t('users.users.inactiveTooltip')"></i>
</template>
<template #user="user">
{{ user.displayName }}
<span class="text-muted" style="margin: 0 6px" v-if="user.username">{{ user.username }}</span>
<span class="text-muted" style="margin: 0 6px" v-else>{{ user.email }}</span>
<i v-show="user.source" class="far fa-address-book" v-tooltip="$t('users.users.externalLdapTooltip')"></i>
2025-02-11 18:50:10 +01:00
</template>
<template #groups="user">
<span class="group-label" v-for="groupId in user.groupIds" :key="groupId">
{{ groupsById[groupId] ? groupsById[groupId].name : groupId }}
</span>
</template>
<template #actions="user">
<div style="text-align: right;">
<Button tool plain secondary @click.capture="onUserActionMenu(user, $event)" icon="fa-solid fa-ellipsis" />
2025-02-11 18:50:10 +01:00
</div>
</template>
</TableView>
2025-02-12 13:09:07 +01:00
<br/>
2025-02-11 18:50:10 +01:00
<div>{{ $t('users.users.count', { count: users.length }) }}</div>
</Section>
<Section :title="$t('users.groups.title')" :title-badge="!features.userGroups ? 'Upgrade' : ''">
2025-02-11 18:50:10 +01:00
<template #header-buttons>
2025-02-12 13:09:07 +01:00
<Button icon="fa-solid fa-plus" @click="onEditOrAddGroup()">{{ $t('users.groups.newGroupAction') }}</Button>
2025-02-11 18:50:10 +01:00
</template>
2025-07-16 12:49:22 +02:00
<TableView :columns="groupsColumns" :model="groups" :busy="busy" style="max-height: 400px;" :placeholder="$t('users.groups.emptyPlaceholder')">
2025-02-11 18:50:10 +01:00
<template #name="group">
2025-02-12 13:09:07 +01:00
{{ group.name }} &nbsp; <i v-if="group.source" class="far fa-address-book" v-tooltip="$t('users.groups.externalLdapTooltip')"></i>
2025-02-11 18:50:10 +01:00
</template>
<template #users="group">
{{ groupMembers(group) }}
</template>
<template #actions="group">
<div style="text-align: right;">
<Button tool plain secondary @click.capture="onGroupActionMenu(group, $event)" icon="fa-solid fa-ellipsis" />
2025-02-11 18:50:10 +01:00
</div>
</template>
</TableView>
2025-03-17 19:14:32 +01:00
<br/>
<div>{{ $t('users.groups.count', { count: groups.length }) }}</div>
2025-02-11 18:50:10 +01:00
</Section>
</div>
</template>
<style scoped>
.group-label {
padding: 0 5px;
}
</style>