mirror of
https://github.com/ZeppelinBot/Zeppelin.git
synced 2025-03-15 05:41:51 +00:00
Add rudimentary user management to dashboard
This commit is contained in:
parent
48c4b3578d
commit
3b09d2d679
12 changed files with 395 additions and 81 deletions
|
@ -149,7 +149,7 @@ export function initAuth(app: express.Express) {
|
|||
return res.json({ valid: false });
|
||||
}
|
||||
|
||||
res.json({ valid: true });
|
||||
res.json({ valid: true, userId });
|
||||
});
|
||||
app.post("/auth/logout", ...apiTokenAuthHandlers(), async (req: Request, res: Response) => {
|
||||
await apiLogins.expireApiKey(req.user!.apiKey);
|
||||
|
|
|
@ -3,15 +3,21 @@ import express, { Request, Response } from "express";
|
|||
import { YAMLException } from "js-yaml";
|
||||
import { validateGuildConfig } from "../configValidator";
|
||||
import { AllowedGuilds } from "../data/AllowedGuilds";
|
||||
import { ApiPermissionAssignments } from "../data/ApiPermissionAssignments";
|
||||
import { ApiPermissionAssignments, ApiPermissionTypes } from "../data/ApiPermissionAssignments";
|
||||
import { Configs } from "../data/Configs";
|
||||
import { apiTokenAuthHandlers } from "./auth";
|
||||
import { hasGuildPermission, requireGuildPermission } from "./permissions";
|
||||
import { clientError, ok, serverError, unauthorized } from "./responses";
|
||||
import { loadYamlSafely } from "../utils/loadYamlSafely";
|
||||
import { ObjectAliasError } from "../utils/validateNoObjectAliases";
|
||||
import { isSnowflake } from "../utils";
|
||||
import moment from "moment-timezone";
|
||||
import { ApiAuditLog } from "../data/ApiAuditLog";
|
||||
import { AuditLogEventTypes } from "../data/apiAuditLogTypes";
|
||||
import { Queue } from "../Queue";
|
||||
|
||||
const apiPermissionAssignments = new ApiPermissionAssignments();
|
||||
const auditLog = new ApiAuditLog();
|
||||
|
||||
export function initGuildsAPI(app: express.Express) {
|
||||
const allowedGuilds = new AllowedGuilds();
|
||||
|
@ -25,6 +31,14 @@ export function initGuildsAPI(app: express.Express) {
|
|||
res.json(guilds);
|
||||
});
|
||||
|
||||
guildRouter.get(
|
||||
"/my-permissions", // a
|
||||
async (req: Request, res: Response) => {
|
||||
const permissions = await apiPermissionAssignments.getByUserId(req.user!.userId);
|
||||
res.json(permissions);
|
||||
},
|
||||
);
|
||||
|
||||
guildRouter.get("/:guildId", async (req: Request, res: Response) => {
|
||||
if (!(await hasGuildPermission(req.user!.userId, req.params.guildId, ApiPermissions.ViewGuild))) {
|
||||
return unauthorized(res);
|
||||
|
@ -101,5 +115,65 @@ export function initGuildsAPI(app: express.Express) {
|
|||
},
|
||||
);
|
||||
|
||||
const permissionManagementQueue = new Queue();
|
||||
guildRouter.post(
|
||||
"/:guildId/set-target-permissions",
|
||||
requireGuildPermission(ApiPermissions.ManageAccess),
|
||||
async (req: Request, res: Response) => {
|
||||
await permissionManagementQueue.add(async () => {
|
||||
const { type, targetId, permissions, expiresAt } = req.body;
|
||||
|
||||
if (type !== ApiPermissionTypes.User) {
|
||||
return clientError(res, "Invalid type");
|
||||
}
|
||||
if (!isSnowflake(targetId)) {
|
||||
return clientError(res, "Invalid targetId");
|
||||
}
|
||||
const validPermissions = new Set(Object.values(ApiPermissions));
|
||||
validPermissions.delete(ApiPermissions.Owner);
|
||||
if (!Array.isArray(permissions) || permissions.some(p => !validPermissions.has(p))) {
|
||||
return clientError(res, "Invalid permissions");
|
||||
}
|
||||
if (expiresAt != null && !moment.utc(expiresAt).isValid()) {
|
||||
return clientError(res, "Invalid expiresAt");
|
||||
}
|
||||
|
||||
const existingAssignment = await apiPermissionAssignments.getByGuildAndUserId(req.params.guildId, targetId);
|
||||
if (existingAssignment && existingAssignment.permissions.includes(ApiPermissions.Owner)) {
|
||||
return clientError(res, "Can't change owner permissions");
|
||||
}
|
||||
|
||||
if (permissions.length === 0) {
|
||||
await apiPermissionAssignments.removeUser(req.params.guildId, targetId);
|
||||
await auditLog.addEntry(req.params.guildId, req.user!.userId, AuditLogEventTypes.REMOVE_API_PERMISSION, {
|
||||
type: ApiPermissionTypes.User,
|
||||
target_id: targetId,
|
||||
});
|
||||
} else {
|
||||
const existing = await apiPermissionAssignments.getByGuildAndUserId(req.params.guildId, targetId);
|
||||
if (existing) {
|
||||
await apiPermissionAssignments.updateUserPermissions(req.params.guildId, targetId, permissions);
|
||||
await auditLog.addEntry(req.params.guildId, req.user!.userId, AuditLogEventTypes.EDIT_API_PERMISSION, {
|
||||
type: ApiPermissionTypes.User,
|
||||
target_id: targetId,
|
||||
permissions,
|
||||
expires_at: existing.expires_at,
|
||||
});
|
||||
} else {
|
||||
await apiPermissionAssignments.addUser(req.params.guildId, targetId, permissions, expiresAt);
|
||||
await auditLog.addEntry(req.params.guildId, req.user!.userId, AuditLogEventTypes.ADD_API_PERMISSION, {
|
||||
type: ApiPermissionTypes.User,
|
||||
target_id: targetId,
|
||||
permissions,
|
||||
expires_at: expiresAt,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ok(res);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
app.use("/guilds", guildRouter);
|
||||
}
|
||||
|
|
|
@ -48,12 +48,13 @@ export class ApiPermissionAssignments extends BaseRepository {
|
|||
});
|
||||
}
|
||||
|
||||
addUser(guildId, userId, permissions: ApiPermissions[]) {
|
||||
addUser(guildId, userId, permissions: ApiPermissions[], expiresAt: string | null = null) {
|
||||
return this.apiPermissions.insert({
|
||||
guild_id: guildId,
|
||||
type: ApiPermissionTypes.User,
|
||||
target_id: userId,
|
||||
permissions,
|
||||
expires_at: expiresAt,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -61,6 +62,19 @@ export class ApiPermissionAssignments extends BaseRepository {
|
|||
return this.apiPermissions.delete({ guild_id: guildId, type: ApiPermissionTypes.User, target_id: userId });
|
||||
}
|
||||
|
||||
async updateUserPermissions(guildId: string, userId: string, permissions: ApiPermissions[]): Promise<void> {
|
||||
await this.apiPermissions.update(
|
||||
{
|
||||
guild_id: guildId,
|
||||
type: ApiPermissionTypes.User,
|
||||
target_id: userId,
|
||||
},
|
||||
{
|
||||
permissions,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async clearExpiredPermissions() {
|
||||
await this.apiPermissions
|
||||
.createQueryBuilder()
|
||||
|
|
|
@ -8,7 +8,7 @@ export class ApiPermissionAssignment {
|
|||
@PrimaryColumn()
|
||||
guild_id: string;
|
||||
|
||||
@Column({ type: "string" })
|
||||
@Column({ type: String })
|
||||
@PrimaryColumn()
|
||||
type: ApiPermissionTypes;
|
||||
|
||||
|
@ -19,8 +19,8 @@ export class ApiPermissionAssignment {
|
|||
@Column("simple-array")
|
||||
permissions: string[];
|
||||
|
||||
@Column()
|
||||
expires_at: string;
|
||||
@Column({ type: String, nullable: true })
|
||||
expires_at: string | null;
|
||||
|
||||
@ManyToOne(
|
||||
type => ApiUserInfo,
|
||||
|
|
|
@ -5,7 +5,7 @@ export class AddExpiresAtToApiPermissions1630837386329 implements MigrationInter
|
|||
await queryRunner.addColumns("api_permissions", [
|
||||
new TableColumn({
|
||||
name: "expires_at",
|
||||
type: "boolean",
|
||||
type: "datetime",
|
||||
isNullable: true,
|
||||
default: null,
|
||||
}),
|
||||
|
|
25
dashboard/package-lock.json
generated
25
dashboard/package-lock.json
generated
|
@ -9,9 +9,11 @@
|
|||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"highlight.js": "^9.15.10",
|
||||
"humanize-duration": "^3.27.0",
|
||||
"js-yaml": "^3.13.1",
|
||||
"marked": "^0.7.0",
|
||||
"modern-css-reset": "^1.0.4",
|
||||
"moment": "^2.29.1",
|
||||
"vue": "^2.6.10",
|
||||
"vue-highlightjs": "git://github.com/Dragory/vue-highlightjs.git#pass-hljs-instance",
|
||||
"vue-material-design-icons": "^4.1.0",
|
||||
|
@ -6038,6 +6040,11 @@
|
|||
"integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/humanize-duration": {
|
||||
"version": "3.27.0",
|
||||
"resolved": "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.27.0.tgz",
|
||||
"integrity": "sha512-qLo/08cNc3Tb0uD7jK0jAcU5cnqCM0n568918E7R2XhMr/+7F37p4EY062W/stg7tmzvknNn9b/1+UhVRzsYrQ=="
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.4.24",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
|
@ -7164,6 +7171,14 @@
|
|||
"resolved": "https://registry.npmjs.org/modern-css-reset/-/modern-css-reset-1.4.0.tgz",
|
||||
"integrity": "sha512-0crZmSFmrxkI7159rvQWjpDhy0u4+Awg/iOycJdlVn0RSeft/a+6BrQHR3IqvmdK25sqt0o6Z5Ap7cWgUee2rw=="
|
||||
},
|
||||
"node_modules/moment": {
|
||||
"version": "2.29.1",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
|
||||
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/move-concurrently": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
|
||||
|
@ -17836,6 +17851,11 @@
|
|||
"integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=",
|
||||
"dev": true
|
||||
},
|
||||
"humanize-duration": {
|
||||
"version": "3.27.0",
|
||||
"resolved": "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.27.0.tgz",
|
||||
"integrity": "sha512-qLo/08cNc3Tb0uD7jK0jAcU5cnqCM0n568918E7R2XhMr/+7F37p4EY062W/stg7tmzvknNn9b/1+UhVRzsYrQ=="
|
||||
},
|
||||
"iconv-lite": {
|
||||
"version": "0.4.24",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
|
@ -18685,6 +18705,11 @@
|
|||
"resolved": "https://registry.npmjs.org/modern-css-reset/-/modern-css-reset-1.4.0.tgz",
|
||||
"integrity": "sha512-0crZmSFmrxkI7159rvQWjpDhy0u4+Awg/iOycJdlVn0RSeft/a+6BrQHR3IqvmdK25sqt0o6Z5Ap7cWgUee2rw=="
|
||||
},
|
||||
"moment": {
|
||||
"version": "2.29.1",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
|
||||
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ=="
|
||||
},
|
||||
"move-concurrently": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
|
||||
|
|
|
@ -39,9 +39,11 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"highlight.js": "^9.15.10",
|
||||
"humanize-duration": "^3.27.0",
|
||||
"js-yaml": "^3.13.1",
|
||||
"marked": "^0.7.0",
|
||||
"modern-css-reset": "^1.0.4",
|
||||
"moment": "^2.29.1",
|
||||
"vue": "^2.6.10",
|
||||
"vue-highlightjs": "git://github.com/Dragory/vue-highlightjs.git#pass-hljs-instance",
|
||||
"vue-material-design-icons": "^4.1.0",
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -12,6 +12,7 @@ export const AuthStore: Module<AuthState, RootState> = {
|
|||
apiKey: null,
|
||||
loadedInitialAuth: false,
|
||||
authRefreshInterval: null,
|
||||
userId: null,
|
||||
},
|
||||
|
||||
actions: {
|
||||
|
@ -23,7 +24,7 @@ export const AuthStore: Module<AuthState, RootState> = {
|
|||
try {
|
||||
const result = await post("auth/validate-key", { key: storedKey });
|
||||
if (result.valid) {
|
||||
await dispatch("setApiKey", storedKey);
|
||||
await dispatch("setApiKey", { key: storedKey, userId: result.userId });
|
||||
return;
|
||||
}
|
||||
} catch {} // tslint:disable-line
|
||||
|
@ -35,9 +36,9 @@ export const AuthStore: Module<AuthState, RootState> = {
|
|||
commit("markInitialAuthLoaded");
|
||||
},
|
||||
|
||||
setApiKey({ commit, state, dispatch }, newKey: string) {
|
||||
localStorage.setItem("apiKey", newKey);
|
||||
commit("setApiKey", newKey);
|
||||
setApiKey({ commit, state, dispatch }, { key, userId }) {
|
||||
localStorage.setItem("apiKey", key);
|
||||
commit("setApiKey", { key, userId });
|
||||
|
||||
dispatch("startAuthAutoRefresh");
|
||||
},
|
||||
|
@ -64,7 +65,7 @@ export const AuthStore: Module<AuthState, RootState> = {
|
|||
await dispatch("endAuthAutoRefresh");
|
||||
|
||||
localStorage.removeItem("apiKey");
|
||||
commit("setApiKey", null);
|
||||
commit("setApiKey", { key: null, userId: null });
|
||||
},
|
||||
|
||||
async logout({ dispatch }) {
|
||||
|
@ -79,8 +80,9 @@ export const AuthStore: Module<AuthState, RootState> = {
|
|||
},
|
||||
|
||||
mutations: {
|
||||
setApiKey(state: AuthState, key) {
|
||||
setApiKey(state: AuthState, { key, userId }) {
|
||||
state.apiKey = key;
|
||||
state.userId = userId;
|
||||
},
|
||||
|
||||
setAuthRefreshInterval(state: AuthState, interval: IntervalType | null) {
|
||||
|
|
|
@ -11,7 +11,6 @@ export const GuildStore: Module<GuildState, RootState> = {
|
|||
availableGuildsLoadStatus: LoadStatus.None,
|
||||
available: new Map(),
|
||||
configs: {},
|
||||
myPermissions: {},
|
||||
guildPermissionAssignments: {},
|
||||
},
|
||||
|
||||
|
@ -48,9 +47,14 @@ export const GuildStore: Module<GuildState, RootState> = {
|
|||
await post(`guilds/${guildId}/config`, { config });
|
||||
},
|
||||
|
||||
async checkPermission({ commit }, { guildId, permission }) {
|
||||
const result = await post(`guilds/${guildId}/check-permission`, { permission });
|
||||
commit("setMyPermission", { guildId, permission, value: result.result });
|
||||
async loadMyPermissionAssignments({ commit }) {
|
||||
const myPermissionAssignments = await get(`guilds/my-permissions`);
|
||||
for (const permissionAssignment of myPermissionAssignments) {
|
||||
commit("setGuildPermissionAssignments", {
|
||||
guildId: permissionAssignment.guild_id,
|
||||
permissionAssignments: [permissionAssignment],
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
async loadGuildPermissionAssignments({ commit }, guildId) {
|
||||
|
@ -58,8 +62,9 @@ export const GuildStore: Module<GuildState, RootState> = {
|
|||
commit("setGuildPermissionAssignments", { guildId, permissionAssignments });
|
||||
},
|
||||
|
||||
async setTargetPermissions({ commit }, { guildId, targetId, type, permissions }) {
|
||||
commit("setTargetPermissions", { guildId, targetId, type, permissions });
|
||||
async setTargetPermissions({ commit }, { guildId, targetId, type, permissions, expiresAt }) {
|
||||
await post(`guilds/${guildId}/set-target-permissions`, { guildId, targetId, type, permissions, expiresAt });
|
||||
commit("setTargetPermissions", { guildId, targetId, type, permissions, expiresAt });
|
||||
},
|
||||
},
|
||||
|
||||
|
@ -77,12 +82,11 @@ export const GuildStore: Module<GuildState, RootState> = {
|
|||
Vue.set(state.configs, guildId, config);
|
||||
},
|
||||
|
||||
setMyPermission(state: GuildState, { guildId, permission, value }) {
|
||||
Vue.set(state.myPermissions, guildId, state.myPermissions[guildId] || {});
|
||||
Vue.set(state.myPermissions[guildId], permission, value);
|
||||
},
|
||||
|
||||
setGuildPermissionAssignments(state: GuildState, { guildId, permissionAssignments }) {
|
||||
if (!state.guildPermissionAssignments) {
|
||||
Vue.set(state, "guildPermissionAssignments", {});
|
||||
}
|
||||
|
||||
Vue.set(
|
||||
state.guildPermissionAssignments,
|
||||
guildId,
|
||||
|
@ -93,12 +97,29 @@ export const GuildStore: Module<GuildState, RootState> = {
|
|||
);
|
||||
},
|
||||
|
||||
setTargetPermissions(state: GuildState, { guildId, targetId, type, permissions }) {
|
||||
setTargetPermissions(state: GuildState, { guildId, targetId, type, permissions, expiresAt }) {
|
||||
const guildPermissionAssignments = state.guildPermissionAssignments[guildId] || [];
|
||||
const itemToEdit = guildPermissionAssignments.find(p => p.target_id === targetId && p.type === type);
|
||||
if (!itemToEdit) return;
|
||||
if (permissions.length === 0) {
|
||||
// No permissions -> remove permission assignment
|
||||
guildPermissionAssignments.splice(
|
||||
guildPermissionAssignments.findIndex(p => p.target_id === targetId && p.type === type),
|
||||
1,
|
||||
);
|
||||
} else {
|
||||
// Update/add permission assignment
|
||||
const itemToEdit = guildPermissionAssignments.find(p => p.target_id === targetId && p.type === type);
|
||||
if (itemToEdit) {
|
||||
itemToEdit.permissions = new Set(permissions);
|
||||
} else {
|
||||
state.guildPermissionAssignments[guildId].push({
|
||||
type,
|
||||
target_id: targetId,
|
||||
permissions: new Set(permissions),
|
||||
expires_at: expiresAt,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
itemToEdit.permissions = permissions;
|
||||
state.guildPermissionAssignments = { ...state.guildPermissionAssignments };
|
||||
},
|
||||
},
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { ApiPermissions } from "@shared/apiPermissions";
|
||||
import { ApiPermissionTypes } from "../../../backend/src/data/ApiPermissionAssignments";
|
||||
|
||||
export enum LoadStatus {
|
||||
None = 1,
|
||||
|
@ -14,6 +13,14 @@ export interface AuthState {
|
|||
apiKey: string | null;
|
||||
loadedInitialAuth: boolean;
|
||||
authRefreshInterval: IntervalType | null;
|
||||
userId: string | null;
|
||||
}
|
||||
|
||||
export interface GuildPermissionAssignment {
|
||||
type: string;
|
||||
target_id: string;
|
||||
permissions: Set<ApiPermissions>;
|
||||
expires_at: string | null;
|
||||
}
|
||||
|
||||
export interface GuildState {
|
||||
|
@ -29,17 +36,8 @@ export interface GuildState {
|
|||
configs: {
|
||||
[key: string]: string;
|
||||
};
|
||||
myPermissions: {
|
||||
[guildId: string]: {
|
||||
[K in ApiPermissions]?: boolean;
|
||||
};
|
||||
};
|
||||
guildPermissionAssignments: {
|
||||
[guildId: string]: Array<{
|
||||
target_id: string;
|
||||
type: ApiPermissionTypes;
|
||||
permissions: Set<ApiPermissions>;
|
||||
}>;
|
||||
[guildId: string]: GuildPermissionAssignment[];
|
||||
};
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue