diff options
author | Puyodead1 <puyodead@proton.me> | 2023-12-20 03:33:28 -0500 |
---|---|---|
committer | Puyodead1 <puyodead@proton.me> | 2023-12-20 03:33:28 -0500 |
commit | e34887261f8d86aa4e98f4b8ccd6e57ce72c6620 (patch) | |
tree | b7cb601c7e818349b3000eaf20bc75e44c22ff87 /src/api/routes | |
parent | add missing license headers (diff) | |
download | server-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.ts | 11 | ||||
-rw-r--r-- | src/api/routes/guilds/#guild_id/channels.ts | 2 | ||||
-rw-r--r-- | src/api/routes/guilds/#guild_id/delete.ts | 5 | ||||
-rw-r--r-- | src/api/routes/guilds/#guild_id/index.ts | 14 | ||||
-rw-r--r-- | src/api/routes/guilds/#guild_id/members/#member_id/nick.ts | 15 | ||||
-rw-r--r-- | src/api/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts | 2 | ||||
-rw-r--r-- | src/api/routes/guilds/#guild_id/members/index.ts | 6 | ||||
-rw-r--r-- | src/api/routes/guilds/#guild_id/messages/search.ts | 12 | ||||
-rw-r--r-- | src/api/routes/guilds/#guild_id/roles/#role_id/index.ts | 8 | ||||
-rw-r--r-- | src/api/routes/guilds/#guild_id/roles/#role_id/members.ts | 6 | ||||
-rw-r--r-- | src/api/routes/guilds/#guild_id/roles/index.ts | 12 | ||||
-rw-r--r-- | src/api/routes/guilds/#guild_id/roles/member-counts.ts | 9 | ||||
-rw-r--r-- | src/api/routes/guilds/index.ts | 77 | ||||
-rw-r--r-- | src/api/routes/users/#id/index.ts | 11 | ||||
-rw-r--r-- | src/api/routes/users/index.ts | 82 |
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; |