From 1ce7879ee85ea5bb5efaff3cf950e65513098d3c Mon Sep 17 00:00:00 2001 From: Puyodead1 Date: Sat, 25 Mar 2023 16:09:04 -0400 Subject: [PATCH] oapi: users progress --- assets/openapi.json | Bin 415232 -> 436484 bytes assets/schemas.json | Bin 13825921 -> 15037707 bytes scripts/openapi.js | 1 + .../messages/#message_id/reactions.ts | 2 +- src/api/routes/users/#id/delete.ts | 13 +- src/api/routes/users/#id/index.ts | 22 ++- src/api/routes/users/#id/relationships.ts | 9 +- src/api/routes/users/@me/channels.ts | 43 +++-- src/api/routes/users/@me/delete.ts | 67 ++++--- src/api/routes/users/@me/disable.ts | 61 +++--- src/api/routes/users/@me/guilds.ts | 139 ++++++++------ src/api/routes/users/@me/index.ts | 41 +++- .../users/@me/mfa/codes-verification.ts | 15 +- src/api/routes/users/@me/mfa/codes.ts | 18 +- src/api/routes/users/@me/notes.ts | 134 +++++++------ src/api/routes/users/@me/relationships.ts | 179 +++++++++++------- src/api/routes/users/@me/settings.ts | 42 +++- src/api/util/handlers/route.ts | 1 + src/util/entities/User.ts | 3 +- src/util/schemas/UserNoteUpdateSchema.ts | 3 + src/util/schemas/UserProfileResponse.ts | 4 +- src/util/schemas/index.ts | 1 + .../schemas/responses/UserNoteResponse.ts | 5 + .../schemas/responses/UserProfileResponse.ts | 4 +- .../responses/UserRelationshipsResponse.ts | 8 + src/util/schemas/responses/UserResponse.ts | 22 +++ src/util/schemas/responses/index.ts | 3 + 27 files changed, 573 insertions(+), 267 deletions(-) create mode 100644 src/util/schemas/UserNoteUpdateSchema.ts create mode 100644 src/util/schemas/responses/UserNoteResponse.ts create mode 100644 src/util/schemas/responses/UserRelationshipsResponse.ts create mode 100644 src/util/schemas/responses/UserResponse.ts diff --git a/assets/openapi.json b/assets/openapi.json index bc43189a94d09c4bee5ff2ebf91d8b9ac86a9d5d..211b84510862b00e71f4f2b2d62a5d2696f30866 100644 GIT binary patch delta 1753 zcmdT_TWnNS6wP_uI}D{b6|v8m-YKXx(spVi5v(#TltQaAXd7baV}_ZFy`jA`%=Ezr zu~Rj{53rM-w!20x1*@dg;;5v@SER8CKB{fO7-E8fNTr~}!jCbi=(%@DX^0>G`ghLW zd+mMBTI-%0eLMZd(;3m@==&Rdr3I&8IGcs7WJA>!aIVEQ%)7S`ypwnvo35yV`>RoL zVrRRU2+P5+_N=DE)?ECsnE{s_Nsw$swJ>bQdzfch$Wtx~E*sliF%i;!#Czsxdk0$xOl5=V5)cqxm=o-ywp~gb#k_V0JKst9^w)U#AQ82o z1c-XGv_{eexSm}bQDI;PZ@h_QhpuEKGB6ONjvg15j5Gz;30wrClLfAX$Qw|65-H5} zQU-XUlEf!>lWmwbXrTx=10(?PLi8$g9A3hX@CVuT(fr<>D1$|l)4+WgtxI5IvpcA+ z`FFA1yX{>v?QKI@u)Uh3%%3`|nb_#!TN1&$GI1E{8Td4&+igjnu-58pRt*8oxdweK zesB@|876HkdS@!jAF;yrMR>iramT&{gIq4i{frtR7>nY1cCv2`c)!JY&_5w|-tKx( zoQSfueXGr|=xAX-^wcot!D1F#l_&NkZ0veIobN`aKFm9`8qReiD=Rx%%&s1=8&+Y` z@QOsAis2hVNh?boQP}wL0z(q1Iu!6FkEAzCP1Hkv2CzIDmcn=l7btI_yJw6J>9D!kmsQ>|NOSP*?83fdM`0^gQifz;#8O%otoh zA|(`I?~}Bo+ZEw(g1nALACMNx#FcS!G}-KL+Rpq-#Aope7}!iB<_I1-MK0NR{3H6u z0&z-t#SCee5=il$EQZrd<$mt!Cf^|LxI$78_x8~_bUW_ReeH9Ex6jeaQ^qs2va2?y o*F_&PtaS!)*MQVS!2K6p15fWqS#0{E1j>74f*<=_dg|qW0B%K_asU7T delta 439 zcmZp@eZv z_W%9NCs`&pRJc!0SZOw0!GoD^`hIU_vB~pyNP|^xo5}1nZ2~jTbp2V(;?p0@XR_FS z^Bt!rQgEm>LgnzjuyFbjfre$v)kvlzH*wg54~v0i{VfnbQsbGYL<3 zh+>tVu3*B#vR$BzIhtv^S2Z&`%Xa(u%of7aHz=~a**4dw}q5Un%sGsi)+ZdZ8D%*p~W#q$I6 zbqFi{H?!b$)6dM8A%-?YvIa6v7r4Ugw!QNwvjjK9kd+)PbD@U#@w2Sr1&Myy{!fl& zmcTX-mJMJHPTQr7S?XEgZfj@U&St|R$Oso`Vr*}AV%gs8#Oi$!Jp>G5m;|OTNMw|n bK0$+3Z2Ey1Ho5H$uBs= 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 diff --git a/scripts/openapi.js b/scripts/openapi.js index 0b21f1a3..abb40975 100644 --- a/scripts/openapi.js +++ b/scripts/openapi.js @@ -145,6 +145,7 @@ function apiRoutes() { if (route.description) obj.description = route.description; if (route.summary) obj.summary = route.summary; + if (route.deprecated) obj.deprecated = route.deprecated; if (route.requestBody) { obj.requestBody = { diff --git a/src/api/routes/channels/#channel_id/messages/#message_id/reactions.ts b/src/api/routes/channels/#channel_id/messages/#message_id/reactions.ts index c6db772b..5efa0f14 100644 --- a/src/api/routes/channels/#channel_id/messages/#message_id/reactions.ts +++ b/src/api/routes/channels/#channel_id/messages/#message_id/reactions.ts @@ -144,7 +144,7 @@ router.get( permission: "VIEW_CHANNEL", responses: { 200: { - body: "UserPublic", + body: "PublicUser", }, 400: { body: "APIErrorResponse", diff --git a/src/api/routes/users/#id/delete.ts b/src/api/routes/users/#id/delete.ts index e36a35e6..5b1a682c 100644 --- a/src/api/routes/users/#id/delete.ts +++ b/src/api/routes/users/#id/delete.ts @@ -30,7 +30,18 @@ const router = Router(); router.post( "/", - route({ right: "MANAGE_USERS" }), + route({ + right: "MANAGE_USERS", + responses: { + 204: {}, + 403: { + body: "APIErrorResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), async (req: Request, res: Response) => { await User.findOneOrFail({ where: { id: req.params.id }, diff --git a/src/api/routes/users/#id/index.ts b/src/api/routes/users/#id/index.ts index 0c7cfe37..4e3625a4 100644 --- a/src/api/routes/users/#id/index.ts +++ b/src/api/routes/users/#id/index.ts @@ -16,16 +16,26 @@ along with this program. If not, see . */ -import { Router, Request, Response } from "express"; -import { User } from "@spacebar/util"; import { route } from "@spacebar/api"; +import { User } from "@spacebar/util"; +import { Request, Response, Router } from "express"; const router: Router = Router(); -router.get("/", route({}), async (req: Request, res: Response) => { - const { id } = req.params; +router.get( + "/", + route({ + responses: { + 200: { + body: "PublicUserResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const { id } = req.params; - res.json(await User.getPublicUser(id)); -}); + res.json(await User.getPublicUser(id)); + }, +); export default router; diff --git a/src/api/routes/users/#id/relationships.ts b/src/api/routes/users/#id/relationships.ts index f18672b1..7accad3b 100644 --- a/src/api/routes/users/#id/relationships.ts +++ b/src/api/routes/users/#id/relationships.ts @@ -24,7 +24,14 @@ const router: Router = Router(); router.get( "/", - route({ responses: { 200: { body: "UserRelationsResponse" } } }), + route({ + responses: { + 200: { body: "UserRelationsResponse" }, + 404: { + body: "APIErrorResponse", + }, + }, + }), async (req: Request, res: Response) => { const mutual_relations: object[] = []; const requested_relations = await User.findOneOrFail({ diff --git a/src/api/routes/users/@me/channels.ts b/src/api/routes/users/@me/channels.ts index aaba7b70..9354d0cf 100644 --- a/src/api/routes/users/@me/channels.ts +++ b/src/api/routes/users/@me/channels.ts @@ -27,21 +27,40 @@ import { Request, Response, Router } from "express"; const router: Router = Router(); -router.get("/", route({}), async (req: Request, res: Response) => { - const recipients = await Recipient.find({ - where: { user_id: req.user_id, closed: false }, - relations: ["channel", "channel.recipients"], - }); - res.json( - await Promise.all( - recipients.map((r) => DmChannelDTO.from(r.channel, [req.user_id])), - ), - ); -}); +router.get( + "/", + route({ + responses: { + 200: { + body: "UserChannelsResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const recipients = await Recipient.find({ + where: { user_id: req.user_id, closed: false }, + relations: ["channel", "channel.recipients"], + }); + res.json( + await Promise.all( + recipients.map((r) => + DmChannelDTO.from(r.channel, [req.user_id]), + ), + ), + ); + }, +); router.post( "/", - route({ requestBody: "DmChannelCreateSchema" }), + route({ + requestBody: "DmChannelCreateSchema", + responses: { + 200: { + body: "DmChannelDTO", + }, + }, + }), async (req: Request, res: Response) => { const body = req.body as DmChannelCreateSchema; res.json( diff --git a/src/api/routes/users/@me/delete.ts b/src/api/routes/users/@me/delete.ts index dce737fc..e36a1e92 100644 --- a/src/api/routes/users/@me/delete.ts +++ b/src/api/routes/users/@me/delete.ts @@ -16,41 +16,58 @@ along with this program. If not, see . */ -import { Router, Request, Response } from "express"; -import { Member, User } from "@spacebar/util"; import { route } from "@spacebar/api"; +import { Member, User } from "@spacebar/util"; import bcrypt from "bcrypt"; +import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server"; const router = Router(); -router.post("/", route({}), async (req: Request, res: Response) => { - const user = await User.findOneOrFail({ - where: { id: req.user_id }, - select: ["data"], - }); //User object - let correctpass = true; +router.post( + "/", + route({ + responses: { + 204: {}, + 401: { + body: "APIErrorResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const user = await User.findOneOrFail({ + where: { id: req.user_id }, + select: ["data"], + }); //User object + let correctpass = true; - if (user.data.hash) { - // guest accounts can delete accounts without password - correctpass = await bcrypt.compare(req.body.password, user.data.hash); - if (!correctpass) { - throw new HTTPError(req.t("auth:login.INVALID_PASSWORD")); + if (user.data.hash) { + // guest accounts can delete accounts without password + correctpass = await bcrypt.compare( + req.body.password, + user.data.hash, + ); + if (!correctpass) { + throw new HTTPError(req.t("auth:login.INVALID_PASSWORD")); + } } - } - // TODO: decrement guild member count + // TODO: decrement guild member count - if (correctpass) { - await Promise.all([ - User.delete({ id: req.user_id }), - Member.delete({ id: req.user_id }), - ]); + if (correctpass) { + await Promise.all([ + User.delete({ id: req.user_id }), + Member.delete({ id: req.user_id }), + ]); - res.sendStatus(204); - } else { - res.sendStatus(401); - } -}); + res.sendStatus(204); + } else { + res.sendStatus(401); + } + }, +); export default router; diff --git a/src/api/routes/users/@me/disable.ts b/src/api/routes/users/@me/disable.ts index d123a6a1..b4d03e62 100644 --- a/src/api/routes/users/@me/disable.ts +++ b/src/api/routes/users/@me/disable.ts @@ -16,35 +16,52 @@ along with this program. If not, see . */ -import { User } from "@spacebar/util"; -import { Router, Response, Request } from "express"; import { route } from "@spacebar/api"; +import { User } from "@spacebar/util"; import bcrypt from "bcrypt"; +import { Request, Response, Router } from "express"; const router = Router(); -router.post("/", route({}), async (req: Request, res: Response) => { - const user = await User.findOneOrFail({ - where: { id: req.user_id }, - select: ["data"], - }); //User object - let correctpass = true; +router.post( + "/", + route({ + responses: { + 204: {}, + 400: { + body: "APIErrorResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const user = await User.findOneOrFail({ + where: { id: req.user_id }, + select: ["data"], + }); //User object + let correctpass = true; - if (user.data.hash) { - // guest accounts can delete accounts without password - correctpass = await bcrypt.compare(req.body.password, user.data.hash); //Not sure if user typed right password :/ - } + if (user.data.hash) { + // guest accounts can delete accounts without password + correctpass = await bcrypt.compare( + req.body.password, + user.data.hash, + ); //Not sure if user typed right password :/ + } - if (correctpass) { - await User.update({ id: req.user_id }, { disabled: true }); + if (correctpass) { + await User.update({ id: req.user_id }, { disabled: true }); - res.sendStatus(204); - } else { - res.status(400).json({ - message: "Password does not match", - code: 50018, - }); - } -}); + res.sendStatus(204); + } else { + res.status(400).json({ + message: "Password does not match", + code: 50018, + }); + } + }, +); export default router; diff --git a/src/api/routes/users/@me/guilds.ts b/src/api/routes/users/@me/guilds.ts index b16b909d..b5fdca26 100644 --- a/src/api/routes/users/@me/guilds.ts +++ b/src/api/routes/users/@me/guilds.ts @@ -16,79 +16,106 @@ along with this program. If not, see . */ -import { Router, Request, Response } from "express"; +import { route } from "@spacebar/api"; import { + Config, Guild, - Member, - User, GuildDeleteEvent, GuildMemberRemoveEvent, + Member, + User, emitEvent, - Config, } from "@spacebar/util"; +import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server"; -import { route } from "@spacebar/api"; const router: Router = Router(); -router.get("/", route({}), async (req: Request, res: Response) => { - const members = await Member.find({ - relations: ["guild"], - where: { id: req.user_id }, - }); +router.get( + "/", + route({ + responses: { + 200: { + body: "UserGuildsResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const members = await Member.find({ + relations: ["guild"], + where: { id: req.user_id }, + }); - let guild = members.map((x) => x.guild); + let guild = members.map((x) => x.guild); - if ("with_counts" in req.query && req.query.with_counts == "true") { - guild = []; // TODO: Load guilds with user role permissions number - } + if ("with_counts" in req.query && req.query.with_counts == "true") { + guild = []; // TODO: Load guilds with user role permissions number + } - res.json(guild); -}); + res.json(guild); + }, +); // user send to leave a certain guild -router.delete("/:guild_id", route({}), async (req: Request, res: Response) => { - const { autoJoin } = Config.get().guild; - const { guild_id } = req.params; - const guild = await Guild.findOneOrFail({ - where: { id: guild_id }, - select: ["owner_id"], - }); - - if (!guild) throw new HTTPError("Guild doesn't exist", 404); - if (guild.owner_id === req.user_id) - throw new HTTPError("You can't leave your own guild", 400); - if ( - autoJoin.enabled && - autoJoin.guilds.includes(guild_id) && - !autoJoin.canLeave - ) { - throw new HTTPError("You can't leave instance auto join guilds", 400); - } - - await Promise.all([ - Member.delete({ id: req.user_id, guild_id: guild_id }), - emitEvent({ - event: "GUILD_DELETE", - data: { - id: guild_id, +router.delete( + "/:guild_id", + route({ + responses: { + 204: {}, + 400: { + body: "APIErrorResponse", + }, + 404: { + body: "APIErrorResponse", }, - user_id: req.user_id, - } as GuildDeleteEvent), - ]); - - const user = await User.getPublicUser(req.user_id); - - await emitEvent({ - event: "GUILD_MEMBER_REMOVE", - data: { - guild_id: guild_id, - user: user, }, - guild_id: guild_id, - } as GuildMemberRemoveEvent); + }), + async (req: Request, res: Response) => { + const { autoJoin } = Config.get().guild; + const { guild_id } = req.params; + const guild = await Guild.findOneOrFail({ + where: { id: guild_id }, + select: ["owner_id"], + }); - return res.sendStatus(204); -}); + if (!guild) throw new HTTPError("Guild doesn't exist", 404); + if (guild.owner_id === req.user_id) + throw new HTTPError("You can't leave your own guild", 400); + if ( + autoJoin.enabled && + autoJoin.guilds.includes(guild_id) && + !autoJoin.canLeave + ) { + throw new HTTPError( + "You can't leave instance auto join guilds", + 400, + ); + } + + await Promise.all([ + Member.delete({ id: req.user_id, guild_id: guild_id }), + emitEvent({ + event: "GUILD_DELETE", + data: { + id: guild_id, + }, + user_id: req.user_id, + } as GuildDeleteEvent), + ]); + + const user = await User.getPublicUser(req.user_id); + + await emitEvent({ + event: "GUILD_MEMBER_REMOVE", + data: { + guild_id: guild_id, + user: user, + }, + guild_id: guild_id, + } as GuildMemberRemoveEvent); + + return res.sendStatus(204); + }, +); export default router; diff --git a/src/api/routes/users/@me/index.ts b/src/api/routes/users/@me/index.ts index 58697cbf..14feb1b1 100644 --- a/src/api/routes/users/@me/index.ts +++ b/src/api/routes/users/@me/index.ts @@ -34,18 +34,41 @@ import { Request, Response, Router } from "express"; const router: Router = Router(); -router.get("/", route({}), async (req: Request, res: Response) => { - res.json( - await User.findOne({ - select: PrivateUserProjection, - where: { id: req.user_id }, - }), - ); -}); +router.get( + "/", + route({ + responses: { + 200: { + body: "PrivateUserResponse", + }, + }, + }), + async (req: Request, res: Response) => { + res.json( + await User.findOne({ + select: PrivateUserProjection, + where: { id: req.user_id }, + }), + ); + }, +); router.patch( "/", - route({ requestBody: "UserModifySchema" }), + route({ + requestBody: "UserModifySchema", + responses: { + 200: { + body: "UserUpdateResponse", + }, + 400: { + body: "APIErrorResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), async (req: Request, res: Response) => { const body = req.body as UserModifySchema; diff --git a/src/api/routes/users/@me/mfa/codes-verification.ts b/src/api/routes/users/@me/mfa/codes-verification.ts index 7459ede3..7e336e5a 100644 --- a/src/api/routes/users/@me/mfa/codes-verification.ts +++ b/src/api/routes/users/@me/mfa/codes-verification.ts @@ -30,7 +30,20 @@ const router = Router(); router.post( "/", - route({ requestBody: "CodesVerificationSchema" }), + route({ + requestBody: "CodesVerificationSchema", + responses: { + 200: { + body: "UserBackupCodesResponse", + }, + 400: { + body: "APIErrorResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), async (req: Request, res: Response) => { // const { key, nonce, regenerate } = req.body as CodesVerificationSchema; const { regenerate } = req.body as CodesVerificationSchema; diff --git a/src/api/routes/users/@me/mfa/codes.ts b/src/api/routes/users/@me/mfa/codes.ts index 178e25c9..7a60522a 100644 --- a/src/api/routes/users/@me/mfa/codes.ts +++ b/src/api/routes/users/@me/mfa/codes.ts @@ -33,7 +33,23 @@ const router = Router(); router.post( "/", - route({ requestBody: "MfaCodesSchema" }), + route({ + requestBody: "MfaCodesSchema", + deprecated: true, + description: + "This route is replaced with users/@me/mfa/codes-verification in newer clients", + responses: { + 200: { + body: "UserBackupCodesResponse", + }, + 400: { + body: "APIErrorResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), async (req: Request, res: Response) => { const { password, regenerate } = req.body as MfaCodesSchema; diff --git a/src/api/routes/users/@me/notes.ts b/src/api/routes/users/@me/notes.ts index d05c799c..248e61f9 100644 --- a/src/api/routes/users/@me/notes.ts +++ b/src/api/routes/users/@me/notes.ts @@ -16,71 +16,99 @@ along with this program. If not, see . */ -import { Request, Response, Router } from "express"; import { route } from "@spacebar/api"; -import { User, Note, emitEvent, Snowflake } from "@spacebar/util"; +import { Note, Snowflake, User, emitEvent } from "@spacebar/util"; +import { Request, Response, Router } from "express"; const router: Router = Router(); -router.get("/:id", route({}), async (req: Request, res: Response) => { - const { id } = req.params; - - const note = await Note.findOneOrFail({ - where: { - owner: { id: req.user_id }, - target: { id: id }, +router.get( + "/:id", + route({ + responses: { + 200: { + body: "UserNoteResponse", + }, + 404: { + body: "APIErrorResponse", + }, }, - }); + }), + async (req: Request, res: Response) => { + const { id } = req.params; - return res.json({ - note: note?.content, - note_user_id: id, - user_id: req.user_id, - }); -}); + const note = await Note.findOneOrFail({ + where: { + owner: { id: req.user_id }, + target: { id: id }, + }, + }); -router.put("/:id", route({}), async (req: Request, res: Response) => { - const { id } = req.params; - const owner = await User.findOneOrFail({ where: { id: req.user_id } }); - const target = await User.findOneOrFail({ where: { id: id } }); //if noted user does not exist throw - const { note } = req.body; + return res.json({ + note: note?.content, + note_user_id: id, + user_id: req.user_id, + }); + }, +); - if (note && note.length) { - // upsert a note - if ( - await Note.findOne({ - where: { owner: { id: owner.id }, target: { id: target.id } }, - }) - ) { - Note.update( - { owner: { id: owner.id }, target: { id: target.id } }, - { owner, target, content: note }, - ); +router.put( + "/:id", + route({ + requestBody: "UserNoteUpdateSchema", + responses: { + 204: {}, + 404: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const { id } = req.params; + const owner = await User.findOneOrFail({ where: { id: req.user_id } }); + const target = await User.findOneOrFail({ where: { id: id } }); //if noted user does not exist throw + const { note } = req.body; + + if (note && note.length) { + // upsert a note + if ( + await Note.findOne({ + where: { + owner: { id: owner.id }, + target: { id: target.id }, + }, + }) + ) { + Note.update( + { owner: { id: owner.id }, target: { id: target.id } }, + { owner, target, content: note }, + ); + } else { + Note.insert({ + id: Snowflake.generate(), + owner, + target, + content: note, + }); + } } else { - Note.insert({ - id: Snowflake.generate(), - owner, - target, - content: note, + await Note.delete({ + owner: { id: owner.id }, + target: { id: target.id }, }); } - } else { - await Note.delete({ - owner: { id: owner.id }, - target: { id: target.id }, + + await emitEvent({ + event: "USER_NOTE_UPDATE", + data: { + note: note, + id: target.id, + }, + user_id: owner.id, }); - } - await emitEvent({ - event: "USER_NOTE_UPDATE", - data: { - note: note, - id: target.id, - }, - user_id: owner.id, - }); - - return res.status(204); -}); + return res.status(204); + }, +); export default router; diff --git a/src/api/routes/users/@me/relationships.ts b/src/api/routes/users/@me/relationships.ts index 0c32f27c..bce0a654 100644 --- a/src/api/routes/users/@me/relationships.ts +++ b/src/api/routes/users/@me/relationships.ts @@ -38,29 +38,53 @@ const userProjection: (keyof User)[] = [ ...PublicUserProjection, ]; -router.get("/", route({}), async (req: Request, res: Response) => { - const user = await User.findOneOrFail({ - where: { id: req.user_id }, - relations: ["relationships", "relationships.to"], - select: ["id", "relationships"], - }); +router.get( + "/", + route({ + responses: { + 200: { + body: "UserRelationshipsResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const user = await User.findOneOrFail({ + where: { id: req.user_id }, + relations: ["relationships", "relationships.to"], + select: ["id", "relationships"], + }); - //TODO DTO - const related_users = user.relationships.map((r) => { - return { - id: r.to.id, - type: r.type, - nickname: null, - user: r.to.toPublicUser(), - }; - }); + //TODO DTO + const related_users = user.relationships.map((r) => { + return { + id: r.to.id, + type: r.type, + nickname: null, + user: r.to.toPublicUser(), + }; + }); - return res.json(related_users); -}); + return res.json(related_users); + }, +); router.put( "/:id", - route({ requestBody: "RelationshipPutSchema" }), + route({ + requestBody: "RelationshipPutSchema", + responses: { + 204: {}, + 400: { + body: "APIErrorResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), async (req: Request, res: Response) => { return await updateRelationship( req, @@ -77,7 +101,18 @@ router.put( router.post( "/", - route({ requestBody: "RelationshipPostSchema" }), + route({ + requestBody: "RelationshipPostSchema", + responses: { + 204: {}, + 400: { + body: "APIErrorResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), async (req: Request, res: Response) => { return await updateRelationship( req, @@ -98,64 +133,78 @@ router.post( }, ); -router.delete("/:id", route({}), async (req: Request, res: Response) => { - const { id } = req.params; - if (id === req.user_id) - throw new HTTPError("You can't remove yourself as a friend"); +router.delete( + "/:id", + route({ + responses: { + 204: {}, + 400: { + body: "APIErrorResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const { id } = req.params; + if (id === req.user_id) + throw new HTTPError("You can't remove yourself as a friend"); - const user = await User.findOneOrFail({ - where: { id: req.user_id }, - select: userProjection, - relations: ["relationships"], - }); - const friend = await User.findOneOrFail({ - where: { id: id }, - select: userProjection, - relations: ["relationships"], - }); + const user = await User.findOneOrFail({ + where: { id: req.user_id }, + select: userProjection, + relations: ["relationships"], + }); + const friend = await User.findOneOrFail({ + where: { id: id }, + select: userProjection, + relations: ["relationships"], + }); - const relationship = user.relationships.find((x) => x.to_id === id); - const friendRequest = friend.relationships.find( - (x) => x.to_id === req.user_id, - ); + const relationship = user.relationships.find((x) => x.to_id === id); + const friendRequest = friend.relationships.find( + (x) => x.to_id === req.user_id, + ); - if (!relationship) - throw new HTTPError("You are not friends with the user", 404); - if (relationship?.type === RelationshipType.blocked) { - // unblock user + if (!relationship) + throw new HTTPError("You are not friends with the user", 404); + if (relationship?.type === RelationshipType.blocked) { + // unblock user + + await Promise.all([ + Relationship.delete({ id: relationship.id }), + emitEvent({ + event: "RELATIONSHIP_REMOVE", + user_id: req.user_id, + data: relationship.toPublicRelationship(), + } as RelationshipRemoveEvent), + ]); + return res.sendStatus(204); + } + if (friendRequest && friendRequest.type !== RelationshipType.blocked) { + await Promise.all([ + Relationship.delete({ id: friendRequest.id }), + await emitEvent({ + event: "RELATIONSHIP_REMOVE", + data: friendRequest.toPublicRelationship(), + user_id: id, + } as RelationshipRemoveEvent), + ]); + } await Promise.all([ Relationship.delete({ id: relationship.id }), emitEvent({ event: "RELATIONSHIP_REMOVE", - user_id: req.user_id, data: relationship.toPublicRelationship(), + user_id: req.user_id, } as RelationshipRemoveEvent), ]); + return res.sendStatus(204); - } - if (friendRequest && friendRequest.type !== RelationshipType.blocked) { - await Promise.all([ - Relationship.delete({ id: friendRequest.id }), - await emitEvent({ - event: "RELATIONSHIP_REMOVE", - data: friendRequest.toPublicRelationship(), - user_id: id, - } as RelationshipRemoveEvent), - ]); - } - - await Promise.all([ - Relationship.delete({ id: relationship.id }), - emitEvent({ - event: "RELATIONSHIP_REMOVE", - data: relationship.toPublicRelationship(), - user_id: req.user_id, - } as RelationshipRemoveEvent), - ]); - - return res.sendStatus(204); -}); + }, +); export default router; diff --git a/src/api/routes/users/@me/settings.ts b/src/api/routes/users/@me/settings.ts index 9ea4e673..d22d6de1 100644 --- a/src/api/routes/users/@me/settings.ts +++ b/src/api/routes/users/@me/settings.ts @@ -22,17 +22,43 @@ import { Request, Response, Router } from "express"; const router = Router(); -router.get("/", route({}), async (req: Request, res: Response) => { - const user = await User.findOneOrFail({ - where: { id: req.user_id }, - relations: ["settings"], - }); - return res.json(user.settings); -}); +router.get( + "/", + route({ + responses: { + 200: { + body: "UserSettings", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const user = await User.findOneOrFail({ + where: { id: req.user_id }, + relations: ["settings"], + }); + return res.json(user.settings); + }, +); router.patch( "/", - route({ requestBody: "UserSettingsSchema" }), + route({ + requestBody: "UserSettingsSchema", + responses: { + 200: { + body: "UserSettings", + }, + 400: { + body: "APIErrorResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), async (req: Request, res: Response) => { const body = req.body as UserSettingsSchema; if (body.locale === "en") body.locale = "en-US"; // fix discord client crash on unkown locale diff --git a/src/api/util/handlers/route.ts b/src/api/util/handlers/route.ts index 2416b73f..5a0b48e6 100644 --- a/src/api/util/handlers/route.ts +++ b/src/api/util/handlers/route.ts @@ -70,6 +70,7 @@ export interface RouteOptions { values?: string[]; }; }; + deprecated?: boolean; // test?: { // response?: RouteResponse; // body?: unknown; diff --git a/src/util/entities/User.ts b/src/util/entities/User.ts index df9af328..85a5015b 100644 --- a/src/util/entities/User.ts +++ b/src/util/entities/User.ts @@ -86,8 +86,7 @@ export const PrivateUserProjection = [ // Private user data that should never get sent to the client export type PublicUser = Pick; - -export type UserPublic = Pick; +export type PrivateUser = Pick; export interface UserPrivate extends Pick { locale: string; diff --git a/src/util/schemas/UserNoteUpdateSchema.ts b/src/util/schemas/UserNoteUpdateSchema.ts new file mode 100644 index 00000000..0a731279 --- /dev/null +++ b/src/util/schemas/UserNoteUpdateSchema.ts @@ -0,0 +1,3 @@ +export interface UserNoteUpdateSchema { + note: string; +} diff --git a/src/util/schemas/UserProfileResponse.ts b/src/util/schemas/UserProfileResponse.ts index 699d6a29..4ef6431e 100644 --- a/src/util/schemas/UserProfileResponse.ts +++ b/src/util/schemas/UserProfileResponse.ts @@ -16,10 +16,10 @@ along with this program. If not, see . */ -import { PublicConnectedAccount, UserPublic } from ".."; +import { PublicConnectedAccount, PublicUserResponse } from ".."; export interface UserProfileResponse { - user: UserPublic; + user: PublicUserResponse; connected_accounts: PublicConnectedAccount; premium_guild_since?: Date; premium_since?: Date; diff --git a/src/util/schemas/index.ts b/src/util/schemas/index.ts index 23d03190..44a504cd 100644 --- a/src/util/schemas/index.ts +++ b/src/util/schemas/index.ts @@ -69,6 +69,7 @@ export * from "./TotpSchema"; export * from "./UserDeleteSchema"; export * from "./UserGuildSettingsSchema"; export * from "./UserModifySchema"; +export * from "./UserNoteUpdateSchema"; export * from "./UserProfileModifySchema"; export * from "./UserSettingsSchema"; export * from "./Validator"; diff --git a/src/util/schemas/responses/UserNoteResponse.ts b/src/util/schemas/responses/UserNoteResponse.ts new file mode 100644 index 00000000..b142811e --- /dev/null +++ b/src/util/schemas/responses/UserNoteResponse.ts @@ -0,0 +1,5 @@ +export interface UserNoteResponse { + note: string; + note_user_id: string; + user_id: string; +} diff --git a/src/util/schemas/responses/UserProfileResponse.ts b/src/util/schemas/responses/UserProfileResponse.ts index 4e5cd8a6..bd1f46dd 100644 --- a/src/util/schemas/responses/UserProfileResponse.ts +++ b/src/util/schemas/responses/UserProfileResponse.ts @@ -1,7 +1,7 @@ -import { PublicConnectedAccount, UserPublic } from "../../entities"; +import { PublicConnectedAccount, PublicUser } from "../../entities"; export interface UserProfileResponse { - user: UserPublic; + user: PublicUser; connected_accounts: PublicConnectedAccount; premium_guild_since?: Date; premium_since?: Date; diff --git a/src/util/schemas/responses/UserRelationshipsResponse.ts b/src/util/schemas/responses/UserRelationshipsResponse.ts new file mode 100644 index 00000000..dff2f118 --- /dev/null +++ b/src/util/schemas/responses/UserRelationshipsResponse.ts @@ -0,0 +1,8 @@ +import { PublicUser, RelationshipType } from "../../entities"; + +export interface UserRelationshipsResponse { + id: string; + type: RelationshipType; + nickname: null; + user: PublicUser; +} diff --git a/src/util/schemas/responses/UserResponse.ts b/src/util/schemas/responses/UserResponse.ts new file mode 100644 index 00000000..21c30cd5 --- /dev/null +++ b/src/util/schemas/responses/UserResponse.ts @@ -0,0 +1,22 @@ +import { DmChannelDTO } from "../../dtos"; +import { Guild, PrivateUser, PublicUser, User } from "../../entities"; + +export type PublicUserResponse = PublicUser; +export type PrivateUserResponse = PrivateUser; + +export interface UserUpdateResponse extends PrivateUserResponse { + newToken?: string; +} + +export type UserGuildsResponse = Guild[]; + +export type UserChannelsResponse = DmChannelDTO[]; + +export interface UserBackupCodesResponse { + expired: unknown; + user: User; + code: string; + consumed: boolean; + id: string; +} +[]; diff --git a/src/util/schemas/responses/index.ts b/src/util/schemas/responses/index.ts index 3f29b779..1f0e2aed 100644 --- a/src/util/schemas/responses/index.ts +++ b/src/util/schemas/responses/index.ts @@ -39,6 +39,9 @@ export * from "./OAuthAuthorizeResponse"; export * from "./StickerPacksResponse"; export * from "./Tenor"; export * from "./TokenResponse"; +export * from "./UserNoteResponse"; export * from "./UserProfileResponse"; +export * from "./UserRelationshipsResponse"; export * from "./UserRelationsResponse"; +export * from "./UserResponse"; export * from "./WebhookCreateResponse";