feat: add API service keys

This commit is contained in:
Isaac 2023-03-12 22:21:21 +00:00
parent d09598dd3f
commit 6773d9ddbe
No known key found for this signature in database
GPG Key ID: 0DE40AE37BBA5C33
16 changed files with 105 additions and 80 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "discord-tickets", "name": "discord-tickets",
"version": "4.0.0-beta.7", "version": "4.0.0-beta.8",
"private": "true", "private": "true",
"description": "An open-source Discord bot for ticket management", "description": "An open-source Discord bot for ticket management",
"main": "src/", "main": "src/",
@ -40,7 +40,7 @@
"node": ">=18" "node": ">=18"
}, },
"dependencies": { "dependencies": {
"@discord-tickets/settings": "^2.0.0", "@discord-tickets/settings": "^2.1.0",
"@eartharoid/dbf": "^0.3.3", "@eartharoid/dbf": "^0.3.3",
"@eartharoid/dtf": "^2.0.1", "@eartharoid/dtf": "^2.0.1",
"@eartharoid/i18n": "^1.2.1", "@eartharoid/i18n": "^1.2.1",
@ -50,7 +50,7 @@
"@prisma/client": "^4.11.0", "@prisma/client": "^4.11.0",
"boxen": "^7.0.2", "boxen": "^7.0.2",
"cryptr": "^6.2.0", "cryptr": "^6.2.0",
"discord.js": "^14.7.1", "discord.js": "^14.8.0",
"dotenv": "^16.0.3", "dotenv": "^16.0.3",
"fastify": "^4.14.1", "fastify": "^4.14.1",
"figlet": "^1.5.2", "figlet": "^1.5.2",

View File

@ -3,7 +3,7 @@ lockfileVersion: 5.4
specifiers: specifiers:
'@commitlint/cli': ^17.4.4 '@commitlint/cli': ^17.4.4
'@commitlint/config-conventional': ^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/dbf': ^0.3.3
'@eartharoid/dtf': ^2.0.1 '@eartharoid/dtf': ^2.0.1
'@eartharoid/i18n': ^1.2.1 '@eartharoid/i18n': ^1.2.1
@ -16,7 +16,7 @@ specifiers:
bufferutil: ^4.0.7 bufferutil: ^4.0.7
conventional-changelog-cli: ^2.2.2 conventional-changelog-cli: ^2.2.2
cryptr: ^6.2.0 cryptr: ^6.2.0
discord.js: ^14.7.1 discord.js: ^14.8.0
dotenv: ^16.0.3 dotenv: ^16.0.3
erlpack: github:discord/erlpack erlpack: github:discord/erlpack
eslint: ^8.36.0 eslint: ^8.36.0
@ -43,7 +43,7 @@ specifiers:
zlib-sync: ^0.1.8 zlib-sync: ^0.1.8
dependencies: 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/dbf': 0.3.3_3cxu5zja4e2r5wmvge7mdcljwq
'@eartharoid/dtf': 2.0.1 '@eartharoid/dtf': 2.0.1
'@eartharoid/i18n': 1.2.1 '@eartharoid/i18n': 1.2.1
@ -53,7 +53,7 @@ dependencies:
'@prisma/client': 4.11.0_prisma@4.11.0 '@prisma/client': 4.11.0_prisma@4.11.0
boxen: 7.0.2 boxen: 7.0.2
cryptr: 6.2.0 cryptr: 6.2.0
discord.js: 14.7.1_3cxu5zja4e2r5wmvge7mdcljwq discord.js: 14.8.0_3cxu5zja4e2r5wmvge7mdcljwq
dotenv: 16.0.3 dotenv: 16.0.3
fastify: 4.14.1 fastify: 4.14.1
figlet: 1.5.2 figlet: 1.5.2
@ -293,8 +293,8 @@ packages:
'@jridgewell/trace-mapping': 0.3.9 '@jridgewell/trace-mapping': 0.3.9
dev: true dev: true
/@discord-tickets/settings/2.0.0_svelte@3.56.0: /@discord-tickets/settings/2.1.0_svelte@3.56.0:
resolution: {integrity: sha512-oVG8qfX21cmufi/jyJV4LrbzsZK86KUmFfgb5W39LCD/+zIg1+l8XrOfkONjwlbFUKbjtV0CV2xgx9Ns+0Wg2w==} resolution: {integrity: sha512-eWs4hQ/5VtbMsPcs/jOHwrPBOB5XBKuGYY0xILUqYFINa5oJsHIzHEjHjEIDcxW2xUJ3g92soBe5GVM3PjkfMA==}
dependencies: dependencies:
'@fortawesome/fontawesome-free': 6.3.0 '@fortawesome/fontawesome-free': 6.3.0
'@skyra/discord-components-core': 3.6.0 '@skyra/discord-components-core': 3.6.0
@ -310,11 +310,12 @@ packages:
- svelte - svelte
dev: false dev: false
/@discordjs/builders/1.4.0: /@discordjs/builders/1.5.0:
resolution: {integrity: sha512-nEeTCheTTDw5kO93faM1j8ZJPonAX86qpq/QVoznnSa8WWcCgJpjlu6GylfINTDW6o7zZY0my2SYdxx2mfNwGA==} resolution: {integrity: sha512-7XxT78mnNBPigHn2y6KAXkicxIBFtZREGWaRZ249EC1l6gBUEP8IyVY5JTciIjJArxkF+tg675aZvsTNTKBpmA==}
engines: {node: '>=16.9.0'} engines: {node: '>=16.9.0'}
dependencies: dependencies:
'@discordjs/util': 0.1.0 '@discordjs/formatters': 0.2.0
'@discordjs/util': 0.2.0
'@sapphire/shapeshift': 3.8.1 '@sapphire/shapeshift': 3.8.1
discord-api-types: 0.37.35 discord-api-types: 0.37.35
fast-deep-equal: 3.1.3 fast-deep-equal: 3.1.3
@ -322,17 +323,24 @@ packages:
tslib: 2.5.0 tslib: 2.5.0
dev: false dev: false
/@discordjs/collection/1.3.0: /@discordjs/collection/1.4.0:
resolution: {integrity: sha512-ylt2NyZ77bJbRij4h9u/wVy7qYw/aDqQLWnadjvDqW/WoWCxrsX6M3CIw9GVP5xcGCDxsrKj5e0r5evuFYwrKg==} resolution: {integrity: sha512-hiOJyk2CPFf1+FL3a4VKCuu1f448LlROVuu8nLz1+jCOAPokUcdFAV+l4pd3B3h6uJlJQSASoZzrdyNdjdtfzQ==}
engines: {node: '>=16.9.0'} engines: {node: '>=16.9.0'}
dev: false dev: false
/@discordjs/rest/1.5.0: /@discordjs/formatters/0.2.0:
resolution: {integrity: sha512-lXgNFqHnbmzp5u81W0+frdXN6Etf4EUi8FAPcWpSykKd8hmlWh1xy6BmE0bsJypU1pxohaA8lQCgp70NUI3uzA==} resolution: {integrity: sha512-vn4oMSXuMZUm8ITqVOtvE7/fMMISj4cI5oLsR09PEQXHKeKDAMLltG/DWeeIs7Idfy6V8Fk3rn1e69h7NfzuNA==}
engines: {node: '>=16.9.0'} engines: {node: '>=16.9.0'}
dependencies: dependencies:
'@discordjs/collection': 1.3.0 discord-api-types: 0.37.35
'@discordjs/util': 0.1.0 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/async-queue': 1.5.0
'@sapphire/snowflake': 3.4.0 '@sapphire/snowflake': 3.4.0
discord-api-types: 0.37.35 discord-api-types: 0.37.35
@ -341,15 +349,15 @@ packages:
undici: 5.20.0 undici: 5.20.0
dev: false dev: false
/@discordjs/util/0.1.0: /@discordjs/util/0.2.0:
resolution: {integrity: sha512-e7d+PaTLVQav6rOc2tojh2y6FE8S7REkqLldq1XF4soCx74XB/DIjbVbVLtBemf0nLW77ntz0v+o5DytKwFNLQ==} resolution: {integrity: sha512-/8qNbebFzLWKOOg+UV+RB8itp4SmU5jw0tBUD3ifElW6rYNOj1Ku5JaSW7lLl/WgjjxF01l/1uQPCzkwr110vg==}
engines: {node: '>=16.9.0'} engines: {node: '>=16.9.0'}
dev: false dev: false
/@eartharoid/dbf/0.3.3_3cxu5zja4e2r5wmvge7mdcljwq: /@eartharoid/dbf/0.3.3_3cxu5zja4e2r5wmvge7mdcljwq:
resolution: {integrity: sha512-eVDdpFlDV5CAvqoV5g1iAvoYhPjnvcyJ0Nnepc1YihlE1KIYGhVIK/2RaDsltzxRuiweO3Y7dvDj/cUpJnnFPA==} resolution: {integrity: sha512-eVDdpFlDV5CAvqoV5g1iAvoYhPjnvcyJ0Nnepc1YihlE1KIYGhVIK/2RaDsltzxRuiweO3Y7dvDj/cUpJnnFPA==}
dependencies: dependencies:
discord.js: 14.7.1_3cxu5zja4e2r5wmvge7mdcljwq discord.js: 14.8.0_3cxu5zja4e2r5wmvge7mdcljwq
node-dir: 0.1.17 node-dir: 0.1.17
transitivePeerDependencies: transitivePeerDependencies:
- bufferutil - bufferutil
@ -1419,14 +1427,15 @@ packages:
resolution: {integrity: sha512-iyKZ/82k7FX3lcmHiAvvWu5TmyfVo78RtghBV/YsehK6CID83k5SI03DKKopBcln+TiEIYw5MGgq7SJXSpNzMg==} resolution: {integrity: sha512-iyKZ/82k7FX3lcmHiAvvWu5TmyfVo78RtghBV/YsehK6CID83k5SI03DKKopBcln+TiEIYw5MGgq7SJXSpNzMg==}
dev: false dev: false
/discord.js/14.7.1_3cxu5zja4e2r5wmvge7mdcljwq: /discord.js/14.8.0_3cxu5zja4e2r5wmvge7mdcljwq:
resolution: {integrity: sha512-1FECvqJJjjeYcjSm0IGMnPxLqja/pmG1B0W2l3lUY2Gi4KXiyTeQmU1IxWcbXHn2k+ytP587mMWqva2IA87EbA==} resolution: {integrity: sha512-UOxYtc/YnV7jAJ2gISluJyYeBw4e+j8gWn+IoqG8unaHAVuvZ13DdYN0M1f9fbUgUvSarV798inIrYFtDNDjwQ==}
engines: {node: '>=16.9.0'} engines: {node: '>=16.9.0'}
dependencies: dependencies:
'@discordjs/builders': 1.4.0 '@discordjs/builders': 1.5.0
'@discordjs/collection': 1.3.0 '@discordjs/collection': 1.4.0
'@discordjs/rest': 1.5.0 '@discordjs/formatters': 0.2.0
'@discordjs/util': 0.1.0 '@discordjs/rest': 1.6.0
'@discordjs/util': 0.2.0
'@sapphire/snowflake': 3.4.0 '@sapphire/snowflake': 3.4.0
'@types/ws': 8.5.4 '@types/ws': 8.5.4
discord-api-types: 0.37.35 discord-api-types: 0.37.35
@ -1646,8 +1655,8 @@ packages:
strip-final-newline: 2.0.0 strip-final-newline: 2.0.0
dev: true dev: true
/execa/7.0.0: /execa/7.1.0:
resolution: {integrity: sha512-tQbH0pH/8LHTnwTrsKWideqi6rFB/QNUawEwrn+WHyz7PX1Tuz2u7wfTvbaNBdP5JD5LVWxNo8/A8CHNZ3bV6g==} resolution: {integrity: sha512-T6nIJO3LHxUZ6ahVRaxXz9WLEruXLqdcluA+UuTptXmLM7nDAn9lx9IfkxPyzEL21583qSt4RmL44pO71EHaJQ==}
engines: {node: ^14.18.0 || ^16.14.0 || >=18.0.0} engines: {node: ^14.18.0 || ^16.14.0 || >=18.0.0}
dependencies: dependencies:
cross-spawn: 7.0.3 cross-spawn: 7.0.3
@ -2369,9 +2378,9 @@ packages:
cli-truncate: 3.1.0 cli-truncate: 3.1.0
commander: 10.0.0 commander: 10.0.0
debug: 4.3.4 debug: 4.3.4
execa: 7.0.0 execa: 7.1.0
lilconfig: 2.1.0 lilconfig: 2.1.0
listr2: 5.0.7 listr2: 5.0.8
micromatch: 4.0.5 micromatch: 4.0.5
normalize-path: 3.0.0 normalize-path: 3.0.0
object-inspect: 1.12.3 object-inspect: 1.12.3
@ -2383,8 +2392,8 @@ packages:
- supports-color - supports-color
dev: true dev: true
/listr2/5.0.7: /listr2/5.0.8:
resolution: {integrity: sha512-MD+qXHPmtivrHIDRwPYdfNkrzqDiuaKU/rfBcec3WMyMF3xylQj3jMq344OtvQxz7zaCFViRAeqlr2AFhPvXHw==} resolution: {integrity: sha512-mC73LitKHj9w6v30nLNGPetZIlfpUniNSsxxrbaPcWOjDb92SHPzJPi/t+v1YC/lxKz/AJ9egOjww0qUuFxBpA==}
engines: {node: ^14.13.1 || >=16.0.0} engines: {node: ^14.13.1 || >=16.0.0}
peerDependencies: peerDependencies:
enquirer: '>= 2.3.0 < 3' enquirer: '>= 2.3.0 < 3'

View File

@ -33,6 +33,7 @@ const env = {
!!v || !!v ||
new Error('is required'), new Error('is required'),
HTTP_TRUST_PROXY: () => true, // optional HTTP_TRUST_PROXY: () => true, // optional
INVALIDATE_TOKENS: () => true, // optional
OVERRIDE_ARCHIVE: () => true, // optional OVERRIDE_ARCHIVE: () => true, // optional
PUBLIC_BOT: () => true, // optional PUBLIC_BOT: () => true, // optional
PUBLISH_COMMANDS: () => true, // optional PUBLISH_COMMANDS: () => true, // optional

View File

@ -53,22 +53,20 @@ module.exports = async client => {
fastify.decorate('authenticate', async (req, res) => { fastify.decorate('authenticate', async (req, res) => {
try { try {
const data = await req.jwtVerify(); const data = await req.jwtVerify();
if (data.payload.expiresAt < Date.now()) { 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({ return res.code(401).send({
error: 'Unauthorised', error: 'Unauthorised',
message: 'You are not authenticated.', message: error === 'expired' ? 'Your token has expired; please re-authenticate.' : 'You are not authenticated.',
statusCode: 401, statusCode: 401,
}); });
} }
} catch (err) {
res.send(err);
}
}); });
fastify.decorate('isAdmin', async (req, res) => { fastify.decorate('isAdmin', async (req, res) => {
try { try {
const userId = req.user.payload.id; const userId = req.user.id;
const guildId = req.params.guild; const guildId = req.params.guild;
const guild = client.guilds.cache.get(guildId); const guild = client.guilds.cache.get(guildId);
if (!guild) { if (!guild) {

View File

@ -23,7 +23,7 @@ module.exports.delete = fastify => ({
name: category.name, name: category.name,
type: 'category', type: 'category',
}, },
userId: req.user.payload.id, userId: req.user.id,
}); });
return category; return category;
@ -146,7 +146,7 @@ module.exports.patch = fastify => ({
await client.tickets.getCategory(categoryId, true); await client.tickets.getCategory(categoryId, true);
await updateStaffRoles(guild); 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([ Promise.all([
'Create ticket for user', 'Create ticket for user',
'claim', 'claim',
@ -170,7 +170,7 @@ module.exports.patch = fastify => ({
type: ApplicationCommandPermissionType.Role, type: ApplicationCommandPermissionType.Role,
})), })),
], ],
token: req.user.payload.accessToken, token: req.user.accessToken,
}), }),
)) ))
.then(() => client.log.success('Updated application command permissions in "%s"', guild.name)) .then(() => client.log.success('Updated application command permissions in "%s"', guild.name))
@ -189,7 +189,7 @@ module.exports.patch = fastify => ({
name: category.name, name: category.name,
type: 'category', type: 'category',
}, },
userId: req.user.payload.id, userId: req.user.id,
}); });
return category; return category;

View File

@ -20,7 +20,7 @@ module.exports.delete = fastify => ({
name: question.label, name: question.label,
type: 'question', type: 'question',
}, },
userId: req.user.payload.id, userId: req.user.id,
}); });
return question; return question;

View File

@ -54,7 +54,7 @@ module.exports.post = fastify => ({
/** @type {import('client')} */ /** @type {import('client')} */
const client = res.context.config.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 guild = client.guilds.cache.get(req.params.guild);
const data = req.body; const data = req.body;
const allow = ['ViewChannel', 'ReadMessageHistory', 'SendMessages', 'EmbedLinks', 'AttachFiles']; const allow = ['ViewChannel', 'ReadMessageHistory', 'SendMessages', 'EmbedLinks', 'AttachFiles'];
@ -101,7 +101,7 @@ module.exports.post = fastify => ({
await client.tickets.getCategory(category.id, true); await client.tickets.getCategory(category.id, true);
await updateStaffRoles(guild); await updateStaffRoles(guild);
if (req.user.payload.accessToken) { if (req.user.accessToken) {
Promise.all([ Promise.all([
'Create ticket for user', 'Create ticket for user',
'claim', 'claim',
@ -125,7 +125,7 @@ module.exports.post = fastify => ({
type: ApplicationCommandPermissionType.Role, type: ApplicationCommandPermissionType.Role,
})), })),
], ],
token: req.user.payload.accessToken, token: req.user.accessToken,
}), }),
)) ))
.then(() => client.log.success('Updated application command permissions in "%s"', guild.name)) .then(() => client.log.success('Updated application command permissions in "%s"', guild.name))
@ -140,7 +140,7 @@ module.exports.post = fastify => ({
name: category.name, name: category.name,
type: 'category', type: 'category',
}, },
userId: req.user.payload.id, userId: req.user.id,
}); });
return category; return category;

View File

@ -134,7 +134,7 @@ module.exports.post = fastify => ({
id: channel.toString(), id: channel.toString(),
type: 'panel', type: 'panel',
}, },
userId: req.user.payload.id, userId: req.user.id,
}); });
return true; return true;

View File

@ -16,7 +16,7 @@ module.exports.delete = fastify => ({
name: client.guilds.cache.get(id), name: client.guilds.cache.get(id),
type: 'settings', type: 'settings',
}, },
userId: req.user.payload.id, userId: req.user.id,
}); });
return settings; return settings;
}, },
@ -69,7 +69,7 @@ module.exports.patch = fastify => ({
name: client.guilds.cache.get(id).name, name: client.guilds.cache.get(id).name,
type: 'settings', type: 'settings',
}, },
userId: req.user.payload.id, userId: req.user.id,
}); });
return settings; return settings;
}, },

View File

@ -30,7 +30,7 @@ module.exports.delete = fastify => ({
name: tag.name, name: tag.name,
type: 'tag', type: 'tag',
}, },
userId: req.user.payload.id, userId: req.user.id,
}); });
return tag; return tag;
@ -97,7 +97,7 @@ module.exports.patch = fastify => ({
name: tag.name, name: tag.name,
type: 'tag', type: 'tag',
}, },
userId: req.user.payload.id, userId: req.user.id,
}); });
return tag; return tag;

View File

@ -56,7 +56,7 @@ module.exports.post = fastify => ({
name: tag.name, name: tag.name,
type: 'tag', type: 'tag',
}, },
userId: req.user.payload.id, userId: req.user.id,
}); });
return tag; return tag;

View File

@ -3,7 +3,7 @@ const { PermissionsBitField } = require('discord.js');
module.exports.get = fastify => ({ module.exports.get = fastify => ({
handler: async (req, res) => { handler: async (req, res) => {
const { client } = res.context.config; 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( res.send(
guilds guilds
.filter(guild => guild.owner || new PermissionsBitField(guild.permissions.toString()).has(PermissionsBitField.Flags.ManageGuild)) .filter(guild => guild.owner || new PermissionsBitField(guild.permissions.toString()).has(PermissionsBitField.Flags.ManageGuild))

View File

@ -1,4 +1,4 @@
module.exports.get = fastify => ({ module.exports.get = fastify => ({
handler: req => req.user.payload, handler: req => req.user,
onRequest: [fastify.authenticate], onRequest: [fastify.authenticate],
}); });

View File

@ -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],
});

View File

@ -7,17 +7,15 @@ module.exports.get = () => ({
expires_in: expiresIn, expires_in: expiresIn,
} = await this.discord.getAccessTokenFromAuthorizationCodeFlow(req); } = await this.discord.getAccessTokenFromAuthorizationCodeFlow(req);
const user = await (await fetch('https://discordapp.com/api/users/@me', { headers: { 'Authorization': `Bearer ${accessToken}` } })).json(); const user = await (await fetch('https://discordapp.com/api/users/@me', { headers: { 'Authorization': `Bearer ${accessToken}` } })).json();
const payload = { const token = this.jwt.sign({
accessToken, accessToken,
avatar: `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.webp`, avatar: user.avatar,
discriminator: user.discriminator, discriminator: user.discriminator,
expiresAt: Date.now() + (expiresIn * 1000), expiresAt: Date.now() + (expiresIn * 1000),
id: user.id, id: user.id,
locale: user.locale, locale: user.locale,
username: user.username, username: user.username,
});
};
const token = this.jwt.sign({ payload });
res res
.setCookie('token', token, { .setCookie('token', token, {
domain, domain,

View File

@ -1,7 +1,7 @@
module.exports.get = fastify => ({ module.exports.get = fastify => ({
handler: async function (req, res) { handler: async function (req, res) {
await fetch('https://discord.com/api/oauth2/token/revoke', { 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' }, headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
method: 'POST', method: 'POST',
}); });