mirror of
https://github.com/ZeppelinBot/Zeppelin.git
synced 2025-03-15 05:41:51 +00:00
dashboard/api: add support for Zeppelin staff members; add ViewGuild permission; code cleanup
This commit is contained in:
parent
7e60950900
commit
d03d729438
13 changed files with 175 additions and 75 deletions
|
@ -7,3 +7,4 @@ DB_HOST=
|
|||
DB_USER=
|
||||
DB_PASSWORD=
|
||||
DB_DATABASE=
|
||||
STAFF=
|
||||
|
|
|
@ -12,7 +12,7 @@ import { ok } from "./responses";
|
|||
|
||||
interface IPassportApiUser {
|
||||
apiKey: string;
|
||||
userId: number;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
|
|
@ -5,35 +5,46 @@ import { Configs } from "../data/Configs";
|
|||
import { validateGuildConfig } from "../configValidator";
|
||||
import yaml, { YAMLException } from "js-yaml";
|
||||
import { apiTokenAuthHandlers } from "./auth";
|
||||
import { ApiPermissions, hasPermission, permissionArrToSet } from "@shared/apiPermissions";
|
||||
import { ApiPermissionAssignments } from "../data/ApiPermissionAssignments";
|
||||
import { ApiPermissions } from "@shared/apiPermissions";
|
||||
import { hasGuildPermission, requireGuildPermission } from "./permissions";
|
||||
|
||||
export function initGuildsAPI(app: express.Express) {
|
||||
const allowedGuilds = new AllowedGuilds();
|
||||
const apiPermissionAssignments = new ApiPermissionAssignments();
|
||||
const configs = new Configs();
|
||||
|
||||
app.get("/guilds/available", ...apiTokenAuthHandlers(), async (req: Request, res: Response) => {
|
||||
const guildRouter = express.Router();
|
||||
guildRouter.use(...apiTokenAuthHandlers());
|
||||
|
||||
guildRouter.get("/available", async (req: Request, res: Response) => {
|
||||
const guilds = await allowedGuilds.getForApiUser(req.user.userId);
|
||||
res.json(guilds);
|
||||
});
|
||||
|
||||
app.get("/guilds/:guildId/config", ...apiTokenAuthHandlers(), async (req: Request, res: Response) => {
|
||||
const permAssignment = await apiPermissionAssignments.getByGuildAndUserId(req.params.guildId, req.user.userId);
|
||||
if (!permAssignment || !hasPermission(permissionArrToSet(permAssignment.permissions), ApiPermissions.ReadConfig)) {
|
||||
guildRouter.get("/:guildId", async (req: Request, res: Response) => {
|
||||
if (!(await hasGuildPermission(req.user.userId, req.params.guildId, ApiPermissions.ViewGuild))) {
|
||||
return unauthorized(res);
|
||||
}
|
||||
|
||||
const config = await configs.getActiveByKey(`guild-${req.params.guildId}`);
|
||||
res.json({ config: config ? config.config : "" });
|
||||
const guild = await allowedGuilds.find(req.params.guildId);
|
||||
res.json(guild);
|
||||
});
|
||||
|
||||
app.post("/guilds/:guildId/config", ...apiTokenAuthHandlers(), async (req, res) => {
|
||||
const permAssignment = await apiPermissionAssignments.getByGuildAndUserId(req.params.guildId, req.user.userId);
|
||||
if (!permAssignment || !hasPermission(permissionArrToSet(permAssignment.permissions), ApiPermissions.EditConfig)) {
|
||||
return unauthorized(res);
|
||||
}
|
||||
guildRouter.post("/:guildId/check-permission", async (req: Request, res: Response) => {
|
||||
const permission = req.body.permission;
|
||||
const hasPermission = await hasGuildPermission(req.user.userId, req.params.guildId, permission);
|
||||
res.json({ result: hasPermission });
|
||||
});
|
||||
|
||||
guildRouter.get(
|
||||
"/:guildId/config",
|
||||
requireGuildPermission(ApiPermissions.ReadConfig),
|
||||
async (req: Request, res: Response) => {
|
||||
const config = await configs.getActiveByKey(`guild-${req.params.guildId}`);
|
||||
res.json({ config: config ? config.config : "" });
|
||||
},
|
||||
);
|
||||
|
||||
guildRouter.post("/:guildId/config", requireGuildPermission(ApiPermissions.EditConfig), async (req, res) => {
|
||||
let config = req.body.config;
|
||||
if (config == null) return clientError(res, "No config supplied");
|
||||
|
||||
|
@ -70,4 +81,6 @@ export function initGuildsAPI(app: express.Express) {
|
|||
await configs.saveNewRevision(`guild-${req.params.guildId}`, config, req.user.userId);
|
||||
ok(res);
|
||||
});
|
||||
|
||||
app.use("/guilds", guildRouter);
|
||||
}
|
||||
|
|
|
@ -1,14 +1,5 @@
|
|||
import { clientError, error, notFound } from "./responses";
|
||||
import express from "express";
|
||||
import cors from "cors";
|
||||
import { initAuth } from "./auth";
|
||||
import { initGuildsAPI } from "./guilds";
|
||||
import { initArchives } from "./archives";
|
||||
import { initDocs } from "./docs";
|
||||
import { connect } from "../data/db";
|
||||
import path from "path";
|
||||
import { TokenError } from "passport-oauth2";
|
||||
import { PluginError } from "knub";
|
||||
|
||||
require("dotenv").config({ path: path.resolve(process.cwd(), "api.env") });
|
||||
|
||||
|
@ -19,43 +10,8 @@ function errorHandler(err) {
|
|||
|
||||
process.on("unhandledRejection", errorHandler);
|
||||
|
||||
// Connect to the database before loading the rest of the code (that depend on the database connection)
|
||||
console.log("Connecting to database..."); // tslint:disable-line
|
||||
connect().then(() => {
|
||||
const app = express();
|
||||
|
||||
app.use(
|
||||
cors({
|
||||
origin: process.env.DASHBOARD_URL,
|
||||
}),
|
||||
);
|
||||
app.use(express.json());
|
||||
|
||||
initAuth(app);
|
||||
initGuildsAPI(app);
|
||||
initArchives(app);
|
||||
initDocs(app);
|
||||
|
||||
// Default route
|
||||
app.get("/", (req, res) => {
|
||||
res.json({ status: "cookies", with: "milk" });
|
||||
});
|
||||
|
||||
// Error response
|
||||
app.use((err, req, res, next) => {
|
||||
if (err instanceof TokenError) {
|
||||
clientError(res, "Invalid code");
|
||||
} else {
|
||||
console.error(err); // tslint:disable-line
|
||||
error(res, "Server error", err.status || 500);
|
||||
}
|
||||
});
|
||||
|
||||
// 404 response
|
||||
app.use((req, res, next) => {
|
||||
return notFound(res);
|
||||
});
|
||||
|
||||
const port = process.env.PORT || 3000;
|
||||
// tslint:disable-next-line
|
||||
app.listen(port, () => console.log(`API server listening on port ${port}`));
|
||||
import("./start");
|
||||
});
|
||||
|
|
33
backend/src/api/permissions.ts
Normal file
33
backend/src/api/permissions.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
import { ApiPermissions, hasPermission, permissionArrToSet } from "@shared/apiPermissions";
|
||||
import { isStaff } from "../staff";
|
||||
import { ApiPermissionAssignments } from "../data/ApiPermissionAssignments";
|
||||
import { Request, Response } from "express";
|
||||
import { unauthorized } from "./responses";
|
||||
|
||||
const apiPermissionAssignments = new ApiPermissionAssignments();
|
||||
|
||||
export const hasGuildPermission = async (userId: string, guildId: string, permission: ApiPermissions) => {
|
||||
if (isStaff(userId)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const permAssignment = await apiPermissionAssignments.getByGuildAndUserId(guildId, userId);
|
||||
if (!permAssignment) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return hasPermission(permissionArrToSet(permAssignment.permissions), permission);
|
||||
};
|
||||
|
||||
/**
|
||||
* Requires `guildId` in req.params
|
||||
*/
|
||||
export function requireGuildPermission(permission: ApiPermissions) {
|
||||
return async (req: Request, res: Response, next) => {
|
||||
if (!(await hasGuildPermission(req.user.userId, req.params.guildId, permission))) {
|
||||
return unauthorized(res);
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
48
backend/src/api/start.ts
Normal file
48
backend/src/api/start.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
import { clientError, error, notFound } from "./responses";
|
||||
import express from "express";
|
||||
import cors from "cors";
|
||||
import { initAuth } from "./auth";
|
||||
import { initGuildsAPI } from "./guilds";
|
||||
import { initArchives } from "./archives";
|
||||
import { initDocs } from "./docs";
|
||||
import { connect } from "../data/db";
|
||||
import path from "path";
|
||||
import { TokenError } from "passport-oauth2";
|
||||
import { PluginError } from "knub";
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(
|
||||
cors({
|
||||
origin: process.env.DASHBOARD_URL,
|
||||
}),
|
||||
);
|
||||
app.use(express.json());
|
||||
|
||||
initAuth(app);
|
||||
initGuildsAPI(app);
|
||||
initArchives(app);
|
||||
initDocs(app);
|
||||
|
||||
// Default route
|
||||
app.get("/", (req, res) => {
|
||||
res.json({ status: "cookies", with: "milk" });
|
||||
});
|
||||
|
||||
// Error response
|
||||
app.use((err, req, res, next) => {
|
||||
if (err instanceof TokenError) {
|
||||
clientError(res, "Invalid code");
|
||||
} else {
|
||||
console.error(err); // tslint:disable-line
|
||||
error(res, "Server error", err.status || 500);
|
||||
}
|
||||
});
|
||||
|
||||
// 404 response
|
||||
app.use((req, res, next) => {
|
||||
return notFound(res);
|
||||
});
|
||||
|
||||
const port = process.env.PORT || 3000;
|
||||
app.listen(port, () => console.log(`API server listening on port ${port}`));
|
|
@ -28,6 +28,10 @@ export class AllowedGuilds extends BaseRepository {
|
|||
return count !== 0;
|
||||
}
|
||||
|
||||
find(guildId) {
|
||||
return this.allowedGuilds.findOne(guildId);
|
||||
}
|
||||
|
||||
getForApiUser(userId) {
|
||||
return this.allowedGuilds
|
||||
.createQueryBuilder("allowed_guilds")
|
||||
|
|
6
backend/src/staff.ts
Normal file
6
backend/src/staff.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
/**
|
||||
* Zeppelin staff have full access to the dashboard
|
||||
*/
|
||||
export function isStaff(userId: string) {
|
||||
return (process.env.STAFF ?? "").split(",").includes(userId);
|
||||
}
|
|
@ -41,7 +41,17 @@
|
|||
AceEditor,
|
||||
},
|
||||
async mounted() {
|
||||
await this.$store.dispatch("guilds/loadAvailableGuilds");
|
||||
try {
|
||||
await this.$store.dispatch("guilds/loadGuild", this.$route.params.guildId);
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError) {
|
||||
this.$router.push('/dashboard');
|
||||
return;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (this.guild == null) {
|
||||
this.$router.push('/dashboard');
|
||||
return;
|
||||
|
@ -66,7 +76,7 @@
|
|||
computed: {
|
||||
...mapState('guilds', {
|
||||
guild() {
|
||||
return this.$store.state.guilds.available.find(g => g.id === this.$route.params.guildId);
|
||||
return this.$store.state.guilds.available.get(this.$route.params.guildId);
|
||||
},
|
||||
config() {
|
||||
return this.$store.state.guilds.configs[this.$route.params.guildId];
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
computed: {
|
||||
...mapState('guilds', {
|
||||
guilds: state => {
|
||||
const guilds = Array.from(state.available || []);
|
||||
const guilds = Array.from(state.available.values());
|
||||
guilds.sort((a, b) => {
|
||||
if (a.name > b.name) return 1;
|
||||
if (a.name < b.name) return -1;
|
||||
|
|
|
@ -7,7 +7,7 @@ export const GuildStore: Module<GuildState, RootState> = {
|
|||
|
||||
state: {
|
||||
availableGuildsLoadStatus: LoadStatus.None,
|
||||
available: [],
|
||||
available: new Map(),
|
||||
configs: {},
|
||||
},
|
||||
|
||||
|
@ -17,7 +17,22 @@ export const GuildStore: Module<GuildState, RootState> = {
|
|||
commit("setAvailableGuildsLoadStatus", LoadStatus.Loading);
|
||||
|
||||
const availableGuilds = await get("guilds/available");
|
||||
commit("setAvailableGuilds", availableGuilds);
|
||||
for (const guild of availableGuilds) {
|
||||
commit("addGuild", guild);
|
||||
}
|
||||
|
||||
commit("setAvailableGuildsLoadStatus", LoadStatus.Done);
|
||||
},
|
||||
|
||||
async loadGuild({ commit, state }, guildId) {
|
||||
if (state.available.has(guildId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const guild = await get(`guilds/${guildId}`);
|
||||
if (guild) {
|
||||
commit("addGuild", guild);
|
||||
}
|
||||
},
|
||||
|
||||
async loadConfig({ commit }, guildId) {
|
||||
|
@ -35,9 +50,8 @@ export const GuildStore: Module<GuildState, RootState> = {
|
|||
state.availableGuildsLoadStatus = status;
|
||||
},
|
||||
|
||||
setAvailableGuilds(state: GuildState, guilds) {
|
||||
state.available = guilds;
|
||||
state.availableGuildsLoadStatus = LoadStatus.Done;
|
||||
addGuild(state: GuildState, guild) {
|
||||
state.available.set(guild.id, guild);
|
||||
},
|
||||
|
||||
setConfig(state: GuildState, { guildId, config }) {
|
||||
|
|
|
@ -11,11 +11,14 @@ export interface AuthState {
|
|||
|
||||
export interface GuildState {
|
||||
availableGuildsLoadStatus: LoadStatus;
|
||||
available: Array<{
|
||||
guild_id: string;
|
||||
available: Map<
|
||||
string,
|
||||
{
|
||||
id: string;
|
||||
name: string;
|
||||
icon: string | null;
|
||||
}>;
|
||||
}
|
||||
>;
|
||||
configs: {
|
||||
[key: string]: string;
|
||||
};
|
||||
|
|
|
@ -3,6 +3,7 @@ export enum ApiPermissions {
|
|||
ManageAccess = "MANAGE_ACCESS",
|
||||
EditConfig = "EDIT_CONFIG",
|
||||
ReadConfig = "READ_CONFIG",
|
||||
ViewGuild = "VIEW_GUILD",
|
||||
}
|
||||
|
||||
const reverseApiPermissions = Object.entries(ApiPermissions).reduce((map, [key, value]) => {
|
||||
|
@ -15,13 +16,24 @@ export const permissionNames = {
|
|||
[ApiPermissions.ManageAccess]: "Manage dashboard access",
|
||||
[ApiPermissions.EditConfig]: "Edit config",
|
||||
[ApiPermissions.ReadConfig]: "Read config",
|
||||
[ApiPermissions.ViewGuild]: "View server",
|
||||
};
|
||||
|
||||
export type TPermissionHierarchy = Array<ApiPermissions | [ApiPermissions, TPermissionHierarchy]>;
|
||||
|
||||
// prettier-ignore-start
|
||||
export const permissionHierarchy: TPermissionHierarchy = [
|
||||
[ApiPermissions.Owner, [[ApiPermissions.ManageAccess, [[ApiPermissions.EditConfig, [ApiPermissions.ReadConfig]]]]]],
|
||||
[
|
||||
ApiPermissions.Owner,
|
||||
[
|
||||
[
|
||||
ApiPermissions.ManageAccess,
|
||||
[[ApiPermissions.EditConfig, [[ApiPermissions.ReadConfig, [ApiPermissions.ViewGuild]]]]],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
// prettier-ignore-end
|
||||
|
||||
export function permissionArrToSet(permissions: string[]): Set<ApiPermissions> {
|
||||
return new Set(permissions.filter(p => reverseApiPermissions[p])) as Set<ApiPermissions>;
|
||||
|
|
Loading…
Add table
Reference in a new issue