diff --git a/package.json b/package.json index 2aa597c..4d16c5e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "discord-tickets", - "version": "4.0.0-beta.7", + "version": "4.0.0-beta.8", "private": "true", "description": "An open-source Discord bot for ticket management", "main": "src/", @@ -40,7 +40,7 @@ "node": ">=18" }, "dependencies": { - "@discord-tickets/settings": "^2.0.0", + "@discord-tickets/settings": "^2.1.0", "@eartharoid/dbf": "^0.3.3", "@eartharoid/dtf": "^2.0.1", "@eartharoid/i18n": "^1.2.1", @@ -50,7 +50,7 @@ "@prisma/client": "^4.11.0", "boxen": "^7.0.2", "cryptr": "^6.2.0", - "discord.js": "^14.7.1", + "discord.js": "^14.8.0", "dotenv": "^16.0.3", "fastify": "^4.14.1", "figlet": "^1.5.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a5d1c6a..d022fc8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3,7 +3,7 @@ lockfileVersion: 5.4 specifiers: '@commitlint/cli': ^17.4.4 '@commitlint/config-conventional': ^17.4.4 - '@discord-tickets/settings': ^2.0.0 + '@discord-tickets/settings': ^2.1.0 '@eartharoid/dbf': ^0.3.3 '@eartharoid/dtf': ^2.0.1 '@eartharoid/i18n': ^1.2.1 @@ -16,7 +16,7 @@ specifiers: bufferutil: ^4.0.7 conventional-changelog-cli: ^2.2.2 cryptr: ^6.2.0 - discord.js: ^14.7.1 + discord.js: ^14.8.0 dotenv: ^16.0.3 erlpack: github:discord/erlpack eslint: ^8.36.0 @@ -43,7 +43,7 @@ specifiers: zlib-sync: ^0.1.8 dependencies: - '@discord-tickets/settings': 2.0.0_svelte@3.56.0 + '@discord-tickets/settings': 2.1.0_svelte@3.56.0 '@eartharoid/dbf': 0.3.3_3cxu5zja4e2r5wmvge7mdcljwq '@eartharoid/dtf': 2.0.1 '@eartharoid/i18n': 1.2.1 @@ -53,7 +53,7 @@ dependencies: '@prisma/client': 4.11.0_prisma@4.11.0 boxen: 7.0.2 cryptr: 6.2.0 - discord.js: 14.7.1_3cxu5zja4e2r5wmvge7mdcljwq + discord.js: 14.8.0_3cxu5zja4e2r5wmvge7mdcljwq dotenv: 16.0.3 fastify: 4.14.1 figlet: 1.5.2 @@ -293,8 +293,8 @@ packages: '@jridgewell/trace-mapping': 0.3.9 dev: true - /@discord-tickets/settings/2.0.0_svelte@3.56.0: - resolution: {integrity: sha512-oVG8qfX21cmufi/jyJV4LrbzsZK86KUmFfgb5W39LCD/+zIg1+l8XrOfkONjwlbFUKbjtV0CV2xgx9Ns+0Wg2w==} + /@discord-tickets/settings/2.1.0_svelte@3.56.0: + resolution: {integrity: sha512-eWs4hQ/5VtbMsPcs/jOHwrPBOB5XBKuGYY0xILUqYFINa5oJsHIzHEjHjEIDcxW2xUJ3g92soBe5GVM3PjkfMA==} dependencies: '@fortawesome/fontawesome-free': 6.3.0 '@skyra/discord-components-core': 3.6.0 @@ -310,11 +310,12 @@ packages: - svelte dev: false - /@discordjs/builders/1.4.0: - resolution: {integrity: sha512-nEeTCheTTDw5kO93faM1j8ZJPonAX86qpq/QVoznnSa8WWcCgJpjlu6GylfINTDW6o7zZY0my2SYdxx2mfNwGA==} + /@discordjs/builders/1.5.0: + resolution: {integrity: sha512-7XxT78mnNBPigHn2y6KAXkicxIBFtZREGWaRZ249EC1l6gBUEP8IyVY5JTciIjJArxkF+tg675aZvsTNTKBpmA==} engines: {node: '>=16.9.0'} dependencies: - '@discordjs/util': 0.1.0 + '@discordjs/formatters': 0.2.0 + '@discordjs/util': 0.2.0 '@sapphire/shapeshift': 3.8.1 discord-api-types: 0.37.35 fast-deep-equal: 3.1.3 @@ -322,17 +323,24 @@ packages: tslib: 2.5.0 dev: false - /@discordjs/collection/1.3.0: - resolution: {integrity: sha512-ylt2NyZ77bJbRij4h9u/wVy7qYw/aDqQLWnadjvDqW/WoWCxrsX6M3CIw9GVP5xcGCDxsrKj5e0r5evuFYwrKg==} + /@discordjs/collection/1.4.0: + resolution: {integrity: sha512-hiOJyk2CPFf1+FL3a4VKCuu1f448LlROVuu8nLz1+jCOAPokUcdFAV+l4pd3B3h6uJlJQSASoZzrdyNdjdtfzQ==} engines: {node: '>=16.9.0'} dev: false - /@discordjs/rest/1.5.0: - resolution: {integrity: sha512-lXgNFqHnbmzp5u81W0+frdXN6Etf4EUi8FAPcWpSykKd8hmlWh1xy6BmE0bsJypU1pxohaA8lQCgp70NUI3uzA==} + /@discordjs/formatters/0.2.0: + resolution: {integrity: sha512-vn4oMSXuMZUm8ITqVOtvE7/fMMISj4cI5oLsR09PEQXHKeKDAMLltG/DWeeIs7Idfy6V8Fk3rn1e69h7NfzuNA==} engines: {node: '>=16.9.0'} dependencies: - '@discordjs/collection': 1.3.0 - '@discordjs/util': 0.1.0 + discord-api-types: 0.37.35 + dev: false + + /@discordjs/rest/1.6.0: + resolution: {integrity: sha512-HGvqNCZ5Z5j0tQHjmT1lFvE5ETO4hvomJ1r0cbnpC1zM23XhCpZ9wgTCiEmaxKz05cyf2CI9p39+9LL+6Yz1bA==} + engines: {node: '>=16.9.0'} + dependencies: + '@discordjs/collection': 1.4.0 + '@discordjs/util': 0.2.0 '@sapphire/async-queue': 1.5.0 '@sapphire/snowflake': 3.4.0 discord-api-types: 0.37.35 @@ -341,15 +349,15 @@ packages: undici: 5.20.0 dev: false - /@discordjs/util/0.1.0: - resolution: {integrity: sha512-e7d+PaTLVQav6rOc2tojh2y6FE8S7REkqLldq1XF4soCx74XB/DIjbVbVLtBemf0nLW77ntz0v+o5DytKwFNLQ==} + /@discordjs/util/0.2.0: + resolution: {integrity: sha512-/8qNbebFzLWKOOg+UV+RB8itp4SmU5jw0tBUD3ifElW6rYNOj1Ku5JaSW7lLl/WgjjxF01l/1uQPCzkwr110vg==} engines: {node: '>=16.9.0'} dev: false /@eartharoid/dbf/0.3.3_3cxu5zja4e2r5wmvge7mdcljwq: resolution: {integrity: sha512-eVDdpFlDV5CAvqoV5g1iAvoYhPjnvcyJ0Nnepc1YihlE1KIYGhVIK/2RaDsltzxRuiweO3Y7dvDj/cUpJnnFPA==} dependencies: - discord.js: 14.7.1_3cxu5zja4e2r5wmvge7mdcljwq + discord.js: 14.8.0_3cxu5zja4e2r5wmvge7mdcljwq node-dir: 0.1.17 transitivePeerDependencies: - bufferutil @@ -1419,14 +1427,15 @@ packages: resolution: {integrity: sha512-iyKZ/82k7FX3lcmHiAvvWu5TmyfVo78RtghBV/YsehK6CID83k5SI03DKKopBcln+TiEIYw5MGgq7SJXSpNzMg==} dev: false - /discord.js/14.7.1_3cxu5zja4e2r5wmvge7mdcljwq: - resolution: {integrity: sha512-1FECvqJJjjeYcjSm0IGMnPxLqja/pmG1B0W2l3lUY2Gi4KXiyTeQmU1IxWcbXHn2k+ytP587mMWqva2IA87EbA==} + /discord.js/14.8.0_3cxu5zja4e2r5wmvge7mdcljwq: + resolution: {integrity: sha512-UOxYtc/YnV7jAJ2gISluJyYeBw4e+j8gWn+IoqG8unaHAVuvZ13DdYN0M1f9fbUgUvSarV798inIrYFtDNDjwQ==} engines: {node: '>=16.9.0'} dependencies: - '@discordjs/builders': 1.4.0 - '@discordjs/collection': 1.3.0 - '@discordjs/rest': 1.5.0 - '@discordjs/util': 0.1.0 + '@discordjs/builders': 1.5.0 + '@discordjs/collection': 1.4.0 + '@discordjs/formatters': 0.2.0 + '@discordjs/rest': 1.6.0 + '@discordjs/util': 0.2.0 '@sapphire/snowflake': 3.4.0 '@types/ws': 8.5.4 discord-api-types: 0.37.35 @@ -1646,8 +1655,8 @@ packages: strip-final-newline: 2.0.0 dev: true - /execa/7.0.0: - resolution: {integrity: sha512-tQbH0pH/8LHTnwTrsKWideqi6rFB/QNUawEwrn+WHyz7PX1Tuz2u7wfTvbaNBdP5JD5LVWxNo8/A8CHNZ3bV6g==} + /execa/7.1.0: + resolution: {integrity: sha512-T6nIJO3LHxUZ6ahVRaxXz9WLEruXLqdcluA+UuTptXmLM7nDAn9lx9IfkxPyzEL21583qSt4RmL44pO71EHaJQ==} engines: {node: ^14.18.0 || ^16.14.0 || >=18.0.0} dependencies: cross-spawn: 7.0.3 @@ -2369,9 +2378,9 @@ packages: cli-truncate: 3.1.0 commander: 10.0.0 debug: 4.3.4 - execa: 7.0.0 + execa: 7.1.0 lilconfig: 2.1.0 - listr2: 5.0.7 + listr2: 5.0.8 micromatch: 4.0.5 normalize-path: 3.0.0 object-inspect: 1.12.3 @@ -2383,8 +2392,8 @@ packages: - supports-color dev: true - /listr2/5.0.7: - resolution: {integrity: sha512-MD+qXHPmtivrHIDRwPYdfNkrzqDiuaKU/rfBcec3WMyMF3xylQj3jMq344OtvQxz7zaCFViRAeqlr2AFhPvXHw==} + /listr2/5.0.8: + resolution: {integrity: sha512-mC73LitKHj9w6v30nLNGPetZIlfpUniNSsxxrbaPcWOjDb92SHPzJPi/t+v1YC/lxKz/AJ9egOjww0qUuFxBpA==} engines: {node: ^14.13.1 || >=16.0.0} peerDependencies: enquirer: '>= 2.3.0 < 3' diff --git a/src/env.js b/src/env.js index 80d642e..f872f25 100644 --- a/src/env.js +++ b/src/env.js @@ -33,6 +33,7 @@ const env = { !!v || new Error('is required'), HTTP_TRUST_PROXY: () => true, // optional + INVALIDATE_TOKENS: () => true, // optional OVERRIDE_ARCHIVE: () => true, // optional PUBLIC_BOT: () => true, // optional PUBLISH_COMMANDS: () => true, // optional @@ -53,4 +54,4 @@ const load = options => { module.exports = { env, load, -}; \ No newline at end of file +}; diff --git a/src/http.js b/src/http.js index f582175..25a2681 100644 --- a/src/http.js +++ b/src/http.js @@ -53,22 +53,20 @@ module.exports = async client => { fastify.decorate('authenticate', async (req, res) => { try { const data = await req.jwtVerify(); - if (data.payload.expiresAt < Date.now()) { - return res.code(401).send({ - error: 'Unauthorised', - message: 'You are not authenticated.', - statusCode: 401, - - }); - } - } catch (err) { - res.send(err); + if (data.expiresAt < Date.now()) throw 'expired'; + if (data.createdAt < new Date(process.env.INVALIDATE_TOKENS).getTime()) throw 'expired'; + } catch (error) { + return res.code(401).send({ + error: 'Unauthorised', + message: error === 'expired' ? 'Your token has expired; please re-authenticate.' : 'You are not authenticated.', + statusCode: 401, + }); } }); fastify.decorate('isAdmin', async (req, res) => { try { - const userId = req.user.payload.id; + const userId = req.user.id; const guildId = req.params.guild; const guild = client.guilds.cache.get(guildId); if (!guild) { @@ -175,4 +173,4 @@ module.exports = async client => { }) => { client.log.error.http(`SvelteKit ${errorId} ${error}`); }); -}; \ No newline at end of file +}; 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 9900ea1..413cfad 100644 --- a/src/routes/api/admin/guilds/[guild]/categories/[category]/index.js +++ b/src/routes/api/admin/guilds/[guild]/categories/[category]/index.js @@ -23,7 +23,7 @@ module.exports.delete = fastify => ({ name: category.name, type: 'category', }, - userId: req.user.payload.id, + userId: req.user.id, }); return category; @@ -146,7 +146,7 @@ module.exports.patch = fastify => ({ await client.tickets.getCategory(categoryId, true); await updateStaffRoles(guild); - if (req.user.payload.accessToken && JSON.stringify(category.staffRoles) !== JSON.stringify(original.staffRoles)) { + if (req.user.accessToken && JSON.stringify(category.staffRoles) !== JSON.stringify(original.staffRoles)) { Promise.all([ 'Create ticket for user', 'claim', @@ -170,7 +170,7 @@ module.exports.patch = fastify => ({ type: ApplicationCommandPermissionType.Role, })), ], - token: req.user.payload.accessToken, + token: req.user.accessToken, }), )) .then(() => client.log.success('Updated application command permissions in "%s"', guild.name)) @@ -189,10 +189,10 @@ module.exports.patch = fastify => ({ name: category.name, type: 'category', }, - userId: req.user.payload.id, + userId: req.user.id, }); return category; }, onRequest: [fastify.authenticate, fastify.isAdmin], -}); \ No newline at end of file +}); 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 213e102..160c20b 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 @@ -20,10 +20,10 @@ module.exports.delete = fastify => ({ name: question.label, type: 'question', }, - userId: req.user.payload.id, + userId: req.user.id, }); return question; }, onRequest: [fastify.authenticate, fastify.isAdmin], -}); \ No newline at end of file +}); diff --git a/src/routes/api/admin/guilds/[guild]/categories/index.js b/src/routes/api/admin/guilds/[guild]/categories/index.js index 08f896a..a171454 100644 --- a/src/routes/api/admin/guilds/[guild]/categories/index.js +++ b/src/routes/api/admin/guilds/[guild]/categories/index.js @@ -54,7 +54,7 @@ module.exports.post = fastify => ({ /** @type {import('client')} */ const client = res.context.config.client; - const user = await client.users.fetch(req.user.payload.id); + const user = await client.users.fetch(req.user.id); const guild = client.guilds.cache.get(req.params.guild); const data = req.body; const allow = ['ViewChannel', 'ReadMessageHistory', 'SendMessages', 'EmbedLinks', 'AttachFiles']; @@ -101,7 +101,7 @@ module.exports.post = fastify => ({ await client.tickets.getCategory(category.id, true); await updateStaffRoles(guild); - if (req.user.payload.accessToken) { + if (req.user.accessToken) { Promise.all([ 'Create ticket for user', 'claim', @@ -125,7 +125,7 @@ module.exports.post = fastify => ({ type: ApplicationCommandPermissionType.Role, })), ], - token: req.user.payload.accessToken, + token: req.user.accessToken, }), )) .then(() => client.log.success('Updated application command permissions in "%s"', guild.name)) @@ -140,10 +140,10 @@ module.exports.post = fastify => ({ name: category.name, type: 'category', }, - userId: req.user.payload.id, + userId: req.user.id, }); return category; }, onRequest: [fastify.authenticate, fastify.isAdmin], -}); \ No newline at end of file +}); diff --git a/src/routes/api/admin/guilds/[guild]/panels.js b/src/routes/api/admin/guilds/[guild]/panels.js index 7181b1b..50991c4 100644 --- a/src/routes/api/admin/guilds/[guild]/panels.js +++ b/src/routes/api/admin/guilds/[guild]/panels.js @@ -134,10 +134,10 @@ module.exports.post = fastify => ({ id: channel.toString(), type: 'panel', }, - userId: req.user.payload.id, + userId: req.user.id, }); return true; }, onRequest: [fastify.authenticate, fastify.isAdmin], -}); \ No newline at end of file +}); diff --git a/src/routes/api/admin/guilds/[guild]/settings.js b/src/routes/api/admin/guilds/[guild]/settings.js index 0c6e929..09350f5 100644 --- a/src/routes/api/admin/guilds/[guild]/settings.js +++ b/src/routes/api/admin/guilds/[guild]/settings.js @@ -16,7 +16,7 @@ module.exports.delete = fastify => ({ name: client.guilds.cache.get(id), type: 'settings', }, - userId: req.user.payload.id, + userId: req.user.id, }); return settings; }, @@ -69,9 +69,9 @@ module.exports.patch = fastify => ({ name: client.guilds.cache.get(id).name, type: 'settings', }, - userId: req.user.payload.id, + userId: req.user.id, }); return settings; }, onRequest: [fastify.authenticate, fastify.isAdmin], -}); \ No newline at end of file +}); diff --git a/src/routes/api/admin/guilds/[guild]/tags/[tag].js b/src/routes/api/admin/guilds/[guild]/tags/[tag].js index 0f06192..13fb25d 100644 --- a/src/routes/api/admin/guilds/[guild]/tags/[tag].js +++ b/src/routes/api/admin/guilds/[guild]/tags/[tag].js @@ -30,7 +30,7 @@ module.exports.delete = fastify => ({ name: tag.name, type: 'tag', }, - userId: req.user.payload.id, + userId: req.user.id, }); return tag; @@ -97,10 +97,10 @@ module.exports.patch = fastify => ({ name: tag.name, type: 'tag', }, - userId: req.user.payload.id, + userId: req.user.id, }); return tag; }, onRequest: [fastify.authenticate, fastify.isAdmin], -}); \ No newline at end of file +}); diff --git a/src/routes/api/admin/guilds/[guild]/tags/index.js b/src/routes/api/admin/guilds/[guild]/tags/index.js index 35af225..46a84e8 100644 --- a/src/routes/api/admin/guilds/[guild]/tags/index.js +++ b/src/routes/api/admin/guilds/[guild]/tags/index.js @@ -56,10 +56,10 @@ module.exports.post = fastify => ({ name: tag.name, type: 'tag', }, - userId: req.user.payload.id, + userId: req.user.id, }); return tag; }, onRequest: [fastify.authenticate, fastify.isAdmin], -}); \ No newline at end of file +}); diff --git a/src/routes/api/admin/guilds/index.js b/src/routes/api/admin/guilds/index.js index 0b05178..9b6dc06 100644 --- a/src/routes/api/admin/guilds/index.js +++ b/src/routes/api/admin/guilds/index.js @@ -3,7 +3,7 @@ const { PermissionsBitField } = require('discord.js'); module.exports.get = fastify => ({ handler: async (req, res) => { const { client } = res.context.config; - const guilds = await (await fetch('https://discordapp.com/api/users/@me/guilds', { headers: { 'Authorization': `Bearer ${req.user.payload.accessToken}` } })).json(); + const guilds = await (await fetch('https://discordapp.com/api/users/@me/guilds', { headers: { 'Authorization': `Bearer ${req.user.accessToken}` } })).json(); res.send( guilds .filter(guild => guild.owner || new PermissionsBitField(guild.permissions.toString()).has(PermissionsBitField.Flags.ManageGuild)) @@ -16,4 +16,4 @@ module.exports.get = fastify => ({ ); }, onRequest: [fastify.authenticate], -}); \ No newline at end of file +}); diff --git a/src/routes/api/users/@me.js b/src/routes/api/users/@me/index.js similarity index 65% rename from src/routes/api/users/@me.js rename to src/routes/api/users/@me/index.js index f8fcf24..8608fc1 100644 --- a/src/routes/api/users/@me.js +++ b/src/routes/api/users/@me/index.js @@ -1,4 +1,4 @@ module.exports.get = fastify => ({ - handler: req => req.user.payload, + handler: req => req.user, onRequest: [fastify.authenticate], -}); \ No newline at end of file +}); diff --git a/src/routes/api/users/@me/key.js b/src/routes/api/users/@me/key.js new file mode 100644 index 0000000..7d8b202 --- /dev/null +++ b/src/routes/api/users/@me/key.js @@ -0,0 +1,19 @@ +module.exports.get = fastify => ({ + handler: async function (req, res) { // MUST NOT use arrow function syntax + if (process.env.PUBLIC_BOT === 'true') { + return res.code(400).send({ + error: 'Bad Request', + message: 'API keys are not available on public bots.', + statusCode: 400, + }); + } else { + return { + token: this.jwt.sign({ + createdAt: Date.now(), + id: req.user.id, + }), + }; + } + }, + onRequest: [fastify.authenticate], +}); diff --git a/src/routes/auth/callback.js b/src/routes/auth/callback.js index 2d04e9e..2860783 100644 --- a/src/routes/auth/callback.js +++ b/src/routes/auth/callback.js @@ -7,17 +7,15 @@ module.exports.get = () => ({ expires_in: expiresIn, } = await this.discord.getAccessTokenFromAuthorizationCodeFlow(req); const user = await (await fetch('https://discordapp.com/api/users/@me', { headers: { 'Authorization': `Bearer ${accessToken}` } })).json(); - const payload = { + const token = this.jwt.sign({ accessToken, - avatar: `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.webp`, + avatar: user.avatar, discriminator: user.discriminator, expiresAt: Date.now() + (expiresIn * 1000), id: user.id, locale: user.locale, username: user.username, - - }; - const token = this.jwt.sign({ payload }); + }); res .setCookie('token', token, { domain, @@ -30,4 +28,4 @@ module.exports.get = () => ({ .redirect(this.states.get(req.query.state) || '/'); this.states.delete(req.query.state); }, -}); \ No newline at end of file +}); diff --git a/src/routes/auth/logout.js b/src/routes/auth/logout.js index 82677f6..22fc77c 100644 --- a/src/routes/auth/logout.js +++ b/src/routes/auth/logout.js @@ -1,7 +1,7 @@ module.exports.get = fastify => ({ handler: async function (req, res) { await fetch('https://discord.com/api/oauth2/token/revoke', { - body: new URLSearchParams({ token: req.user.payload.accessToken }).toString(), + body: new URLSearchParams({ token: req.user.accessToken }).toString(), headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, method: 'POST', }); @@ -10,4 +10,4 @@ module.exports.get = fastify => ({ .send('The token has been revoked.'); }, onRequest: [fastify.authenticate], -}); \ No newline at end of file +});