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_USER=
|
||||||
DB_PASSWORD=
|
DB_PASSWORD=
|
||||||
DB_DATABASE=
|
DB_DATABASE=
|
||||||
|
STAFF=
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { ok } from "./responses";
|
||||||
|
|
||||||
interface IPassportApiUser {
|
interface IPassportApiUser {
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
userId: number;
|
userId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|
|
@ -5,35 +5,46 @@ import { Configs } from "../data/Configs";
|
||||||
import { validateGuildConfig } from "../configValidator";
|
import { validateGuildConfig } from "../configValidator";
|
||||||
import yaml, { YAMLException } from "js-yaml";
|
import yaml, { YAMLException } from "js-yaml";
|
||||||
import { apiTokenAuthHandlers } from "./auth";
|
import { apiTokenAuthHandlers } from "./auth";
|
||||||
import { ApiPermissions, hasPermission, permissionArrToSet } from "@shared/apiPermissions";
|
import { ApiPermissions } from "@shared/apiPermissions";
|
||||||
import { ApiPermissionAssignments } from "../data/ApiPermissionAssignments";
|
import { hasGuildPermission, requireGuildPermission } from "./permissions";
|
||||||
|
|
||||||
export function initGuildsAPI(app: express.Express) {
|
export function initGuildsAPI(app: express.Express) {
|
||||||
const allowedGuilds = new AllowedGuilds();
|
const allowedGuilds = new AllowedGuilds();
|
||||||
const apiPermissionAssignments = new ApiPermissionAssignments();
|
|
||||||
const configs = new Configs();
|
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);
|
const guilds = await allowedGuilds.getForApiUser(req.user.userId);
|
||||||
res.json(guilds);
|
res.json(guilds);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get("/guilds/:guildId/config", ...apiTokenAuthHandlers(), async (req: Request, res: Response) => {
|
guildRouter.get("/:guildId", async (req: Request, res: Response) => {
|
||||||
const permAssignment = await apiPermissionAssignments.getByGuildAndUserId(req.params.guildId, req.user.userId);
|
if (!(await hasGuildPermission(req.user.userId, req.params.guildId, ApiPermissions.ViewGuild))) {
|
||||||
if (!permAssignment || !hasPermission(permissionArrToSet(permAssignment.permissions), ApiPermissions.ReadConfig)) {
|
|
||||||
return unauthorized(res);
|
return unauthorized(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = await configs.getActiveByKey(`guild-${req.params.guildId}`);
|
const guild = await allowedGuilds.find(req.params.guildId);
|
||||||
res.json({ config: config ? config.config : "" });
|
res.json(guild);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post("/guilds/:guildId/config", ...apiTokenAuthHandlers(), async (req, res) => {
|
guildRouter.post("/:guildId/check-permission", async (req: Request, res: Response) => {
|
||||||
const permAssignment = await apiPermissionAssignments.getByGuildAndUserId(req.params.guildId, req.user.userId);
|
const permission = req.body.permission;
|
||||||
if (!permAssignment || !hasPermission(permissionArrToSet(permAssignment.permissions), ApiPermissions.EditConfig)) {
|
const hasPermission = await hasGuildPermission(req.user.userId, req.params.guildId, permission);
|
||||||
return unauthorized(res);
|
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;
|
let config = req.body.config;
|
||||||
if (config == null) return clientError(res, "No config supplied");
|
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);
|
await configs.saveNewRevision(`guild-${req.params.guildId}`, config, req.user.userId);
|
||||||
ok(res);
|
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 { connect } from "../data/db";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { TokenError } from "passport-oauth2";
|
|
||||||
import { PluginError } from "knub";
|
|
||||||
|
|
||||||
require("dotenv").config({ path: path.resolve(process.cwd(), "api.env") });
|
require("dotenv").config({ path: path.resolve(process.cwd(), "api.env") });
|
||||||
|
|
||||||
|
@ -19,43 +10,8 @@ function errorHandler(err) {
|
||||||
|
|
||||||
process.on("unhandledRejection", errorHandler);
|
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
|
console.log("Connecting to database..."); // tslint:disable-line
|
||||||
connect().then(() => {
|
connect().then(() => {
|
||||||
const app = express();
|
import("./start");
|
||||||
|
|
||||||
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}`));
|
|
||||||
});
|
});
|
||||||
|
|
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;
|
return count !== 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
find(guildId) {
|
||||||
|
return this.allowedGuilds.findOne(guildId);
|
||||||
|
}
|
||||||
|
|
||||||
getForApiUser(userId) {
|
getForApiUser(userId) {
|
||||||
return this.allowedGuilds
|
return this.allowedGuilds
|
||||||
.createQueryBuilder("allowed_guilds")
|
.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,
|
AceEditor,
|
||||||
},
|
},
|
||||||
async mounted() {
|
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) {
|
if (this.guild == null) {
|
||||||
this.$router.push('/dashboard');
|
this.$router.push('/dashboard');
|
||||||
return;
|
return;
|
||||||
|
@ -66,7 +76,7 @@
|
||||||
computed: {
|
computed: {
|
||||||
...mapState('guilds', {
|
...mapState('guilds', {
|
||||||
guild() {
|
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() {
|
config() {
|
||||||
return this.$store.state.guilds.configs[this.$route.params.guildId];
|
return this.$store.state.guilds.configs[this.$route.params.guildId];
|
||||||
|
|
|
@ -42,7 +42,7 @@
|
||||||
computed: {
|
computed: {
|
||||||
...mapState('guilds', {
|
...mapState('guilds', {
|
||||||
guilds: state => {
|
guilds: state => {
|
||||||
const guilds = Array.from(state.available || []);
|
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;
|
||||||
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: {
|
state: {
|
||||||
availableGuildsLoadStatus: LoadStatus.None,
|
availableGuildsLoadStatus: LoadStatus.None,
|
||||||
available: [],
|
available: new Map(),
|
||||||
configs: {},
|
configs: {},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -17,7 +17,22 @@ export const GuildStore: Module<GuildState, RootState> = {
|
||||||
commit("setAvailableGuildsLoadStatus", LoadStatus.Loading);
|
commit("setAvailableGuildsLoadStatus", LoadStatus.Loading);
|
||||||
|
|
||||||
const availableGuilds = await get("guilds/available");
|
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) {
|
async loadConfig({ commit }, guildId) {
|
||||||
|
@ -35,9 +50,8 @@ export const GuildStore: Module<GuildState, RootState> = {
|
||||||
state.availableGuildsLoadStatus = status;
|
state.availableGuildsLoadStatus = status;
|
||||||
},
|
},
|
||||||
|
|
||||||
setAvailableGuilds(state: GuildState, guilds) {
|
addGuild(state: GuildState, guild) {
|
||||||
state.available = guilds;
|
state.available.set(guild.id, guild);
|
||||||
state.availableGuildsLoadStatus = LoadStatus.Done;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
setConfig(state: GuildState, { guildId, config }) {
|
setConfig(state: GuildState, { guildId, config }) {
|
||||||
|
|
|
@ -11,11 +11,14 @@ export interface AuthState {
|
||||||
|
|
||||||
export interface GuildState {
|
export interface GuildState {
|
||||||
availableGuildsLoadStatus: LoadStatus;
|
availableGuildsLoadStatus: LoadStatus;
|
||||||
available: Array<{
|
available: Map<
|
||||||
guild_id: string;
|
string,
|
||||||
name: string;
|
{
|
||||||
icon: string | null;
|
id: string;
|
||||||
}>;
|
name: string;
|
||||||
|
icon: string | null;
|
||||||
|
}
|
||||||
|
>;
|
||||||
configs: {
|
configs: {
|
||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,6 +3,7 @@ export enum ApiPermissions {
|
||||||
ManageAccess = "MANAGE_ACCESS",
|
ManageAccess = "MANAGE_ACCESS",
|
||||||
EditConfig = "EDIT_CONFIG",
|
EditConfig = "EDIT_CONFIG",
|
||||||
ReadConfig = "READ_CONFIG",
|
ReadConfig = "READ_CONFIG",
|
||||||
|
ViewGuild = "VIEW_GUILD",
|
||||||
}
|
}
|
||||||
|
|
||||||
const reverseApiPermissions = Object.entries(ApiPermissions).reduce((map, [key, value]) => {
|
const reverseApiPermissions = Object.entries(ApiPermissions).reduce((map, [key, value]) => {
|
||||||
|
@ -15,13 +16,24 @@ export const permissionNames = {
|
||||||
[ApiPermissions.ManageAccess]: "Manage dashboard access",
|
[ApiPermissions.ManageAccess]: "Manage dashboard access",
|
||||||
[ApiPermissions.EditConfig]: "Edit config",
|
[ApiPermissions.EditConfig]: "Edit config",
|
||||||
[ApiPermissions.ReadConfig]: "Read config",
|
[ApiPermissions.ReadConfig]: "Read config",
|
||||||
|
[ApiPermissions.ViewGuild]: "View server",
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TPermissionHierarchy = Array<ApiPermissions | [ApiPermissions, TPermissionHierarchy]>;
|
export type TPermissionHierarchy = Array<ApiPermissions | [ApiPermissions, TPermissionHierarchy]>;
|
||||||
|
|
||||||
|
// prettier-ignore-start
|
||||||
export const permissionHierarchy: TPermissionHierarchy = [
|
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> {
|
export function permissionArrToSet(permissions: string[]): Set<ApiPermissions> {
|
||||||
return new Set(permissions.filter(p => reverseApiPermissions[p])) as Set<ApiPermissions>;
|
return new Set(permissions.filter(p => reverseApiPermissions[p])) as Set<ApiPermissions>;
|
||||||
|
|
Loading…
Add table
Reference in a new issue