diff --git a/assets/openapi.json b/assets/openapi.json index 346355025..509181b40 100644 Binary files a/assets/openapi.json and b/assets/openapi.json differ diff --git a/assets/schemas.json b/assets/schemas.json index c7d164718..760fed831 100644 Binary files a/assets/schemas.json and b/assets/schemas.json differ diff --git a/src/api/routes/oauth2/applications/#id/allowlist.ts b/src/api/routes/oauth2/applications/#id/allowlist.ts new file mode 100644 index 000000000..10698b74b --- /dev/null +++ b/src/api/routes/oauth2/applications/#id/allowlist.ts @@ -0,0 +1,114 @@ +/* + 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 { Request, Response, Router } from "express"; +import { route } from "@spacebar/api/middlewares"; +import { Application, User } from "@spacebar/database"; +import { Config, DiscordApiErrors } from "@spacebar/util"; +import { AllowlistAddRequestSchema, AllowlistedUser, ApplicationAllowlistResponse, ApplicationMembershipState, PublicUserProjection } from "@spacebar/schemas"; + +const router: Router = Router({ mergeParams: true }); + +router.get( + "/", + route({ + responses: { + 200: { + body: "ApplicationAllowlistResponse", + }, + }, + }), + async (req: Request, res: Response) => { + // TODO: stub + const app = await Application.findOneOrFail({ + where: { id: req.params.id as string }, + relations: { bot: true, owner: true }, + select: { + owner: { id: true }, + }, + }); + + if (req.user_id != app.owner.id) throw DiscordApiErrors.ACTION_NOT_AUTHORIZED_ON_APPLICATION; + + return res.json([] satisfies ApplicationAllowlistResponse); + }, +); + +router.post( + "/", + route({ + requestBody: "AllowlistAddRequestSchema", + responses: { + 200: { + body: "AllowlistedUser", + }, + }, + }), + async (req: Request, res: Response) => { + // TODO: stub + const app = await Application.findOneOrFail({ + where: { id: req.params.id as string }, + relations: { bot: true, owner: true }, + select: { + owner: { id: true }, + }, + }); + + if (req.user_id != app.owner.id) throw DiscordApiErrors.ACTION_NOT_AUTHORIZED_ON_APPLICATION; + if (Number(1) >= Config.get().limits.application.maxTesters) throw DiscordApiErrors.MAXIMUM_OAUTH2_APPLICATIONS; // TODO: correct error + + const body = req.body as AllowlistAddRequestSchema; + + const targetUser = await User.findOneOrFail({ + where: { + username: body.username, + discriminator: body.discriminator ?? "0", + }, + }); + + return res.json({ + user: targetUser.toPartialUser(), + state: ApplicationMembershipState.INVITED, + } satisfies AllowlistedUser); + }, +); + +router.delete( + "/:user_id", + route({ + responses: { + 204: {}, + }, + }), + async (req: Request, res: Response) => { + // TODO: stub + const app = await Application.findOneOrFail({ + where: { id: req.params.id as string }, + relations: { bot: true, owner: true }, + select: { + owner: Object.fromEntries(PublicUserProjection.map((x) => [x, true])), + }, + }); + + if (req.user_id != app.owner.id) throw DiscordApiErrors.ACTION_NOT_AUTHORIZED_ON_APPLICATION; + + return res.status(204).json({}); + }, +); + +export default router; diff --git a/src/api/routes/oauth2/applications/@me.ts b/src/api/routes/oauth2/applications/@me.ts index 872cb5b27..732b74ef0 100644 --- a/src/api/routes/oauth2/applications/@me.ts +++ b/src/api/routes/oauth2/applications/@me.ts @@ -35,7 +35,7 @@ router.get( }), async (req: Request, res: Response) => { const app = await Application.findOneOrFail({ - where: { id: req.params.id as string }, // ...huh? there's no ID in the path... + where: { id: req.user_id as string }, relations: { bot: true, owner: true }, select: { owner: Object.fromEntries(PublicUserProjection.map((x) => [x, true])), diff --git a/src/api/routes/oauth2/applications/allowlist.ts b/src/api/routes/oauth2/applications/allowlist.ts new file mode 100644 index 000000000..43d07d903 --- /dev/null +++ b/src/api/routes/oauth2/applications/allowlist.ts @@ -0,0 +1,62 @@ +/* + 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 { Request, Response, Router } from "express"; +import { route } from "@spacebar/api/middlewares"; +import { Application, User } from "@spacebar/database"; +import { Config, DiscordApiErrors } from "@spacebar/util"; +import { + AllowlistAcceptSchema, + AllowlistAddRequestSchema, + AllowlistedUser, + ApplicationAllowlistResponse, + ApplicationMembershipState, + PublicUserProjection, +} from "@spacebar/schemas"; + +const router: Router = Router({ mergeParams: true }); + +router.post( + "/accept", + route({ + requestBody: "AllowlistAcceptSchema", + responses: { + 200: { + body: "AllowlistedUser", + }, + }, + }), + async (req: Request, res: Response) => { + // TODO: stub + const app = await Application.findOneOrFail({ + where: { id: req.params.id as string }, + relations: { bot: true, owner: true }, + select: { + owner: { id: true }, + }, + }); + + if (req.user_id != app.owner.id) throw DiscordApiErrors.ACTION_NOT_AUTHORIZED_ON_APPLICATION; + + const body = req.body as AllowlistAcceptSchema; + + return res.status(204).json({}); + }, +); + +export default router; diff --git a/src/schemas/api/developers/Application.ts b/src/schemas/api/developers/Application.ts index b2ccd1e46..62fc818b8 100644 --- a/src/schemas/api/developers/Application.ts +++ b/src/schemas/api/developers/Application.ts @@ -344,3 +344,20 @@ export enum EmbeddedActivityReleasePhase { SOFT_LAUNCH_MULTI_GEO = "soft_launch_multi_geo", GLOBAL_LAUNCH = "global_launch", } + +export type ApplicationAllowlistResponse = AllowlistedUser[]; +export interface AllowlistedUser { + user: PartialUser; + state: ApplicationMembershipState; +} +export enum ApplicationMembershipState { + INVITED = 1, + ACCEPTED = 2, +} +export interface AllowlistAddRequestSchema { + username: string; + discriminator?: string | null; +} +export interface AllowlistAcceptSchema { + token: string; +} diff --git a/src/util/config/types/subconfigurations/limits/ApplicationLimits.ts b/src/util/config/types/subconfigurations/limits/ApplicationLimits.ts index 52d492607..40d692ba9 100644 --- a/src/util/config/types/subconfigurations/limits/ApplicationLimits.ts +++ b/src/util/config/types/subconfigurations/limits/ApplicationLimits.ts @@ -18,6 +18,7 @@ export class ApplicationLimits { maxEmojis: number = 1000; + maxTesters: number = 50; commandLimits: CommandLimits = new CommandLimits(); }