3
0
Fork 0
mirror of https://github.com/ZeppelinBot/Zeppelin.git synced 2025-05-10 20:35:02 +00:00

Add rudimentary user management to dashboard

This commit is contained in:
Dragory 2021-09-05 16:42:35 +03:00
parent 48c4b3578d
commit 3b09d2d679
No known key found for this signature in database
GPG key ID: 5F387BA66DF8AAC1
12 changed files with 395 additions and 81 deletions

View file

@ -1,70 +1,182 @@
<template>
<div>
<h1>Guild Access</h1>
<h1>Dashboard access</h1>
<p>
<img class="inline-block w-16 mr-4" style="vertical-align: -20px" src="../../img/squint.png"> Or here
On this page you can manage who has access to the server's Zeppelin dashboard.
</p>
<div v-for="permAssignment in permissionAssignments">
<strong>{{ permAssignment.type }} {{ permAssignment.target_id }}</strong>
<permission-tree :tree="permAssignment._permissionTree" :granted-permissions="permAssignment.permissions" :on-change="onTreeUpdate.bind(null, permAssignment)" />
<h2 class="mt-8">Roles</h2>
<ul>
<li>
<strong>Owner:</strong> All permissions. Managed automatically by the bot.
</li>
<li>
<strong>Bot manager:</strong> Can manage dashboard users (including other bot managers) and edit server configuration
</li>
<li>
<strong>Bot operator:</strong> Can edit server configuration
</li>
</ul>
<h2 class="mt-8">Dashboard users</h2>
<div class="mt-4">
<div v-if="permanentPermissionAssignments.length === 0">
No dashboard users
</div>
<ul v-if="permanentPermissionAssignments.length">
<li v-for="perm in permanentPermissionAssignments">
<div class="flex gap-4">
<div>
<strong>{{ perm.target_id }}</strong>
</div>
<div class="flex gap-4">
<label class="block" v-if="isOwner(perm)">
<input type="checkbox" checked="checked" disabled>
Owner
</label>
<label class="block">
<input
type="checkbox"
:checked="hasPermission(perm, 'MANAGE_ACCESS')"
@change="ev => setPermissionValue(perm, 'MANAGE_ACCESS', ev.target.checked)"
:disabled="hasPermissionIndirectly(perm, 'MANAGE_ACCESS')"
>
Bot manager
</label>
<label class="block">
<input
type="checkbox"
:checked="hasPermission(perm, 'EDIT_CONFIG')"
@change="ev => setPermissionValue(perm, 'EDIT_CONFIG', ev.target.checked)"
:disabled="hasPermissionIndirectly(perm, 'EDIT_CONFIG')"
>
Bot operator
</label>
<a href="#" v-on:click="deletePermissionAssignment(perm)" v-if="!isOwner(perm)">
Delete
</a>
</div>
</div>
</li>
</ul>
<div class="mt-2">
<a href="#" v-on:click="addPermissionAssignment()">
Add new user
</a>
</div>
</div>
<h2 class="mt-8">Temporary dashboard users</h2>
<p>
You can add temporary dashboard users to e.g. request help from a person outside your organization.<br>
Temporary users always have <strong>Bot operator</strong> permissions.
</p>
<div v-if="temporaryPermissionAssignments.length === 0">
No temporary dashboard users
</div>
<ul v-if="temporaryPermissionAssignments.length">
<li v-for="perm in temporaryPermissionAssignments">
<div class="flex gap-4">
<div>
<strong>{{ perm.target_id }}</strong>
</div>
<div>
Expires in {{ formatTimeRemaining(perm) }}
</div>
<div>
<a href="#" v-on:click="deletePermissionAssignment(perm)">
Delete
</a>
</div>
</div>
</li>
</ul>
<div class="mt-2">
<a href="#" v-on:click="addTemporaryPermissionAssignment()">
Add temporary user for 1 hour
</a>
</div>
</div>
</template>
<script lang="ts">
import { ApiPermissions, permissionHierarchy } from "@shared/apiPermissions";
import PermissionTree from "./PermissionTree.vue";
import { applyStateToPermissionHierarchy } from "./permissionTreeUtils";
import { mapState } from "vuex";
import { GuildState } from "../../store/types";
import { ApiPermissions, hasPermission } from "@shared/apiPermissions";
import PermissionTree from "./PermissionTree.vue";
import { mapState } from "vuex";
import {
GuildPermissionAssignment,
GuildState,
RootState
} from "../../store/types";
import humanizeDuration from "humanize-duration";
import moment from "moment";
export default {
export default {
components: {PermissionTree},
data() {
return {
managerPermissions: new Set([ApiPermissions.ManageAccess]),
};
},
computed: {
...mapState({
canManage(state: RootState): boolean {
const guildPermissions = state.guilds.guildPermissionAssignments[this.$route.params.guildId] || [];
const myPermissions = guildPermissions.find(p => p.type === "USER" && p.target_id === state.auth.userId) || null;
return myPermissions && hasPermission(myPermissions.permissions, ApiPermissions.ManageAccess);
},
}),
...mapState<GuildState>("guilds", {
canManage(guilds) {
return guilds.myPermissions[this.$route.params.guildId]?.[ApiPermissions.ManageAccess];
permanentPermissionAssignments(guilds: GuildState): GuildPermissionAssignment[] {
return (guilds.guildPermissionAssignments[this.$route.params.guildId] || []).filter(perm => perm.expires_at == null);
},
permissionAssignments(guilds) {
return (guilds.guildPermissionAssignments[this.$route.params.guildId] || []).map(permAssignment => {
return {
...permAssignment,
_permissionTree: applyStateToPermissionHierarchy(permissionHierarchy, permAssignment.permissions, this.managerPermissions),
};
});
temporaryPermissionAssignments(guilds: GuildState): GuildPermissionAssignment[] {
return (guilds.guildPermissionAssignments[this.$route.params.guildId] || []).filter(perm => perm.expires_at != null);
},
}),
},
// beforeMount() {
// this.tree = applyStateToPermissionHierarchy(permissionHierarchy, this.grantedPermissions, this.managerPermissions);
// },
async mounted() {
await this.$store.dispatch("guilds/checkPermission", {
guildId: this.$route.params.guildId,
permission: ApiPermissions.ManageAccess,
});
await this.$store.dispatch("guilds/loadGuildPermissionAssignments", this.$route.params.guildId).catch(() => {});
if (! this.canManage) {
this.$router.push('/dashboard');
return;
}
await this.$store.dispatch("guilds/loadGuildPermissionAssignments", this.$route.params.guildId);
},
methods: {
// updateTreeState() {
// this.tree = applyStateToPermissionHierarchy(permissionHierarchy, this.grantedPermissions, this.managerPermissions);
// },
//
// onChange() {
// this.updateTreeState();
// }
isOwner(perm: GuildPermissionAssignment) {
return perm.permissions.has(ApiPermissions.Owner);
},
hasPermission(perm: GuildPermissionAssignment, permissionName: ApiPermissions) {
return hasPermission(perm.permissions, permissionName);
},
hasPermissionIndirectly(perm: GuildPermissionAssignment, permissionName: ApiPermissions) {
return hasPermission(perm.permissions, permissionName) && ! perm.permissions.has(permissionName);
},
setPermissionValue(perm: GuildPermissionAssignment, permissionName: ApiPermissions, value) {
if (value) {
perm.permissions.add(permissionName);
} else {
perm.permissions.delete(permissionName);
}
this.$store.dispatch("guilds/setTargetPermissions", {
guildId: this.$route.params.guildId,
type: perm.type,
targetId: perm.target_id,
permissions: Array.from(perm.permissions),
expiresAt: null,
});
this.$set(perm, "permissions", new Set(perm.permissions));
},
onTreeUpdate(targetPermissions) {
this.$store.dispatch("guilds/setTargetPermissions", {
@ -73,7 +185,58 @@
type: targetPermissions.type,
permissions: targetPermissions.permissions,
});
}
},
formatTimeRemaining(perm: GuildPermissionAssignment) {
const ms = Math.max(moment.utc(perm.expires_at).valueOf() - Date.now(), 0);
return humanizeDuration(ms, { largest: 2, round: true });
},
addPermissionAssignment() {
const userId = window.prompt("Enter user ID");
if (!userId) {
return;
}
this.$store.dispatch("guilds/setTargetPermissions", {
guildId: this.$route.params.guildId,
type: "USER",
targetId: userId,
permissions: [ApiPermissions.EditConfig],
expiresAt: null,
});
},
addTemporaryPermissionAssignment() {
const userId = window.prompt("Enter user ID");
if (!userId) {
return;
}
const expiresAt = moment.utc().add(1, "hour").format("YYYY-MM-DD HH:mm:ss");
this.$store.dispatch("guilds/setTargetPermissions", {
guildId: this.$route.params.guildId,
type: "USER",
targetId: userId,
permissions: [ApiPermissions.EditConfig],
expiresAt,
});
},
deletePermissionAssignment(perm: GuildPermissionAssignment) {
const confirm = window.confirm(`Remove ${perm.target_id} from dashboard users?`);
if (! confirm) {
return;
}
this.$store.dispatch("guilds/setTargetPermissions", {
guildId: this.$route.params.guildId,
type: perm.type,
targetId: perm.target_id,
permissions: [],
expiresAt: null,
});
},
}
}
</script>

View file

@ -17,9 +17,8 @@
<div class="text-gray-600 text-sm leading-tight">{{ guild.id }}</div>
</div>
<div class="pt-1">
<span class="inline-block bg-gray-700 rounded px-1 opacity-50 select-none">Info</span>
<router-link class="inline-block bg-gray-700 rounded px-1 hover:bg-gray-800" :to="'/dashboard/guilds/' + guild.id + '/config'">Config</router-link>
<span class="inline-block bg-gray-700 rounded px-1 opacity-50 select-none">Access</span>
<router-link v-if="canManageAccess(guild.id)" class="inline-block bg-gray-700 rounded px-1 hover:bg-gray-800" :to="'/dashboard/guilds/' + guild.id + '/access'">Access</router-link>
</div>
</div>
</div>
@ -28,12 +27,15 @@
</div>
</template>
<script>
import {mapState} from "vuex";
<script lang="ts">
import { mapState } from "vuex";
import { ApiPermissions, hasPermission } from "@shared/apiPermissions";
import { AuthState, GuildState } from "../../store/types";
export default {
async mounted() {
await this.$store.dispatch("guilds/loadAvailableGuilds");
await this.$store.dispatch("guilds/loadMyPermissionAssignments");
this.loading = false;
},
data() {
@ -41,7 +43,7 @@
},
computed: {
...mapState('guilds', {
guilds: state => {
guilds: (state: GuildState) => {
const guilds = Array.from(state.available.values());
guilds.sort((a, b) => {
if (a.name > b.name) return 1;
@ -52,7 +54,20 @@
});
return guilds;
},
guildPermissionAssignments: (state: GuildState) => state.guildPermissionAssignments,
}),
...mapState('auth', {
userId: (state: AuthState) => state.userId!,
}),
},
methods: {
canManageAccess(guildId: string) {
const guildPermissions = this.guildPermissionAssignments[guildId] || [];
const myPermissions = guildPermissions.find(p => p.type === "USER" && p.target_id === this.userId) || null;
return myPermissions && hasPermission(new Set(myPermissions.permissions), ApiPermissions.ManageAccess);
},
},
};
</script>