From 57fb5d7b302fe405cecfcf2ad62b843e17804b49 Mon Sep 17 00:00:00 2001 From: Puyodead1 Date: Wed, 20 Dec 2023 03:33:28 -0500 Subject: [PATCH] initial progress for admin api --- assets/openapi.json | Bin 612567 -> 618569 bytes assets/schemas.json | Bin 22134205 -> 22297672 bytes src/api/middlewares/Authentication.ts | 2 + src/api/routes/guilds/#guild_id/bans.ts | 11 ++- src/api/routes/guilds/#guild_id/channels.ts | 2 + src/api/routes/guilds/#guild_id/delete.ts | 5 +- src/api/routes/guilds/#guild_id/index.ts | 14 +-- .../#guild_id/members/#member_id/nick.ts | 15 +++- .../#member_id/roles/#role_id/index.ts | 2 + .../routes/guilds/#guild_id/members/index.ts | 6 +- .../guilds/#guild_id/messages/search.ts | 12 ++- .../guilds/#guild_id/roles/#role_id/index.ts | 8 +- .../#guild_id/roles/#role_id/members.ts | 6 +- .../routes/guilds/#guild_id/roles/index.ts | 12 ++- .../guilds/#guild_id/roles/member-counts.ts | 9 +- src/api/routes/guilds/index.ts | 77 +++++++++++++++- src/api/routes/users/#id/index.ts | 11 ++- src/api/routes/users/index.ts | 82 ++++++++++++++++++ src/api/util/handlers/route.ts | 3 +- src/util/entities/User.ts | 1 + src/util/schemas/GuildCreateSchema.ts | 1 + .../schemas/responses/AdminGuildsResponse.ts | 21 +++++ src/util/schemas/responses/index.ts | 1 + 23 files changed, 259 insertions(+), 42 deletions(-) create mode 100644 src/api/routes/users/index.ts create mode 100644 src/util/schemas/responses/AdminGuildsResponse.ts diff --git a/assets/openapi.json b/assets/openapi.json index b6544d27b11cf8574a987e84c7b9a4e84733cb34..21742458b0210948e2634f60f2ee3751374230ae 100644 GIT binary patch delta 1255 zcmbVKYe-XJ80I_sZZp)CBQH%kvkQ*WPC?LxYC)wHv=y2}2wJW(Z{aqne$=2cD5B*_ z-=f=6=mJSIqBo()AKm;>j)MA8)c!;iM58EzQaVSA)sTpOp7T8K`&_>FS?|<=_|(uo za!qkv(IW-fP+hF7(!CiIAX;O|hwxXzu%8|#{2Pmg5=xGQM| z#G=F)l%zn9PPD_z*U}2lH_;6c0focDag(W??=hEl2S zenZjipihXtcBr6Tr7)O989EARNBTby!614;ok`Q6ZJIU9RL(bT6+-$sO9o`lq?53x zgu-s9XyDU9{mkre=@>f$i8)GUW~ncr)~F42{(#!NSq*}7KF!AXE8!q%BPlEVz7{`> z@22e-`9x0S&wuJ9 z@6S$>oByMf=rX$s21oIvo5Z%O8JkrGv5}bXK z@3f^rU_htQwp0ueh#%zhu!@UT;~uW@xwtaQz3F4}y?L@y!BDR_M9qOV()nCEQkY3m zjX}InmZCQxz9~ro7q&mKMX)Pd*l|OK=wF-^LZg)Pqov^;kLXyGguwk&_CsVZdwV@BR*10xOdJ;v zGJiviZMisjQP}wyKW`Ge9u=QcAu#ye;9V!*p{Gx@$&^VGz6a_gYb$C?c!-Y1`mkGi O$N_CYpf(^c9Qgr&#@u%R delta 562 zcmZvYOK1~e5XYI#_swkDqzxssRN4@*0lkEP$)O&!MN6#)jICCD)jfzewF>H~n1k4Z zRTqL83Ra5>S}<%X(h+>62VWq%#e?(~yr_tvf<=&mn-zlvkKh0Co0;!rHSumXkv|Lf zjI~Z7*VMw5s?o{zPtfwjG2Ea`tT$q?(&7B9fqXuVQ91Cy2z@J*Tcey?HiNu>A+WEQ z32b3^FSN^YDRBCjSM4wKd{As1fi@XUs)rzvxVjqtkHMa?hnp@!gOpOLfa3G}za>v+rR4u7T`k?v2qTyTkeuyJgUqn-$8NMtpRg zzplpET-H1XVwG_$;=hYobBgVZo{`7|T>#nML8pMNt++<6JVh&94@vH%x!VwH7zdTa z7w|D~{1A1ngr+r*-c{|AdX3k-Lu-_wkBG@Sg+0ool($j$U&ztp*zGS-b9nj{Cgoq-Wei^h-|6(~$Z{td!J}ym(GO{HmY4;eVvqE{&Uha@?!S$fEhV_#3lJ B)1CkT diff --git a/assets/schemas.json b/assets/schemas.json index 08ba372c4e19ee04607e4b8e375834c88cf9cc9d..3d8bbc861c6b0b4764954acc29ba6f9c57bc9ffe 100644 GIT binary patch delta 1664 zcmb``Sya?z9LDi8;(!h^4Er!9P=edY?k=kvh>En?W@=`T8J3mWGR0JHO=ToG*}p=v zY_V*!vZhU?RHDtLv~0t&4Ni(-Qkq45pNsCg?Besidd~S>yyx9~vc=JQe7D1IY^Ik9 zGrf)7gqsM{$MiK06KVRHDC0EI#${qmtcf%6Ccz|{B;z*8#JxE^19PH&A-b`tGIGQ+kefwJOcw=o^)hj5C&ri zhGH1J7>-P2Asac!MIQ1o0wXaBqcH|!F%IKVfI<|Z7$qph1Wd#vOvV(Hp&V0DfoYhI z8K}feRN-`-firOy&c-a9gL82n&c_9qjSDdcb1@GWVLmR#0$hS>)ZkJq#AR57%TbHP zxB^#V39iD`xCTqH49jsXuEX`X0Y2P_n@|ToZpJOR6$ZEAb_8$-?!;YKfxA(Ud$1A> zxEHGs#C=$e`(fb$Jcx%vU7p6-+4j-}wF?(p^{q|m!L&S=)ll8-2&Qd`TI=8GvL&tB z7ai!V`5A{ooi&Gd_<}jEcx!%jcS>-cE6ZvtEGV=5Ek%w%Pemivpa~CSEgr$6Sck{( zIG(_hSdXXhG&bNFJd5Y>JYK*?yoi_ZGB)8AyowNB!|QkhZ{jU%#ujWvGv3BK*oN)c zffl@rR=kJz@c}->N7#v7*o{5-7@uG-_Tf`}hW+>)U*Jo8g|E?u1Na8t;vl}m_xJ%n z;-^qgMf;v6t9ebL%bI&6*`r@;rLBvYWjU85#9Fl{lH;tXo_M#_cCbC#Y6y#ThUz-n zqimKlEwAhj193ID%i$74p>_&G7%f zSGjx)-S`c^<2X*>51hoG_zOMwdqrJ`Z6%amN|@4Hu`A(9gwjXpt2mTMrJoX|IF)F{ zrNk()N}Lj}Bq)hWlHyj96_3(iIYk+uq$sIMnsTagnleyHS2C1A%3x)PGE^C+c$MKw zrjn&(D>+K8lBeV=Bb1TKC}p%VMj5M&Q^qR=N}*Du6e}f4sWL&Cs7z8OD^rv*rCgb+ XR4CJw>B~=@Qs`QX`-cfE__GC=)*s`bYPIf2R zE1;q$$sST*GH=A$ExG?Ub2&oYE=LtqMKx4M4b(&!YN0mjpf1AUL>6n2}kccFP2%qB%q~c3_g~dq25-i0sq+>Z&V5P_9ST!^}&1nmA zJF`|J1DRNZwOEJs*nqF`4K`vEHe(CE#dr7~Kj26FgrBh$+prxwuoE8a!fx!rUhKnu z9Kb;w!Y}w0hw&SJ#}OPw7P9dNj^Q{?;3Q5V2d8lcXK@baaRC?cCobVKuHY)JAs5$i z12=ICdC13K_#3xz2X{Trtb5}!^ftZH{U-zmcw$H1kGG_`g1yp1QpyFoa}w|4A3VTA zJi=oH zB1%!kTk%nR6{}KADX#b_{z?hOrj%3ylt3j&306uer4_qUMk%W}lyXXWrGipX2~jF3 zm6cGXic(dnrc_sIC^eNZrIu1#siV|Y!WE|yp+qY6l=?~orJ>SDX{CYYl { const { guild_id, user_id } = req.params; - 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 3488b64dd..976fc448e 100644 --- a/src/api/routes/guilds/#guild_id/channels.ts +++ b/src/api/routes/guilds/#guild_id/channels.ts @@ -59,6 +59,7 @@ router.post( route({ requestBody: "ChannelModifySchema", permission: "MANAGE_CHANNELS", + right: "OPERATOR", responses: { 201: { body: "Channel", @@ -95,6 +96,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 dee52c812..364faa4fc 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 75d05c9b3..ee8b2c46f 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: "GuildCreateResponse", @@ -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 7b8e44d3d..decc7bba7 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 46dd70bb7..f6da0ffb7 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 9260308d1..07ed3acfb 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 94adf9c6a..fed05fa44 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 ea1a782a6..d854c1f1c 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 539cd5d87..22744abeb 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 . */ -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 e2c34e7f3..4f56232d2 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 88243b42c..8b098dcf2 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 . */ -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 545beb185..242d49a0e 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 1bd413d35..dd47a0cd4 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 000000000..a8373fd0f --- /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 . +*/ + +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 2c98783a4..47915b26e 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) { req.permission = await getPermission( req.user_id, req.params.guild_id, @@ -118,6 +118,7 @@ export function route(opts: RouteOptions) { opts.right as string, ); } + req.has_right = true; } if (validate) { diff --git a/src/util/entities/User.ts b/src/util/entities/User.ts index 0323de528..743fc2cb7 100644 --- a/src/util/entities/User.ts +++ b/src/util/entities/User.ts @@ -66,6 +66,7 @@ export enum PrivateUserEnum { purchased_flags, premium_usage_flags, disabled, + rights, // settings, // now a relation // locale } diff --git a/src/util/schemas/GuildCreateSchema.ts b/src/util/schemas/GuildCreateSchema.ts index 41e3b214e..ad61db5ee 100644 --- a/src/util/schemas/GuildCreateSchema.ts +++ b/src/util/schemas/GuildCreateSchema.ts @@ -28,4 +28,5 @@ export interface GuildCreateSchema { channels?: ChannelModifySchema[]; system_channel_id?: string; rules_channel_id?: string; + owner_id?: string; // used by admins to create a guild for someone else } diff --git a/src/util/schemas/responses/AdminGuildsResponse.ts b/src/util/schemas/responses/AdminGuildsResponse.ts new file mode 100644 index 000000000..2f9d6bc37 --- /dev/null +++ b/src/util/schemas/responses/AdminGuildsResponse.ts @@ -0,0 +1,21 @@ +/* + 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 . +*/ + +import { Guild } from "../../entities"; + +export type AdminGuildsResponse = Guild[]; diff --git a/src/util/schemas/responses/index.ts b/src/util/schemas/responses/index.ts index 070a2e55f..fd952846e 100644 --- a/src/util/schemas/responses/index.ts +++ b/src/util/schemas/responses/index.ts @@ -18,6 +18,7 @@ export * from "./APIErrorOrCaptchaResponse"; export * from "./APIErrorResponse"; +export * from "./AdminGuildsResponse"; export * from "./BackupCodesChallengeResponse"; export * from "./CaptchaRequiredResponse"; export * from "./DiscoverableGuildsResponse";