From 8cdb9dea30159b02b19fe9633125b36d8fa9f9a0 Mon Sep 17 00:00:00 2001 From: Isaac Date: Wed, 17 Apr 2024 23:31:38 +0100 Subject: [PATCH 1/7] remove discriminator from session --- src/routes/auth/callback.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/routes/auth/callback.js b/src/routes/auth/callback.js index 6205c8121..59821abb4 100644 --- a/src/routes/auth/callback.js +++ b/src/routes/auth/callback.js @@ -19,7 +19,6 @@ module.exports.get = () => ({ const token = this.jwt.sign({ accessToken: data.access_token, avatar: user.avatar, - discriminator: user.discriminator, expiresAt: Date.now() + (data.expires_in * 1000), id: user.id, locale: user.locale, From c6a982e702e37119071bd6b13e9ddb48f9f16599 Mon Sep 17 00:00:00 2001 From: Isaac Date: Sun, 21 Apr 2024 02:03:56 +0100 Subject: [PATCH 2/7] fix: token samesite=secure --- src/routes/auth/callback.js | 11 +++++++++-- src/routes/auth/logout.js | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/routes/auth/callback.js b/src/routes/auth/callback.js index 59821abb4..361a317c1 100644 --- a/src/routes/auth/callback.js +++ b/src/routes/auth/callback.js @@ -29,9 +29,16 @@ module.exports.get = () => ({ httpOnly: true, maxAge: data.expires_in, path: '/', - sameSite: 'Lax', + sameSite: 'Strict', secure: false, }); - return res.redirect(303, redirect); + res.header('Content-Type', 'text/html'); + return res.send(` + + + + + +`); }, }); diff --git a/src/routes/auth/logout.js b/src/routes/auth/logout.js index bf5908c4b..63996165d 100644 --- a/src/routes/auth/logout.js +++ b/src/routes/auth/logout.js @@ -18,7 +18,7 @@ module.exports.get = fastify => ({ domain, httpOnly: true, path: '/', - sameSite: 'Lax', + sameSite: 'Strict', secure: false, }).send('The token has been revoked.'); }, From 46bd58daf626c6df1e5600e017f6df8428026c52 Mon Sep 17 00:00:00 2001 From: Isaac Date: Sun, 21 Apr 2024 02:04:46 +0100 Subject: [PATCH 3/7] feat(api): generate missing icons --- src/lib/misc.js | 3 +++ src/routes/api/admin/guilds/[guild]/index.js | 3 ++- src/routes/api/admin/guilds/index.js | 10 +++++++++- src/routes/api/guilds/[guild]/index.js | 16 ++++++++++++++++ src/routes/api/guilds/index.js | 18 ++++++++++++++++++ 5 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 src/routes/api/guilds/[guild]/index.js create mode 100644 src/routes/api/guilds/index.js diff --git a/src/lib/misc.js b/src/lib/misc.js index 35885e619..3f2330131 100644 --- a/src/lib/misc.js +++ b/src/lib/misc.js @@ -1,3 +1,6 @@ const { createHash } = require('crypto'); module.exports.md5 = str => createHash('md5').update(str).digest('hex'); module.exports.msToMins = ms => Number((ms / 1000 / 60).toFixed(2)); +module.exports.iconURL = guildLike => guildLike.icon + ? guildLike.client.rest.cdn.icon(guildLike.id, guildLike.icon) + : `https://api.dicebear.com/8.x/initials/png?seed=${encodeURIComponent(guildLike.name)}&size=96&backgroundType=gradientLinear&fontWeight=600`; diff --git a/src/routes/api/admin/guilds/[guild]/index.js b/src/routes/api/admin/guilds/[guild]/index.js index 39e49b96c..06f4c65b9 100644 --- a/src/routes/api/admin/guilds/[guild]/index.js +++ b/src/routes/api/admin/guilds/[guild]/index.js @@ -1,4 +1,5 @@ /* eslint-disable no-underscore-dangle */ +const { iconURL } = require('../../../../../lib/misc'); const { getAvgResolutionTime, getAvgResponseTime, @@ -37,7 +38,7 @@ module.exports.get = fastify => ({ cached = { createdAt: settings.createdAt, id: guild.id, - logo: guild.iconURL(), + logo: iconURL(guild), name: guild.name, stats: { avgResolutionTime: ms(getAvgResolutionTime(closedTickets)), diff --git a/src/routes/api/admin/guilds/index.js b/src/routes/api/admin/guilds/index.js index c63544050..b4779ab60 100644 --- a/src/routes/api/admin/guilds/index.js +++ b/src/routes/api/admin/guilds/index.js @@ -1,4 +1,5 @@ const { PermissionsBitField } = require('discord.js'); +const { iconURL } = require('../../../../lib/misc'); module.exports.get = fastify => ({ handler: async (req, res) => { @@ -10,7 +11,14 @@ module.exports.get = fastify => ({ .map(guild => ({ added: client.guilds.cache.has(guild.id), id: guild.id, - logo: `https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.webp`, + logo: iconURL( + client.guilds.cache.get(guild.id) || + { + client, + icon: guild.icon, + id: guild.id, + }, + ), name: guild.name, })), ); diff --git a/src/routes/api/guilds/[guild]/index.js b/src/routes/api/guilds/[guild]/index.js new file mode 100644 index 000000000..caff864cc --- /dev/null +++ b/src/routes/api/guilds/[guild]/index.js @@ -0,0 +1,16 @@ +const { iconURL } = require('../../../../lib/misc'); + +module.exports.get = fastify => ({ + handler: async (req, res) => { + const { client } = req.routeOptions.config; + const guild = client.guilds.cache.get(req.params.guild); + if (!guild) return res.status(404).send(new Error('Not Found')); + res.send({ + id: guild.id, + logo: iconURL(guild), + name: guild.name, + }); + }, + onRequest: [fastify.authenticate], +}); + diff --git a/src/routes/api/guilds/index.js b/src/routes/api/guilds/index.js new file mode 100644 index 000000000..e608167e5 --- /dev/null +++ b/src/routes/api/guilds/index.js @@ -0,0 +1,18 @@ +const { iconURL } = require('../../../lib/misc'); + +module.exports.get = fastify => ({ + handler: async (req, res) => { + const { client } = req.routeOptions.config; + const guilds = await (await fetch('https://discordapp.com/api/users/@me/guilds', { headers: { 'Authorization': `Bearer ${req.user.accessToken}` } })).json(); + res.send( + guilds + .filter(guild => client.guilds.cache.has(guild.id)) + .map(guild => ({ + id: guild.id, + logo: iconURL(client.guilds.cache.get(guild.id)), + name: guild.name, + })), + ); + }, + onRequest: [fastify.authenticate], +}); From 130f5dc590f0856982b440f58bcad5161be0699f Mon Sep 17 00:00:00 2001 From: Isaac Date: Mon, 22 Apr 2024 03:03:33 +0100 Subject: [PATCH 4/7] feat(api): privilege levels --- src/http.js | 31 ++++++++++++++++++++++++-- src/lib/users.js | 20 +++++++++++++++++ src/routes/api/guilds/[guild]/index.js | 4 +++- src/routes/api/guilds/index.js | 21 +++++++++++------ 4 files changed, 66 insertions(+), 10 deletions(-) diff --git a/src/http.js b/src/http.js index b36869449..688795f13 100644 --- a/src/http.js +++ b/src/http.js @@ -4,7 +4,7 @@ const { randomBytes } = require('crypto'); const { short } = require('leeks.js'); const { join } = require('path'); const { files } = require('node-dir'); -const { PermissionsBitField } = require('discord.js'); +const { getPrivilegeLevel } = require('./lib/users'); process.env.ORIGIN = process.env.HTTP_INTERNAL || process.env.HTTP_EXTERNAL; @@ -65,6 +65,33 @@ module.exports = async client => { } }); + fastify.decorate('isMember', async (req, res) => { + try { + const userId = req.user.id; + const guildId = req.params.guild; + const guild = client.guilds.cache.get(guildId); + if (!guild) { + return res.code(404).send({ + error: 'Not Found', + message: 'The requested resource could not be found.', + statusCode: 404, + + }); + } + const guildMember = await guild.members.fetch(userId); + if (!guildMember) { + return res.code(403).send({ + error: 'Forbidden', + message: 'You are not permitted for this action.', + statusCode: 403, + + }); + } + } catch (err) { + res.send(err); + } + }); + fastify.decorate('isAdmin', async (req, res) => { try { const userId = req.user.id; @@ -79,7 +106,7 @@ module.exports = async client => { }); } const guildMember = await guild.members.fetch(userId); - const isAdmin = guildMember?.permissions.has(PermissionsBitField.Flags.ManageGuild) || client.supers.includes(userId); + const isAdmin = await getPrivilegeLevel(guildMember) >= 2; if (!isAdmin) { return res.code(403).send({ error: 'Forbidden', diff --git a/src/lib/users.js b/src/lib/users.js index c263662f1..1a2ec2b67 100644 --- a/src/lib/users.js +++ b/src/lib/users.js @@ -46,3 +46,23 @@ module.exports.isStaff = async (guild, userId) => { const staffRoles = await client.keyv.get(`cache/guild-staff:${guild.id}`) || await updateStaffRoles(guild); return staffRoles.some(r => guildMember.roles.cache.has(r)); }; + +/** + * + * @param {import("discord.js")} member + * @returns {Promise} + * - `4` = OPERATOR (SUPER) + * - `3` = GUILD_OWNER + * - `2` = GUILD_ADMIN + * - `1` = GUILD_STAFF + * - `0` = GUILD_MEMBER + * - `-1` = NONE (NOT A MEMBER) + */ +module.exports.getPrivilegeLevel = async member => { + if (!member) return -1; + else if (member.guild.client.supers.includes(member.id)) return 4; + else if (member.guild.ownerId === member.id) return 3; + else if (member.permissions.has(PermissionsBitField.Flags.ManageGuild)) return 2; + else if (await this.isStaff(member.guild, member.id)) return 1; + else return 0; +}; diff --git a/src/routes/api/guilds/[guild]/index.js b/src/routes/api/guilds/[guild]/index.js index caff864cc..eb854d5f2 100644 --- a/src/routes/api/guilds/[guild]/index.js +++ b/src/routes/api/guilds/[guild]/index.js @@ -1,3 +1,4 @@ +const { getPrivilegeLevel } = require('../../../../lib/users'); const { iconURL } = require('../../../../lib/misc'); module.exports.get = fastify => ({ @@ -9,8 +10,9 @@ module.exports.get = fastify => ({ id: guild.id, logo: iconURL(guild), name: guild.name, + privilegeLevel: await getPrivilegeLevel(await guild.members.fetch(req.user.id)), }); }, - onRequest: [fastify.authenticate], + onRequest: [fastify.authenticate, fastify.isMember], }); diff --git a/src/routes/api/guilds/index.js b/src/routes/api/guilds/index.js index e608167e5..a8931bb26 100644 --- a/src/routes/api/guilds/index.js +++ b/src/routes/api/guilds/index.js @@ -1,3 +1,4 @@ +const { getPrivilegeLevel } = require('../../../lib/users'); const { iconURL } = require('../../../lib/misc'); module.exports.get = fastify => ({ @@ -5,13 +6,19 @@ module.exports.get = fastify => ({ const { client } = req.routeOptions.config; const guilds = await (await fetch('https://discordapp.com/api/users/@me/guilds', { headers: { 'Authorization': `Bearer ${req.user.accessToken}` } })).json(); res.send( - guilds - .filter(guild => client.guilds.cache.has(guild.id)) - .map(guild => ({ - id: guild.id, - logo: iconURL(client.guilds.cache.get(guild.id)), - name: guild.name, - })), + await Promise.all( + guilds + .filter(partialGuild => client.guilds.cache.has(partialGuild.id)) + .map(async partialGuild => { + const guild = client.guilds.cache.get(partialGuild.id); + return { + id: guild.id, + logo: iconURL(guild), + name: guild.name, + privilegeLevel: await getPrivilegeLevel(await guild.members.fetch(req.user.id)), + }; + }), + ), ); }, onRequest: [fastify.authenticate], From 8818bf6d48df8a80faae84418cf79dc3903357de Mon Sep 17 00:00:00 2001 From: Isaac Date: Fri, 26 Apr 2024 00:13:12 +0100 Subject: [PATCH 5/7] fix(api): use more appropriate status code --- .../api/admin/guilds/[guild]/categories/[category]/index.js | 6 +++--- .../[guild]/categories/[category]/questions/[question].js | 2 +- src/routes/api/admin/guilds/[guild]/tags/[tag].js | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/routes/api/admin/guilds/[guild]/categories/[category]/index.js b/src/routes/api/admin/guilds/[guild]/categories/[category]/index.js index 4ed51f245..7cb8bfe3e 100644 --- a/src/routes/api/admin/guilds/[guild]/categories/[category]/index.js +++ b/src/routes/api/admin/guilds/[guild]/categories/[category]/index.js @@ -9,7 +9,7 @@ module.exports.delete = fastify => ({ const guild = client.guilds.cache.get(req.params.guild); const categoryId = Number(req.params.category); const original = categoryId && await client.prisma.category.findUnique({ where: { id: categoryId } }); - if (!original || original.guildId !== guild.id) return res.status(404).send(new Error('Not Found')); + if (!original || original.guildId !== guild.id) return res.status(400).send(new Error('Bad Request')); const category = await client.prisma.category.delete({ where: { id: categoryId } }); await updateStaffRoles(guild); @@ -58,7 +58,7 @@ module.exports.get = fastify => ({ where: { id: categoryId }, }); - if (!category || category.guildId !== guildId) return res.status(404).send(new Error('Not Found')); + if (!category || category.guildId !== guildId) return res.status(400).send(new Error('Bad Request')); return category; }, @@ -118,7 +118,7 @@ module.exports.patch = fastify => ({ where: { id: categoryId }, }); - if (!original || original.guildId !== guildId) return res.status(404).send(new Error('Not Found')); + if (!original || original.guildId !== guildId) return res.status(400).send(new Error('Bad Request')); if (data.hasOwnProperty('id')) delete data.id; if (data.hasOwnProperty('createdAt')) delete data.createdAt; diff --git a/src/routes/api/admin/guilds/[guild]/categories/[category]/questions/[question].js b/src/routes/api/admin/guilds/[guild]/categories/[category]/questions/[question].js index 653374050..20973b508 100644 --- a/src/routes/api/admin/guilds/[guild]/categories/[category]/questions/[question].js +++ b/src/routes/api/admin/guilds/[guild]/categories/[category]/questions/[question].js @@ -9,7 +9,7 @@ module.exports.delete = fastify => ({ const questionId = req.params.question; const original = questionId && await client.prisma.question.findUnique({ where: { id: questionId } }); const category = categoryId && await client.prisma.category.findUnique({ where: { id: categoryId } }); - if (original?.categoryId !== categoryId || category.guildId !== guildId) return res.status(404).send(new Error('Not Found')); + if (original?.categoryId !== categoryId || category.guildId !== guildId) return res.status(400).send(new Error('Bad Request')); const question = await client.prisma.question.delete({ where: { id: questionId } }); logAdminEvent(client, { diff --git a/src/routes/api/admin/guilds/[guild]/tags/[tag].js b/src/routes/api/admin/guilds/[guild]/tags/[tag].js index ed2b69289..6225a7ab0 100644 --- a/src/routes/api/admin/guilds/[guild]/tags/[tag].js +++ b/src/routes/api/admin/guilds/[guild]/tags/[tag].js @@ -8,7 +8,7 @@ module.exports.delete = fastify => ({ const guildId = req.params.guild; const tagId = Number(req.params.tag); const original = tagId && await client.prisma.tag.findUnique({ where: { id: tagId } }); - if (original.guildId !== guildId) return res.status(404).send(new Error('Not Found')); + if (original.guildId !== guildId) return res.status(400).send(new Error('Bad Request')); const tag = await client.prisma.tag.delete({ where: { id: tagId } }); const cacheKey = `cache/guild-tags:${guildId}`; @@ -46,7 +46,7 @@ module.exports.get = fastify => ({ const tagId = Number(req.params.tag); const tag = await client.prisma.tag.findUnique({ where: { id: tagId } }); - if (!tag || tag.guildId !== guildId) return res.status(404).send(new Error('Not Found')); + if (!tag || tag.guildId !== guildId) return res.status(400).send(new Error('Bad Request')); return tag; }, @@ -64,7 +64,7 @@ module.exports.patch = fastify => ({ const original = req.params.tag && await client.prisma.tag.findUnique({ where: { id: tagId } }); - if (!original || original.guildId !== guildId) return res.status(404).send(new Error('Not Found')); + if (!original || original.guildId !== guildId) return res.status(400).send(new Error('Bad Request')); if (data.hasOwnProperty('id')) delete data.id; if (data.hasOwnProperty('createdAt')) delete data.createdAt; From 4d42269a35b485e04a03df85e7c61a7d5e1282ef Mon Sep 17 00:00:00 2001 From: Isaac Date: Wed, 3 Jul 2024 01:39:08 +0100 Subject: [PATCH 6/7] feat(api): redirect on logout --- src/routes/auth/logout.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/routes/auth/logout.js b/src/routes/auth/logout.js index 63996165d..65a19c6b7 100644 --- a/src/routes/auth/logout.js +++ b/src/routes/auth/logout.js @@ -20,7 +20,15 @@ module.exports.get = fastify => ({ path: '/', sameSite: 'Strict', secure: false, - }).send('The token has been revoked.'); + }); + res.header('Content-Type', 'text/html'); + return res.send(` + + + + + +`); }, onRequest: [fastify.authenticate], }); From b00d2f312eb0e3353213a1ebdcf28002342640ec Mon Sep 17 00:00:00 2001 From: Isaac Date: Wed, 3 Jul 2024 02:03:16 +0100 Subject: [PATCH 7/7] feat(api): something --- src/routes/api/guilds/[guild]/index.js | 1 - src/routes/api/guilds/[guild]/tickets/@me.js | 18 ++++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 src/routes/api/guilds/[guild]/tickets/@me.js diff --git a/src/routes/api/guilds/[guild]/index.js b/src/routes/api/guilds/[guild]/index.js index eb854d5f2..e3fe35c62 100644 --- a/src/routes/api/guilds/[guild]/index.js +++ b/src/routes/api/guilds/[guild]/index.js @@ -5,7 +5,6 @@ module.exports.get = fastify => ({ handler: async (req, res) => { const { client } = req.routeOptions.config; const guild = client.guilds.cache.get(req.params.guild); - if (!guild) return res.status(404).send(new Error('Not Found')); res.send({ id: guild.id, logo: iconURL(guild), diff --git a/src/routes/api/guilds/[guild]/tickets/@me.js b/src/routes/api/guilds/[guild]/tickets/@me.js new file mode 100644 index 000000000..d80e07846 --- /dev/null +++ b/src/routes/api/guilds/[guild]/tickets/@me.js @@ -0,0 +1,18 @@ +module.exports.get = fastify => ({ + handler: async (req, res) => { + const { client } = req.routeOptions.config; + /** @type {import("@prisma/client").PrismaClient} */ + const prisma = client.prisma; + const guild = client.guilds.cache.get(req.params.guild); + res.send( + await prisma.ticket.findMany({ + where: { + createdById: req.user.id, + guildId: guild.id, + }, + }), + ); + }, + onRequest: [fastify.authenticate, fastify.isMember], +}); +