diff --git a/src/api/middlewares/Authentication.ts b/src/api/middlewares/Authentication.ts
index 9e41b453..7b8898dc 100644
--- a/src/api/middlewares/Authentication.ts
+++ b/src/api/middlewares/Authentication.ts
@@ -16,8 +16,8 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import { checkToken, Rights } from "@spacebar/util";
import * as Sentry from "@sentry/node";
+import { checkToken, Rights } from "@spacebar/util";
import { NextFunction, Request, Response } from "express";
import { HTTPError } from "lambert-server";
@@ -67,6 +67,8 @@ declare global {
user_bot: boolean;
token: { id: string; iat: number };
rights: Rights;
+ has_permission?: boolean;
+ has_right?: boolean;
}
}
}
diff --git a/src/api/routes/guilds/#guild_id/bans.ts b/src/api/routes/guilds/#guild_id/bans.ts
index 9aeb27f0..93018ee5 100644
--- a/src/api/routes/guilds/#guild_id/bans.ts
+++ b/src/api/routes/guilds/#guild_id/bans.ts
@@ -39,6 +39,7 @@ router.get(
"/",
route({
permission: "BAN_MEMBERS",
+ right: "OPERATOR",
responses: {
200: {
body: "GuildBansResponse",
@@ -85,6 +86,7 @@ router.get(
"/:user",
route({
permission: "BAN_MEMBERS",
+ right: "OPERATOR",
responses: {
200: {
body: "BanModeratorSchema",
@@ -123,6 +125,7 @@ router.put(
route({
requestBody: "BanCreateSchema",
permission: "BAN_MEMBERS",
+ right: "OPERATOR",
responses: {
200: {
body: "Ban",
@@ -182,6 +185,7 @@ router.delete(
"/:user_id",
route({
permission: "BAN_MEMBERS",
+ right: "OPERATOR",
responses: {
204: {},
403: {
@@ -195,13 +199,12 @@ router.delete(
async (req: Request, res: Response) => {
const { guild_id, user_id } = req.params;
- const ban = await Ban.findOneOrFail({
- where: { guild_id: guild_id, user_id: user_id },
- });
-
const banned_user = await User.getPublicUser(user_id);
await Promise.all([
+ Ban.findOneOrFail({
+ where: { guild_id: guild_id, user_id: user_id },
+ }),
Ban.delete({
user_id: user_id,
guild_id,
diff --git a/src/api/routes/guilds/#guild_id/channels.ts b/src/api/routes/guilds/#guild_id/channels.ts
index 68208fee..fc5d5271 100644
--- a/src/api/routes/guilds/#guild_id/channels.ts
+++ b/src/api/routes/guilds/#guild_id/channels.ts
@@ -50,6 +50,7 @@ router.post(
route({
requestBody: "ChannelModifySchema",
permission: "MANAGE_CHANNELS",
+ right: "OPERATOR",
responses: {
201: {
body: "Channel",
@@ -81,6 +82,7 @@ router.patch(
route({
requestBody: "ChannelReorderSchema",
permission: "MANAGE_CHANNELS",
+ right: "OPERATOR",
responses: {
204: {},
400: {
diff --git a/src/api/routes/guilds/#guild_id/delete.ts b/src/api/routes/guilds/#guild_id/delete.ts
index dee52c81..364faa4f 100644
--- a/src/api/routes/guilds/#guild_id/delete.ts
+++ b/src/api/routes/guilds/#guild_id/delete.ts
@@ -17,7 +17,7 @@
*/
import { route } from "@spacebar/api";
-import { Guild, GuildDeleteEvent, emitEvent } from "@spacebar/util";
+import { Guild, GuildDeleteEvent, emitEvent, getRights } from "@spacebar/util";
import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server";
@@ -40,12 +40,13 @@ router.post(
}),
async (req: Request, res: Response) => {
const { guild_id } = req.params;
+ const rights = await getRights(req.user_id);
const guild = await Guild.findOneOrFail({
where: { id: guild_id },
select: ["owner_id"],
});
- if (guild.owner_id !== req.user_id)
+ if (!rights.has("OPERATOR") || guild.owner_id !== req.user_id)
throw new HTTPError("You are not the owner of this guild", 401);
await Promise.all([
diff --git a/src/api/routes/guilds/#guild_id/index.ts b/src/api/routes/guilds/#guild_id/index.ts
index 839ec363..86a75d40 100644
--- a/src/api/routes/guilds/#guild_id/index.ts
+++ b/src/api/routes/guilds/#guild_id/index.ts
@@ -19,7 +19,6 @@
import { route } from "@spacebar/api";
import {
Channel,
- DiscordApiErrors,
Guild,
GuildUpdateEvent,
GuildUpdateSchema,
@@ -27,7 +26,6 @@ import {
Permissions,
SpacebarApiErrors,
emitEvent,
- getPermission,
getRights,
handleFile,
} from "@spacebar/util";
@@ -53,12 +51,13 @@ router.get(
}),
async (req: Request, res: Response) => {
const { guild_id } = req.params;
+ const rights = await getRights(req.user_id);
const [guild, member] = await Promise.all([
Guild.findOneOrFail({ where: { id: guild_id } }),
Member.findOne({ where: { guild_id: guild_id, id: req.user_id } }),
]);
- if (!member)
+ if (!rights.has("OPERATOR") || !member)
throw new HTTPError(
"You are not a member of the guild you are trying to access",
401,
@@ -76,6 +75,7 @@ router.patch(
route({
requestBody: "GuildUpdateSchema",
permission: "MANAGE_GUILD",
+ right: "OPERATOR",
responses: {
"200": {
body: "GuildUpdateSchema",
@@ -95,14 +95,6 @@ router.patch(
const body = req.body as GuildUpdateSchema;
const { guild_id } = req.params;
- const rights = await getRights(req.user_id);
- const permission = await getPermission(req.user_id, guild_id);
-
- if (!rights.has("MANAGE_GUILDS") && !permission.has("MANAGE_GUILD"))
- throw DiscordApiErrors.MISSING_PERMISSIONS.withParams(
- "MANAGE_GUILDS",
- );
-
const guild = await Guild.findOneOrFail({
where: { id: guild_id },
relations: ["emojis", "roles", "stickers"],
diff --git a/src/api/routes/guilds/#guild_id/members/#member_id/nick.ts b/src/api/routes/guilds/#guild_id/members/#member_id/nick.ts
index 7b8e44d3..decc7bba 100644
--- a/src/api/routes/guilds/#guild_id/members/#member_id/nick.ts
+++ b/src/api/routes/guilds/#guild_id/members/#member_id/nick.ts
@@ -17,7 +17,12 @@
*/
import { route } from "@spacebar/api";
-import { getPermission, Member, PermissionResolvable } from "@spacebar/util";
+import {
+ getPermission,
+ getRights,
+ Member,
+ PermissionResolvable,
+} from "@spacebar/util";
import { Request, Response, Router } from "express";
const router = Router();
@@ -38,14 +43,18 @@ router.patch(
}),
async (req: Request, res: Response) => {
const { guild_id } = req.params;
+ const rights = await getRights(req.user_id);
let permissionString: PermissionResolvable = "MANAGE_NICKNAMES";
const member_id =
req.params.member_id === "@me"
? ((permissionString = "CHANGE_NICKNAME"), req.user_id)
: req.params.member_id;
- const perms = await getPermission(req.user_id, guild_id);
- perms.hasThrow(permissionString);
+ // admins dont need to be in the guild
+ if (member_id !== "@me" && !rights.has("OPERATOR")) {
+ const perms = await getPermission(req.user_id, guild_id);
+ perms.hasThrow(permissionString);
+ }
await Member.changeNickname(member_id, guild_id, req.body.nick);
res.status(200).send();
diff --git a/src/api/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts b/src/api/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts
index 46dd70bb..f6da0ffb 100644
--- a/src/api/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts
+++ b/src/api/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts
@@ -26,6 +26,7 @@ router.delete(
"/",
route({
permission: "MANAGE_ROLES",
+ right: "OPERATOR",
responses: {
204: {},
403: {
@@ -45,6 +46,7 @@ router.put(
"/",
route({
permission: "MANAGE_ROLES",
+ right: "OPERATOR",
responses: {
204: {},
403: {},
diff --git a/src/api/routes/guilds/#guild_id/members/index.ts b/src/api/routes/guilds/#guild_id/members/index.ts
index 9260308d..07ed3acf 100644
--- a/src/api/routes/guilds/#guild_id/members/index.ts
+++ b/src/api/routes/guilds/#guild_id/members/index.ts
@@ -17,7 +17,7 @@
*/
import { route } from "@spacebar/api";
-import { Member, PublicMemberProjection } from "@spacebar/util";
+import { Member, PublicMemberProjection, getRights } from "@spacebar/util";
import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server";
import { MoreThan } from "typeorm";
@@ -51,13 +51,15 @@ router.get(
}),
async (req: Request, res: Response) => {
const { guild_id } = req.params;
+ const rights = await getRights(req.user_id);
const limit = Number(req.query.limit) || 1;
if (limit > 1000 || limit < 1)
throw new HTTPError("Limit must be between 1 and 1000");
const after = `${req.query.after}`;
const query = after ? { id: MoreThan(after) } : {};
- await Member.IsInGuildOrFail(req.user_id, guild_id);
+ if (!rights.has("OPERATOR"))
+ await Member.IsInGuildOrFail(req.user_id, guild_id);
const members = await Member.find({
where: { guild_id, ...query },
diff --git a/src/api/routes/guilds/#guild_id/messages/search.ts b/src/api/routes/guilds/#guild_id/messages/search.ts
index 637d1e43..f1111050 100644
--- a/src/api/routes/guilds/#guild_id/messages/search.ts
+++ b/src/api/routes/guilds/#guild_id/messages/search.ts
@@ -19,7 +19,13 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import { route } from "@spacebar/api";
-import { Channel, FieldErrors, Message, getPermission } from "@spacebar/util";
+import {
+ Channel,
+ FieldErrors,
+ Message,
+ getPermission,
+ getRights,
+} from "@spacebar/util";
import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server";
import { FindManyOptions, In, Like } from "typeorm";
@@ -53,6 +59,7 @@ router.get(
author_id,
} = req.query;
+ const rights = await getRights(req.user_id);
const parsedLimit = Number(limit) || 50;
if (parsedLimit < 1 || parsedLimit > 100)
throw new HTTPError("limit must be between 1 and 100", 422);
@@ -75,7 +82,7 @@ router.get(
req.params.guild_id,
channel_id as string | undefined,
);
- permissions.hasThrow("VIEW_CHANNEL");
+ if (!rights.has("OPERATOR")) permissions.hasThrow("VIEW_CHANNEL");
if (!permissions.has("READ_MESSAGE_HISTORY"))
return res.json({ messages: [], total_results: 0 });
@@ -120,6 +127,7 @@ router.get(
channel.id,
);
if (
+ !rights.has("OPERATOR") ||
!perm.has("VIEW_CHANNEL") ||
!perm.has("READ_MESSAGE_HISTORY")
)
diff --git a/src/api/routes/guilds/#guild_id/roles/#role_id/index.ts b/src/api/routes/guilds/#guild_id/roles/#role_id/index.ts
index ea1a782a..d854c1f1 100644
--- a/src/api/routes/guilds/#guild_id/roles/#role_id/index.ts
+++ b/src/api/routes/guilds/#guild_id/roles/#role_id/index.ts
@@ -19,6 +19,7 @@
import { route } from "@spacebar/api";
import {
emitEvent,
+ getRights,
GuildRoleDeleteEvent,
GuildRoleUpdateEvent,
handleFile,
@@ -48,7 +49,10 @@ router.get(
}),
async (req: Request, res: Response) => {
const { guild_id, role_id } = req.params;
- await Member.IsInGuildOrFail(req.user_id, guild_id);
+ const rights = await getRights(req.user_id);
+ // admins dont need to be in the guild
+ if (!rights.has("OPERATOR"))
+ await Member.IsInGuildOrFail(req.user_id, guild_id);
const role = await Role.findOneOrFail({
where: { guild_id, id: role_id },
});
@@ -59,6 +63,7 @@ router.get(
router.delete(
"/",
route({
+ right: "OPERATOR",
permission: "MANAGE_ROLES",
responses: {
204: {},
@@ -103,6 +108,7 @@ router.patch(
"/",
route({
requestBody: "RoleModifySchema",
+ right: "OPERATOR",
permission: "MANAGE_ROLES",
responses: {
200: {
diff --git a/src/api/routes/guilds/#guild_id/roles/#role_id/members.ts b/src/api/routes/guilds/#guild_id/roles/#role_id/members.ts
index 539cd5d8..22744abe 100644
--- a/src/api/routes/guilds/#guild_id/roles/#role_id/members.ts
+++ b/src/api/routes/guilds/#guild_id/roles/#role_id/members.ts
@@ -16,15 +16,15 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import { Router, Request, Response } from "express";
-import { DiscordApiErrors, Member, partition } from "@spacebar/util";
import { route } from "@spacebar/api";
+import { DiscordApiErrors, Member, partition } from "@spacebar/util";
+import { Request, Response, Router } from "express";
const router = Router();
router.patch(
"/",
- route({ permission: "MANAGE_ROLES" }),
+ route({ permission: "MANAGE_ROLES", right: "OPERATOR" }),
async (req: Request, res: Response) => {
// Payload is JSON containing a list of member_ids, the new list of members to have the role
const { guild_id, role_id } = req.params;
diff --git a/src/api/routes/guilds/#guild_id/roles/index.ts b/src/api/routes/guilds/#guild_id/roles/index.ts
index e2c34e7f..4f56232d 100644
--- a/src/api/routes/guilds/#guild_id/roles/index.ts
+++ b/src/api/routes/guilds/#guild_id/roles/index.ts
@@ -49,6 +49,7 @@ router.post(
route({
requestBody: "RoleModifySchema",
permission: "MANAGE_ROLES",
+ right: "OPERATOR",
responses: {
200: {
body: "Role",
@@ -65,11 +66,14 @@ router.post(
const guild_id = req.params.guild_id;
const body = req.body as RoleModifySchema;
- const role_count = await Role.count({ where: { guild_id } });
- const { maxRoles } = Config.get().limits.guild;
+ // admins can bypass this
+ if (!req.has_right) {
+ const role_count = await Role.count({ where: { guild_id } });
+ const { maxRoles } = Config.get().limits.guild;
- if (role_count > maxRoles)
- throw DiscordApiErrors.MAXIMUM_ROLES.withParams(maxRoles);
+ if (role_count > maxRoles)
+ throw DiscordApiErrors.MAXIMUM_ROLES.withParams(maxRoles);
+ }
const role = Role.create({
// values before ...body are default and can be overriden
diff --git a/src/api/routes/guilds/#guild_id/roles/member-counts.ts b/src/api/routes/guilds/#guild_id/roles/member-counts.ts
index 88243b42..8b098dcf 100644
--- a/src/api/routes/guilds/#guild_id/roles/member-counts.ts
+++ b/src/api/routes/guilds/#guild_id/roles/member-counts.ts
@@ -16,16 +16,19 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import { Request, Response, Router } from "express";
-import { Role, Member } from "@spacebar/util";
import { route } from "@spacebar/api";
+import { Member, Role, getRights } from "@spacebar/util";
+import { Request, Response, Router } from "express";
import {} from "typeorm";
const router: Router = Router();
router.get("/", route({}), async (req: Request, res: Response) => {
const { guild_id } = req.params;
- await Member.IsInGuildOrFail(req.user_id, guild_id);
+ const rights = await getRights(req.user_id);
+ // admins dont need to be in the guild
+ if (!rights.has("OPERATOR"))
+ await Member.IsInGuildOrFail(req.user_id, guild_id);
const role_ids = await Role.find({ where: { guild_id }, select: ["id"] });
const counts: { [id: string]: number } = {};
diff --git a/src/api/routes/guilds/index.ts b/src/api/routes/guilds/index.ts
index 545beb18..242d49a0 100644
--- a/src/api/routes/guilds/index.ts
+++ b/src/api/routes/guilds/index.ts
@@ -26,16 +26,71 @@ import {
getRights,
} from "@spacebar/util";
import { Request, Response, Router } from "express";
+import { HTTPError } from "lambert-server";
+import { ILike, MoreThan } from "typeorm";
const router: Router = Router();
+router.get(
+ "/",
+ route({
+ description: "Get a list of guilds",
+ right: "OPERATOR",
+ query: {
+ limit: {
+ description:
+ "max number of guilds to return (1-1000). default 100",
+ type: "number",
+ required: false,
+ },
+ after: {
+ description: "The amount of guilds to skip",
+ type: "number",
+ required: false,
+ },
+ query: {
+ description: "The search query",
+ type: "string",
+ required: false,
+ },
+ },
+ responses: {
+ 200: {
+ body: "AdminGuildsResponse",
+ },
+ 400: {
+ body: "APIErrorResponse",
+ },
+ },
+ }),
+ async (req: Request, res: Response) => {
+ const { after, query } = req.query as {
+ after?: number;
+ query?: string;
+ };
+
+ const limit = Number(req.query.limit) || 100;
+ if (limit > 1000 || limit < 1)
+ throw new HTTPError("Limit must be between 1 and 1000");
+
+ const guilds = await Guild.find({
+ where: {
+ ...(after ? { id: MoreThan(`${after}`) } : {}),
+ ...(query ? { name: ILike(`%${query}%`) } : {}),
+ },
+ take: limit,
+ });
+
+ res.send(guilds);
+ },
+);
+
//TODO: create default channel
router.post(
"/",
route({
requestBody: "GuildCreateSchema",
- right: "CREATE_GUILDS",
responses: {
201: {
body: "GuildCreateResponse",
@@ -50,17 +105,31 @@ router.post(
}),
async (req: Request, res: Response) => {
const body = req.body as GuildCreateSchema;
+ const rights = await getRights(req.user_id);
+ if (!rights.has("CREATE_GUILDS") && !rights.has("OPERATOR")) {
+ throw new HTTPError(
+ `You are missing the following rights CREATE_GUILDS or OPERATOR`,
+ 403,
+ );
+ }
const { maxGuilds } = Config.get().limits.user;
const guild_count = await Member.count({ where: { id: req.user_id } });
- const rights = await getRights(req.user_id);
- if (guild_count >= maxGuilds && !rights.has("MANAGE_GUILDS")) {
+ // allow admins to bypass guild limits
+ if (guild_count >= maxGuilds && !rights.has("OPERATOR")) {
throw DiscordApiErrors.MAXIMUM_GUILDS.withParams(maxGuilds);
}
+ let owner_id = req.user_id;
+
+ // only admins can do this, is ignored otherwise
+ if (body.owner_id && rights.has("OPERATOR")) {
+ owner_id = body.owner_id;
+ }
+
const guild = await Guild.createGuild({
...body,
- owner_id: req.user_id,
+ owner_id,
});
const { autoJoin } = Config.get().guild;
diff --git a/src/api/routes/users/#id/index.ts b/src/api/routes/users/#id/index.ts
index 1bd413d3..dd47a0cd 100644
--- a/src/api/routes/users/#id/index.ts
+++ b/src/api/routes/users/#id/index.ts
@@ -17,7 +17,7 @@
*/
import { route } from "@spacebar/api";
-import { User } from "@spacebar/util";
+import { User, getRights } from "@spacebar/util";
import { Request, Response, Router } from "express";
const router: Router = Router();
@@ -33,8 +33,15 @@ router.get(
}),
async (req: Request, res: Response) => {
const { id } = req.params;
+ const rights = await getRights(req.user_id);
- res.json(await User.getPublicUser(id));
+ const user = await User.findOneOrFail({ where: { id } });
+
+ res.json(
+ rights.has("OPERATOR")
+ ? await user.toPrivateUser()
+ : await user.toPublicUser(),
+ );
},
);
diff --git a/src/api/routes/users/index.ts b/src/api/routes/users/index.ts
new file mode 100644
index 00000000..a8373fd0
--- /dev/null
+++ b/src/api/routes/users/index.ts
@@ -0,0 +1,82 @@
+/*
+ Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
+ Copyright (C) 2023 Spacebar and Spacebar Contributors
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+import { route } from "@spacebar/api";
+import { PrivateUserProjection, User } from "@spacebar/util";
+import { Request, Response, Router } from "express";
+import { HTTPError } from "lambert-server";
+import { ILike, MoreThan } from "typeorm";
+const router = Router();
+
+router.get(
+ "/",
+ route({
+ right: "OPERATOR",
+ description: "Get a list of users",
+ query: {
+ limit: {
+ description:
+ "max number of users to return (1-1000). default 100",
+ type: "number",
+ required: false,
+ },
+ after: {
+ description: "The amount of users to skip",
+ type: "number",
+ required: false,
+ },
+ query: {
+ description: "The search query",
+ type: "string",
+ required: false,
+ },
+ },
+ responses: {
+ 200: {
+ body: "AdminUsersResponse",
+ },
+ 400: {
+ body: "APIErrorResponse",
+ },
+ },
+ }),
+ async (req: Request, res: Response) => {
+ const { after, query } = req.query as {
+ after?: number;
+ query?: string;
+ };
+
+ const limit = Number(req.query.limit) || 100;
+ if (limit > 1000 || limit < 1)
+ throw new HTTPError("Limit must be between 1 and 1000");
+
+ const users = await User.find({
+ where: {
+ ...(after ? { id: MoreThan(`${after}`) } : {}),
+ ...(query ? { username: ILike(`%${query}%`) } : {}),
+ },
+ take: limit,
+ select: PrivateUserProjection,
+ order: { id: "ASC" },
+ });
+
+ res.send(users);
+ },
+);
+
+export default router;
diff --git a/src/api/util/handlers/route.ts b/src/api/util/handlers/route.ts
index 5a0b48e6..eef6c526 100644
--- a/src/api/util/handlers/route.ts
+++ b/src/api/util/handlers/route.ts
@@ -89,7 +89,7 @@ export function route(opts: RouteOptions) {
}
return async (req: Request, res: Response, next: NextFunction) => {
- if (opts.permission) {
+ if (opts.permission && !opts.right) {
const required = new Permissions(opts.permission);
req.permission = await getPermission(
req.user_id,
@@ -103,6 +103,8 @@ export function route(opts: RouteOptions) {
opts.permission as string,
);
}
+
+ req.has_permission = true;
}
if (opts.right) {
@@ -114,6 +116,7 @@ export function route(opts: RouteOptions) {
opts.right as string,
);
}
+ req.has_right = true;
}
if (validate) {
|