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 });
|
return res.json({ valid: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({ valid: true });
|
res.json({ valid: true, userId });
|
||||||
});
|
});
|
||||||
app.post("/auth/logout", ...apiTokenAuthHandlers(), async (req: Request, res: Response) => {
|
app.post("/auth/logout", ...apiTokenAuthHandlers(), async (req: Request, res: Response) => {
|
||||||
await apiLogins.expireApiKey(req.user!.apiKey);
|
await apiLogins.expireApiKey(req.user!.apiKey);
|
||||||
|
|
|
@ -3,15 +3,21 @@ import express, { Request, Response } from "express";
|
||||||
import { YAMLException } from "js-yaml";
|
import { YAMLException } from "js-yaml";
|
||||||
import { validateGuildConfig } from "../configValidator";
|
import { validateGuildConfig } from "../configValidator";
|
||||||
import { AllowedGuilds } from "../data/AllowedGuilds";
|
import { AllowedGuilds } from "../data/AllowedGuilds";
|
||||||
import { ApiPermissionAssignments } from "../data/ApiPermissionAssignments";
|
import { ApiPermissionAssignments, ApiPermissionTypes } from "../data/ApiPermissionAssignments";
|
||||||
import { Configs } from "../data/Configs";
|
import { Configs } from "../data/Configs";
|
||||||
import { apiTokenAuthHandlers } from "./auth";
|
import { apiTokenAuthHandlers } from "./auth";
|
||||||
import { hasGuildPermission, requireGuildPermission } from "./permissions";
|
import { hasGuildPermission, requireGuildPermission } from "./permissions";
|
||||||
import { clientError, ok, serverError, unauthorized } from "./responses";
|
import { clientError, ok, serverError, unauthorized } from "./responses";
|
||||||
import { loadYamlSafely } from "../utils/loadYamlSafely";
|
import { loadYamlSafely } from "../utils/loadYamlSafely";
|
||||||
import { ObjectAliasError } from "../utils/validateNoObjectAliases";
|
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 apiPermissionAssignments = new ApiPermissionAssignments();
|
||||||
|
const auditLog = new ApiAuditLog();
|
||||||
|
|
||||||
export function initGuildsAPI(app: express.Express) {
|
export function initGuildsAPI(app: express.Express) {
|
||||||
const allowedGuilds = new AllowedGuilds();
|
const allowedGuilds = new AllowedGuilds();
|
||||||
|
@ -25,6 +31,14 @@ export function initGuildsAPI(app: express.Express) {
|
||||||
res.json(guilds);
|
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) => {
|
guildRouter.get("/:guildId", async (req: Request, res: Response) => {
|
||||||
if (!(await hasGuildPermission(req.user!.userId, req.params.guildId, ApiPermissions.ViewGuild))) {
|
if (!(await hasGuildPermission(req.user!.userId, req.params.guildId, ApiPermissions.ViewGuild))) {
|
||||||
return unauthorized(res);
|
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);
|
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({
|
return this.apiPermissions.insert({
|
||||||
guild_id: guildId,
|
guild_id: guildId,
|
||||||
type: ApiPermissionTypes.User,
|
type: ApiPermissionTypes.User,
|
||||||
target_id: userId,
|
target_id: userId,
|
||||||
permissions,
|
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 });
|
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() {
|
async clearExpiredPermissions() {
|
||||||
await this.apiPermissions
|
await this.apiPermissions
|
||||||
.createQueryBuilder()
|
.createQueryBuilder()
|
||||||
|
|
|
@ -8,7 +8,7 @@ export class ApiPermissionAssignment {
|
||||||
@PrimaryColumn()
|
@PrimaryColumn()
|
||||||
guild_id: string;
|
guild_id: string;
|
||||||
|
|
||||||
@Column({ type: "string" })
|
@Column({ type: String })
|
||||||
@PrimaryColumn()
|
@PrimaryColumn()
|
||||||
type: ApiPermissionTypes;
|
type: ApiPermissionTypes;
|
||||||
|
|
||||||
|
@ -19,8 +19,8 @@ export class ApiPermissionAssignment {
|
||||||
@Column("simple-array")
|
@Column("simple-array")
|
||||||
permissions: string[];
|
permissions: string[];
|
||||||
|
|
||||||
@Column()
|
@Column({ type: String, nullable: true })
|
||||||
expires_at: string;
|
expires_at: string | null;
|
||||||
|
|
||||||
@ManyToOne(
|
@ManyToOne(
|
||||||
type => ApiUserInfo,
|
type => ApiUserInfo,
|
||||||
|
|
|
@ -5,7 +5,7 @@ export class AddExpiresAtToApiPermissions1630837386329 implements MigrationInter
|
||||||
await queryRunner.addColumns("api_permissions", [
|
await queryRunner.addColumns("api_permissions", [
|
||||||
new TableColumn({
|
new TableColumn({
|
||||||
name: "expires_at",
|
name: "expires_at",
|
||||||
type: "boolean",
|
type: "datetime",
|
||||||
isNullable: true,
|
isNullable: true,
|
||||||
default: null,
|
default: null,
|
||||||
}),
|
}),
|
||||||
|
|
25
dashboard/package-lock.json
generated
25
dashboard/package-lock.json
generated
|
@ -9,9 +9,11 @@
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"highlight.js": "^9.15.10",
|
"highlight.js": "^9.15.10",
|
||||||
|
"humanize-duration": "^3.27.0",
|
||||||
"js-yaml": "^3.13.1",
|
"js-yaml": "^3.13.1",
|
||||||
"marked": "^0.7.0",
|
"marked": "^0.7.0",
|
||||||
"modern-css-reset": "^1.0.4",
|
"modern-css-reset": "^1.0.4",
|
||||||
|
"moment": "^2.29.1",
|
||||||
"vue": "^2.6.10",
|
"vue": "^2.6.10",
|
||||||
"vue-highlightjs": "git://github.com/Dragory/vue-highlightjs.git#pass-hljs-instance",
|
"vue-highlightjs": "git://github.com/Dragory/vue-highlightjs.git#pass-hljs-instance",
|
||||||
"vue-material-design-icons": "^4.1.0",
|
"vue-material-design-icons": "^4.1.0",
|
||||||
|
@ -6038,6 +6040,11 @@
|
||||||
"integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=",
|
"integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=",
|
||||||
"dev": true
|
"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": {
|
"node_modules/iconv-lite": {
|
||||||
"version": "0.4.24",
|
"version": "0.4.24",
|
||||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/modern-css-reset/-/modern-css-reset-1.4.0.tgz",
|
||||||
"integrity": "sha512-0crZmSFmrxkI7159rvQWjpDhy0u4+Awg/iOycJdlVn0RSeft/a+6BrQHR3IqvmdK25sqt0o6Z5Ap7cWgUee2rw=="
|
"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": {
|
"node_modules/move-concurrently": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
|
||||||
|
@ -17836,6 +17851,11 @@
|
||||||
"integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=",
|
"integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=",
|
||||||
"dev": true
|
"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": {
|
"iconv-lite": {
|
||||||
"version": "0.4.24",
|
"version": "0.4.24",
|
||||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/modern-css-reset/-/modern-css-reset-1.4.0.tgz",
|
||||||
"integrity": "sha512-0crZmSFmrxkI7159rvQWjpDhy0u4+Awg/iOycJdlVn0RSeft/a+6BrQHR3IqvmdK25sqt0o6Z5Ap7cWgUee2rw=="
|
"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": {
|
"move-concurrently": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
|
||||||
|
|
|
@ -39,9 +39,11 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"highlight.js": "^9.15.10",
|
"highlight.js": "^9.15.10",
|
||||||
|
"humanize-duration": "^3.27.0",
|
||||||
"js-yaml": "^3.13.1",
|
"js-yaml": "^3.13.1",
|
||||||
"marked": "^0.7.0",
|
"marked": "^0.7.0",
|
||||||
"modern-css-reset": "^1.0.4",
|
"modern-css-reset": "^1.0.4",
|
||||||
|
"moment": "^2.29.1",
|
||||||
"vue": "^2.6.10",
|
"vue": "^2.6.10",
|
||||||
"vue-highlightjs": "git://github.com/Dragory/vue-highlightjs.git#pass-hljs-instance",
|
"vue-highlightjs": "git://github.com/Dragory/vue-highlightjs.git#pass-hljs-instance",
|
||||||
"vue-material-design-icons": "^4.1.0",
|
"vue-material-design-icons": "^4.1.0",
|
||||||
|
|
|
@ -1,70 +1,182 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<h1>Guild Access</h1>
|
<h1>Dashboard access</h1>
|
||||||
<p>
|
<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>
|
</p>
|
||||||
<div v-for="permAssignment in permissionAssignments">
|
|
||||||
<strong>{{ permAssignment.type }} {{ permAssignment.target_id }}</strong>
|
<h2 class="mt-8">Roles</h2>
|
||||||
<permission-tree :tree="permAssignment._permissionTree" :granted-permissions="permAssignment.permissions" :on-change="onTreeUpdate.bind(null, permAssignment)" />
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ApiPermissions, permissionHierarchy } from "@shared/apiPermissions";
|
import { ApiPermissions, hasPermission } from "@shared/apiPermissions";
|
||||||
import PermissionTree from "./PermissionTree.vue";
|
import PermissionTree from "./PermissionTree.vue";
|
||||||
import { applyStateToPermissionHierarchy } from "./permissionTreeUtils";
|
import { mapState } from "vuex";
|
||||||
import { mapState } from "vuex";
|
import {
|
||||||
import { GuildState } from "../../store/types";
|
GuildPermissionAssignment,
|
||||||
|
GuildState,
|
||||||
|
RootState
|
||||||
|
} from "../../store/types";
|
||||||
|
import humanizeDuration from "humanize-duration";
|
||||||
|
import moment from "moment";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {PermissionTree},
|
components: {PermissionTree},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
managerPermissions: new Set([ApiPermissions.ManageAccess]),
|
managerPermissions: new Set([ApiPermissions.ManageAccess]),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
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", {
|
...mapState<GuildState>("guilds", {
|
||||||
canManage(guilds) {
|
permanentPermissionAssignments(guilds: GuildState): GuildPermissionAssignment[] {
|
||||||
return guilds.myPermissions[this.$route.params.guildId]?.[ApiPermissions.ManageAccess];
|
return (guilds.guildPermissionAssignments[this.$route.params.guildId] || []).filter(perm => perm.expires_at == null);
|
||||||
},
|
},
|
||||||
|
|
||||||
permissionAssignments(guilds) {
|
temporaryPermissionAssignments(guilds: GuildState): GuildPermissionAssignment[] {
|
||||||
return (guilds.guildPermissionAssignments[this.$route.params.guildId] || []).map(permAssignment => {
|
return (guilds.guildPermissionAssignments[this.$route.params.guildId] || []).filter(perm => perm.expires_at != null);
|
||||||
return {
|
|
||||||
...permAssignment,
|
|
||||||
_permissionTree: applyStateToPermissionHierarchy(permissionHierarchy, permAssignment.permissions, this.managerPermissions),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
// beforeMount() {
|
|
||||||
// this.tree = applyStateToPermissionHierarchy(permissionHierarchy, this.grantedPermissions, this.managerPermissions);
|
|
||||||
// },
|
|
||||||
async mounted() {
|
async mounted() {
|
||||||
await this.$store.dispatch("guilds/checkPermission", {
|
await this.$store.dispatch("guilds/loadGuildPermissionAssignments", this.$route.params.guildId).catch(() => {});
|
||||||
guildId: this.$route.params.guildId,
|
|
||||||
permission: ApiPermissions.ManageAccess,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (! this.canManage) {
|
if (! this.canManage) {
|
||||||
this.$router.push('/dashboard');
|
this.$router.push('/dashboard');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.$store.dispatch("guilds/loadGuildPermissionAssignments", this.$route.params.guildId);
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
// updateTreeState() {
|
isOwner(perm: GuildPermissionAssignment) {
|
||||||
// this.tree = applyStateToPermissionHierarchy(permissionHierarchy, this.grantedPermissions, this.managerPermissions);
|
return perm.permissions.has(ApiPermissions.Owner);
|
||||||
// },
|
},
|
||||||
//
|
|
||||||
// onChange() {
|
hasPermission(perm: GuildPermissionAssignment, permissionName: ApiPermissions) {
|
||||||
// this.updateTreeState();
|
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) {
|
onTreeUpdate(targetPermissions) {
|
||||||
this.$store.dispatch("guilds/setTargetPermissions", {
|
this.$store.dispatch("guilds/setTargetPermissions", {
|
||||||
|
@ -73,7 +185,58 @@
|
||||||
type: targetPermissions.type,
|
type: targetPermissions.type,
|
||||||
permissions: targetPermissions.permissions,
|
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>
|
</script>
|
||||||
|
|
|
@ -17,9 +17,8 @@
|
||||||
<div class="text-gray-600 text-sm leading-tight">{{ guild.id }}</div>
|
<div class="text-gray-600 text-sm leading-tight">{{ guild.id }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pt-1">
|
<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>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -28,12 +27,15 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script lang="ts">
|
||||||
import {mapState} from "vuex";
|
import { mapState } from "vuex";
|
||||||
|
import { ApiPermissions, hasPermission } from "@shared/apiPermissions";
|
||||||
|
import { AuthState, GuildState } from "../../store/types";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
async mounted() {
|
async mounted() {
|
||||||
await this.$store.dispatch("guilds/loadAvailableGuilds");
|
await this.$store.dispatch("guilds/loadAvailableGuilds");
|
||||||
|
await this.$store.dispatch("guilds/loadMyPermissionAssignments");
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
@ -41,7 +43,7 @@
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState('guilds', {
|
...mapState('guilds', {
|
||||||
guilds: state => {
|
guilds: (state: GuildState) => {
|
||||||
const guilds = Array.from(state.available.values());
|
const guilds = Array.from(state.available.values());
|
||||||
guilds.sort((a, b) => {
|
guilds.sort((a, b) => {
|
||||||
if (a.name > b.name) return 1;
|
if (a.name > b.name) return 1;
|
||||||
|
@ -52,7 +54,20 @@
|
||||||
});
|
});
|
||||||
return guilds;
|
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>
|
</script>
|
||||||
|
|
|
@ -12,6 +12,7 @@ export const AuthStore: Module<AuthState, RootState> = {
|
||||||
apiKey: null,
|
apiKey: null,
|
||||||
loadedInitialAuth: false,
|
loadedInitialAuth: false,
|
||||||
authRefreshInterval: null,
|
authRefreshInterval: null,
|
||||||
|
userId: null,
|
||||||
},
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
|
@ -23,7 +24,7 @@ export const AuthStore: Module<AuthState, RootState> = {
|
||||||
try {
|
try {
|
||||||
const result = await post("auth/validate-key", { key: storedKey });
|
const result = await post("auth/validate-key", { key: storedKey });
|
||||||
if (result.valid) {
|
if (result.valid) {
|
||||||
await dispatch("setApiKey", storedKey);
|
await dispatch("setApiKey", { key: storedKey, userId: result.userId });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch {} // tslint:disable-line
|
} catch {} // tslint:disable-line
|
||||||
|
@ -35,9 +36,9 @@ export const AuthStore: Module<AuthState, RootState> = {
|
||||||
commit("markInitialAuthLoaded");
|
commit("markInitialAuthLoaded");
|
||||||
},
|
},
|
||||||
|
|
||||||
setApiKey({ commit, state, dispatch }, newKey: string) {
|
setApiKey({ commit, state, dispatch }, { key, userId }) {
|
||||||
localStorage.setItem("apiKey", newKey);
|
localStorage.setItem("apiKey", key);
|
||||||
commit("setApiKey", newKey);
|
commit("setApiKey", { key, userId });
|
||||||
|
|
||||||
dispatch("startAuthAutoRefresh");
|
dispatch("startAuthAutoRefresh");
|
||||||
},
|
},
|
||||||
|
@ -64,7 +65,7 @@ export const AuthStore: Module<AuthState, RootState> = {
|
||||||
await dispatch("endAuthAutoRefresh");
|
await dispatch("endAuthAutoRefresh");
|
||||||
|
|
||||||
localStorage.removeItem("apiKey");
|
localStorage.removeItem("apiKey");
|
||||||
commit("setApiKey", null);
|
commit("setApiKey", { key: null, userId: null });
|
||||||
},
|
},
|
||||||
|
|
||||||
async logout({ dispatch }) {
|
async logout({ dispatch }) {
|
||||||
|
@ -79,8 +80,9 @@ export const AuthStore: Module<AuthState, RootState> = {
|
||||||
},
|
},
|
||||||
|
|
||||||
mutations: {
|
mutations: {
|
||||||
setApiKey(state: AuthState, key) {
|
setApiKey(state: AuthState, { key, userId }) {
|
||||||
state.apiKey = key;
|
state.apiKey = key;
|
||||||
|
state.userId = userId;
|
||||||
},
|
},
|
||||||
|
|
||||||
setAuthRefreshInterval(state: AuthState, interval: IntervalType | null) {
|
setAuthRefreshInterval(state: AuthState, interval: IntervalType | null) {
|
||||||
|
|
|
@ -11,7 +11,6 @@ export const GuildStore: Module<GuildState, RootState> = {
|
||||||
availableGuildsLoadStatus: LoadStatus.None,
|
availableGuildsLoadStatus: LoadStatus.None,
|
||||||
available: new Map(),
|
available: new Map(),
|
||||||
configs: {},
|
configs: {},
|
||||||
myPermissions: {},
|
|
||||||
guildPermissionAssignments: {},
|
guildPermissionAssignments: {},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -48,9 +47,14 @@ export const GuildStore: Module<GuildState, RootState> = {
|
||||||
await post(`guilds/${guildId}/config`, { config });
|
await post(`guilds/${guildId}/config`, { config });
|
||||||
},
|
},
|
||||||
|
|
||||||
async checkPermission({ commit }, { guildId, permission }) {
|
async loadMyPermissionAssignments({ commit }) {
|
||||||
const result = await post(`guilds/${guildId}/check-permission`, { permission });
|
const myPermissionAssignments = await get(`guilds/my-permissions`);
|
||||||
commit("setMyPermission", { guildId, permission, value: result.result });
|
for (const permissionAssignment of myPermissionAssignments) {
|
||||||
|
commit("setGuildPermissionAssignments", {
|
||||||
|
guildId: permissionAssignment.guild_id,
|
||||||
|
permissionAssignments: [permissionAssignment],
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async loadGuildPermissionAssignments({ commit }, guildId) {
|
async loadGuildPermissionAssignments({ commit }, guildId) {
|
||||||
|
@ -58,8 +62,9 @@ export const GuildStore: Module<GuildState, RootState> = {
|
||||||
commit("setGuildPermissionAssignments", { guildId, permissionAssignments });
|
commit("setGuildPermissionAssignments", { guildId, permissionAssignments });
|
||||||
},
|
},
|
||||||
|
|
||||||
async setTargetPermissions({ commit }, { guildId, targetId, type, permissions }) {
|
async setTargetPermissions({ commit }, { guildId, targetId, type, permissions, expiresAt }) {
|
||||||
commit("setTargetPermissions", { guildId, targetId, type, permissions });
|
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);
|
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 }) {
|
setGuildPermissionAssignments(state: GuildState, { guildId, permissionAssignments }) {
|
||||||
|
if (!state.guildPermissionAssignments) {
|
||||||
|
Vue.set(state, "guildPermissionAssignments", {});
|
||||||
|
}
|
||||||
|
|
||||||
Vue.set(
|
Vue.set(
|
||||||
state.guildPermissionAssignments,
|
state.guildPermissionAssignments,
|
||||||
guildId,
|
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 guildPermissionAssignments = state.guildPermissionAssignments[guildId] || [];
|
||||||
const itemToEdit = guildPermissionAssignments.find(p => p.target_id === targetId && p.type === type);
|
if (permissions.length === 0) {
|
||||||
if (!itemToEdit) return;
|
// 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 };
|
state.guildPermissionAssignments = { ...state.guildPermissionAssignments };
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { ApiPermissions } from "@shared/apiPermissions";
|
import { ApiPermissions } from "@shared/apiPermissions";
|
||||||
import { ApiPermissionTypes } from "../../../backend/src/data/ApiPermissionAssignments";
|
|
||||||
|
|
||||||
export enum LoadStatus {
|
export enum LoadStatus {
|
||||||
None = 1,
|
None = 1,
|
||||||
|
@ -14,6 +13,14 @@ export interface AuthState {
|
||||||
apiKey: string | null;
|
apiKey: string | null;
|
||||||
loadedInitialAuth: boolean;
|
loadedInitialAuth: boolean;
|
||||||
authRefreshInterval: IntervalType | null;
|
authRefreshInterval: IntervalType | null;
|
||||||
|
userId: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GuildPermissionAssignment {
|
||||||
|
type: string;
|
||||||
|
target_id: string;
|
||||||
|
permissions: Set<ApiPermissions>;
|
||||||
|
expires_at: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GuildState {
|
export interface GuildState {
|
||||||
|
@ -29,17 +36,8 @@ export interface GuildState {
|
||||||
configs: {
|
configs: {
|
||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
};
|
};
|
||||||
myPermissions: {
|
|
||||||
[guildId: string]: {
|
|
||||||
[K in ApiPermissions]?: boolean;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
guildPermissionAssignments: {
|
guildPermissionAssignments: {
|
||||||
[guildId: string]: Array<{
|
[guildId: string]: GuildPermissionAssignment[];
|
||||||
target_id: string;
|
|
||||||
type: ApiPermissionTypes;
|
|
||||||
permissions: Set<ApiPermissions>;
|
|
||||||
}>;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue