import { ApiPermissions } from "@zeppelinbot/shared/apiPermissions.js"; import express, { Request, Response } from "express"; import jsYaml from "js-yaml"; import moment from "moment-timezone"; import { Queue } from "../Queue.js"; import { validateGuildConfig } from "../configValidator.js"; import { AllowedGuilds } from "../data/AllowedGuilds.js"; import { ApiAuditLog } from "../data/ApiAuditLog.js"; import { ApiPermissionAssignments, ApiPermissionTypes } from "../data/ApiPermissionAssignments.js"; import { Configs } from "../data/Configs.js"; import { AuditLogEventTypes } from "../data/apiAuditLogTypes.js"; import { isSnowflake } from "../utils.js"; import { loadYamlSafely } from "../utils/loadYamlSafely.js"; import { ObjectAliasError } from "../utils/validateNoObjectAliases.js"; import { apiTokenAuthHandlers } from "./auth.js"; import { hasGuildPermission, requireGuildPermission } from "./permissions.js"; import { clientError, ok, serverError, unauthorized } from "./responses.js"; const YAMLException = jsYaml.YAMLException; const apiPermissionAssignments = new ApiPermissionAssignments(); const auditLog = new ApiAuditLog(); export function initGuildsAPI(app: express.Express) { const allowedGuilds = new AllowedGuilds(); const configs = new Configs(); 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); }); 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); } const guild = await allowedGuilds.find(req.params.guildId); res.json(guild); }); 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"); config = config.trim() + "\n"; // Normalize start/end whitespace in the config const currentConfig = await configs.getActiveByKey(`guild-${req.params.guildId}`); if (currentConfig && config === currentConfig.config) { return ok(res); } // Validate config let parsedConfig; try { parsedConfig = loadYamlSafely(config); } catch (e) { if (e instanceof YAMLException) { return res.status(400).json({ errors: [e.message] }); } if (e instanceof ObjectAliasError) { return res.status(400).json({ errors: [e.message] }); } // tslint:disable-next-line:no-console console.error("Error when loading YAML: " + e.message); return serverError(res, "Server error"); } if (parsedConfig == null) { parsedConfig = {}; } const error = await validateGuildConfig(parsedConfig); if (error) { return res.status(422).json({ errors: [error] }); } await configs.saveNewRevision(`guild-${req.params.guildId}`, config, req.user!.userId); ok(res); }); guildRouter.get( "/:guildId/permissions", requireGuildPermission(ApiPermissions.ManageAccess), async (req: Request, res: Response) => { const permissions = await apiPermissionAssignments.getByGuildId(req.params.guildId); res.json(permissions); }, ); 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) || targetId === req.user!.userId) { 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); }