summary refs log tree commit diff
path: root/src/api/routes
diff options
context:
space:
mode:
authorPuyodead1 <puyodead@proton.me>2023-12-20 03:33:28 -0500
committerPuyodead1 <puyodead@proton.me>2023-12-20 03:33:28 -0500
commite34887261f8d86aa4e98f4b8ccd6e57ce72c6620 (patch)
treeb7cb601c7e818349b3000eaf20bc75e44c22ff87 /src/api/routes
parentadd missing license headers (diff)
downloadserver-feat/admin-api.tar.xz
initial progress for admin api feat/admin-api
Diffstat (limited to 'src/api/routes')
-rw-r--r--src/api/routes/guilds/#guild_id/bans.ts11
-rw-r--r--src/api/routes/guilds/#guild_id/channels.ts2
-rw-r--r--src/api/routes/guilds/#guild_id/delete.ts5
-rw-r--r--src/api/routes/guilds/#guild_id/index.ts14
-rw-r--r--src/api/routes/guilds/#guild_id/members/#member_id/nick.ts15
-rw-r--r--src/api/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts2
-rw-r--r--src/api/routes/guilds/#guild_id/members/index.ts6
-rw-r--r--src/api/routes/guilds/#guild_id/messages/search.ts12
-rw-r--r--src/api/routes/guilds/#guild_id/roles/#role_id/index.ts8
-rw-r--r--src/api/routes/guilds/#guild_id/roles/#role_id/members.ts6
-rw-r--r--src/api/routes/guilds/#guild_id/roles/index.ts12
-rw-r--r--src/api/routes/guilds/#guild_id/roles/member-counts.ts9
-rw-r--r--src/api/routes/guilds/index.ts77
-rw-r--r--src/api/routes/users/#id/index.ts11
-rw-r--r--src/api/routes/users/index.ts82
15 files changed, 231 insertions, 41 deletions
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;