From bf8fce29f6e7d4c7f983d5cadfc21be47842effc Mon Sep 17 00:00:00 2001 From: RappyTV Date: Sun, 9 Jun 2024 03:02:05 +0200 Subject: [PATCH 1/8] Return session data in SessionValidator --- src/libs/SessionValidator.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/libs/SessionValidator.ts b/src/libs/SessionValidator.ts index 2575c6b..0e6368f 100644 --- a/src/libs/SessionValidator.ts +++ b/src/libs/SessionValidator.ts @@ -1,10 +1,23 @@ import { verify } from "jsonwebtoken"; +import players from "../database/schemas/players"; const publicKey = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt3rCKqrQYcmSEE8zyQTA7flKIe1pr7GHY58lTF74Pw/ZZYzxmScYteXp8XBvrQfPj4U/v9Vum8IPg6GHOv1Gde3rY5ydfunEKi/w4ibVN5buPpndzcNaMoQvEJ/B5VLIzCvLc5HepFKbKFOGu8XoFz8NZY0lUfGLR0rcDsHWZLHPhqYsIsUd9snkWkHaIKD7l9xTd77PpLZiBwCPnVhh3invFY2OnCL6BfiJhhud/aDaAzFW981J9EhyACbuac2qu6Uz2bKX/7Af01gUs48MbKUx8YirBWLD7j/CJMWorTT467It4mAvDlw43s3Py9IvxCzEFnOIftIv+7wwv1RjVQIDAQAB\n-----END PUBLIC KEY-----"; -export function validJWTSession(token: string, uuid: string, equal: boolean): boolean { +type SessionData = { + uuid: string | null, + equal: boolean, + isAdmin: boolean +} + +export async function getJWTSession(token: string, uuid: string): Promise { const tokenUuid = getUuidByJWT(token); - if(equal) return tokenUuid === uuid; - else return !!tokenUuid; + if(!tokenUuid) return { uuid: tokenUuid, equal: tokenUuid == uuid, isAdmin: false }; + const data = await players.findOne({ uuid: tokenUuid }); + if(!data) return { uuid: tokenUuid, equal: tokenUuid == uuid, isAdmin: false }; + return { + uuid: tokenUuid, + equal: uuid == tokenUuid, + isAdmin: data.admin + } } type LabyPayload = { From 9f44dcfbf98bd915f14a35f94880cf35559520da Mon Sep 17 00:00:00 2001 From: RappyTV Date: Sun, 9 Jun 2024 03:02:59 +0200 Subject: [PATCH 2/8] Use new session object, return ban info, allow admins to perform actions --- src/routes/ban.ts | 27 +++++++-------------------- src/routes/icon.ts | 9 ++++----- src/routes/position.ts | 9 ++++----- src/routes/report.ts | 7 +++---- src/routes/root.ts | 36 ++++++++++++++++++------------------ 5 files changed, 36 insertions(+), 52 deletions(-) diff --git a/src/routes/ban.ts b/src/routes/ban.ts index 014c7dc..38caef8 100644 --- a/src/routes/ban.ts +++ b/src/routes/ban.ts @@ -1,5 +1,5 @@ import Elysia, { t } from "elysia"; -import { getUuidByJWT, validJWTSession } from "../libs/SessionValidator"; +import { getUuidByJWT, getJWTSession } from "../libs/SessionValidator"; import players from "../database/schemas/players"; import fetchI18n from "../middleware/FetchI18n"; @@ -8,13 +8,9 @@ export default new Elysia({ }).use(fetchI18n).get(`/`, async ({ error, params, headers, i18n }) => { // Get ban info const uuid = params.uuid.replaceAll(`-`, ``); const { authorization } = headers; - const authenticated = authorization && validJWTSession(authorization, uuid, false); - if(authorization == `0`) return error(401, { error: i18n(`error.premiumAccount`) }); - if(!authenticated) return error(401, { error: i18n(`error.notAllowed`) }); - - const executor = await players.findOne({ uuid: getUuidByJWT(authorization)! }); - if(!executor || !executor.admin) return error(403, { error: i18n(`error.notAllowed`) }); + const session = await getJWTSession(authorization, uuid); + if(!session.isAdmin) return error(403, { error: i18n(`error.notAllowed`) }); const player = await players.findOne({ uuid }); if(!player) return error(404, { error: i18n(`error.playerNotFound`) }); @@ -24,16 +20,11 @@ export default new Elysia({ params: t.Object({ uuid: t.String() }), headers: t.Object({ authorization: t.String({ error: `error.notAllowed` }) }, { error: `error.notAllowed` }) }).post(`/`, async ({ error, params, headers, body, i18n }) => { // Ban player - console.log(body.reason); const uuid = params.uuid.replaceAll(`-`, ``); const { authorization } = headers; - const authenticated = authorization && validJWTSession(authorization, uuid, false); - if(authorization == `0`) return error(401, { error: i18n(`error.premiumAccount`) }); - if(!authenticated) return error(401, { error: i18n(`error.notAllowed`) }); - - const executor = await players.findOne({ uuid: getUuidByJWT(authorization)! }); - if(!executor || !executor.admin) return error(403, { error: i18n(`error.notAllowed`) }); + const session = await getJWTSession(authorization, uuid); + if(!session.isAdmin) return error(403, { error: i18n(`error.notAllowed`) }); const player = await players.findOne({ uuid }); if(!player) return error(404, { error: i18n(`error.playerNotFound`) }); @@ -53,13 +44,9 @@ export default new Elysia({ }).delete(`/`, async ({ error, params, headers, i18n }) => { // Unban player const uuid = params.uuid.replaceAll(`-`, ``); const { authorization } = headers; - const authenticated = authorization && validJWTSession(authorization, uuid, false); - if(authorization == `0`) return error(401, { error: i18n(`error.premiumAccount`) }); - if(!authenticated) return error(401, { error: i18n(`error.notAllowed`) }); - - const executor = await players.findOne({ uuid: getUuidByJWT(authorization)! }); - if(!executor || !executor.admin) return error(403, { error: i18n(`error.notAllowed`) }); + const session = await getJWTSession(authorization, uuid); + if(!session.isAdmin) return error(403, { error: i18n(`error.notAllowed`) }); const player = await players.findOne({ uuid }); if(!player) return error(404, { error: i18n(`error.playerNotFound`) }); diff --git a/src/routes/icon.ts b/src/routes/icon.ts index d0ef4ea..d1dc427 100644 --- a/src/routes/icon.ts +++ b/src/routes/icon.ts @@ -1,5 +1,5 @@ import Elysia, { t } from "elysia"; -import { validJWTSession } from "../libs/SessionValidator"; +import { getJWTSession } from "../libs/SessionValidator"; import players from "../database/schemas/players"; import * as config from "../../config.json"; import fetchI18n from "../middleware/FetchI18n"; @@ -10,10 +10,9 @@ export default new Elysia({ const uuid = params.uuid.replaceAll(`-`, ``); const icon = body.icon.toUpperCase(); const { authorization } = headers; - const authenticated = authorization && validJWTSession(authorization, uuid, true); - if(authorization == `0`) return error(401, { error: i18n(`error.premiumAccount`) }); - if(!authenticated) return error(401, { error: i18n(`error.notAllowed`) }); + const session = await getJWTSession(authorization, uuid); + if(!session.equal && !session.isAdmin) return error(403, { error: i18n(`error.notAllowed`) }); const player = await players.findOne({ uuid }); if(!player) return error(404, { error: i18n(`error.noTag`) }); @@ -25,7 +24,7 @@ export default new Elysia({ player.icon = icon; await player.save(); - return { message: i18n(`icon.success`) }; + return { message: i18n(`icon.success.${session.equal ? 'self' : 'admin'}`) }; }, { detail: { tags: ['Settings'], diff --git a/src/routes/position.ts b/src/routes/position.ts index f9f4235..d42bcc1 100644 --- a/src/routes/position.ts +++ b/src/routes/position.ts @@ -1,5 +1,5 @@ import Elysia, { t } from "elysia"; -import { validJWTSession } from "../libs/SessionValidator"; +import { getJWTSession } from "../libs/SessionValidator"; import players from "../database/schemas/players"; import fetchI18n from "../middleware/FetchI18n"; @@ -9,10 +9,9 @@ export default new Elysia({ const uuid = params.uuid.replaceAll(`-`, ``); const position = body.position.toUpperCase(); const { authorization } = headers; - const authenticated = authorization && validJWTSession(authorization, uuid, true); - if(authorization == `0`) return error(401, { error: i18n(`error.premiumAccount`) }); - if(!authenticated) return error(401, { error: i18n(`error.notAllowed`) }); + const session = await getJWTSession(authorization, uuid); + if(!session.equal && !session.isAdmin) return error(403, { error: i18n(`error.notAllowed`) }); const player = await players.findOne({ uuid }); if(!player) return error(404, { error: i18n(`error.noTag`) }); @@ -24,7 +23,7 @@ export default new Elysia({ player.position = position as "ABOVE" | "BELOW" | "RIGHT" | "LEFT"; await player.save(); - return { message: i18n(`position.success`) }; + return { message: i18n(`position.success.${session.equal ? 'self' : 'admin'}`) }; }, { detail: { tags: ['Settings'], diff --git a/src/routes/report.ts b/src/routes/report.ts index 442d8f8..e52b466 100644 --- a/src/routes/report.ts +++ b/src/routes/report.ts @@ -1,5 +1,5 @@ import Elysia, { t } from "elysia"; -import { getUuidByJWT, validJWTSession } from "../libs/SessionValidator"; +import { getUuidByJWT, getJWTSession } from "../libs/SessionValidator"; import players from "../database/schemas/players"; import { NotificationType, sendMessage } from "../libs/DiscordNotifier"; import fetchI18n from "../middleware/FetchI18n"; @@ -9,10 +9,9 @@ export default new Elysia({ }).use(fetchI18n).post(`/`, async ({ error, params, headers, body, i18n }) => { // Report player const uuid = params.uuid.replaceAll(`-`, ``); const { authorization } = headers; - const authenticated = authorization && validJWTSession(authorization, uuid, false); - if(authorization == `0`) return error(401, { error: i18n(`error.premiumAccount`) }); - if(!authenticated) return error(401, { error: i18n(`error.notAllowed`) }); + const session = await getJWTSession(authorization, uuid); + if(!session.uuid) return error(403, { error: i18n(`error.notAllowed`) }); const player = await players.findOne({ uuid }); if(!player) return error(404, { error: i18n(`error.playerNoTag`) }); diff --git a/src/routes/root.ts b/src/routes/root.ts index 5032ac2..e641a0c 100644 --- a/src/routes/root.ts +++ b/src/routes/root.ts @@ -2,7 +2,7 @@ import Elysia, { t } from "elysia"; import players from "../database/schemas/players"; import Logger from "../libs/Logger"; import { sendMessage, NotificationType } from "../libs/DiscordNotifier"; -import { validJWTSession } from "../libs/SessionValidator"; +import { getJWTSession } from "../libs/SessionValidator"; import * as config from "../../config.json"; import fetchI18n from "../middleware/FetchI18n"; @@ -12,21 +12,23 @@ export default new Elysia() .use(fetchI18n).get(`/`, async ({ error, params, headers, i18n }) => { // Get player info const uuid = params.uuid.replaceAll(`-`, ``); const { authorization } = headers; - const authenticated = authorization && validJWTSession(authorization, uuid, false); - if(authorization == `0`) return error(401, { error: i18n(`error.premiumAccount`) }); - if(config.requireSessionIds && !authenticated) return error(401, { error: i18n(`error.notAllowed`) }); + const session = await getJWTSession(authorization, uuid); + if(config.requireSessionIds && !session.uuid) return error(403, { error: i18n(`error.notAllowed`) }); const player = await players.findOne({ uuid }); if(!player) return error(404, { error: i18n(`error.playerNoTag`) }); - if(player.isBanned()) return error(403, { error: i18n(`getInfo.banned`) }); return { uuid: player.uuid, - tag: player.tag || null, + tag: player.isBanned() ? null : player.tag || null, position: player.position, icon: player.icon, - admin: player.admin + admin: player.admin, + ban: session.equal || session.isAdmin ? { + active: !!player.ban?.active, + reason: player.ban?.reason || null + } : null }; }, { detail: { @@ -34,7 +36,7 @@ export default new Elysia() description: `Get another players' tag info` }, response: { - 200: t.Object({ uuid: t.String(), tag: t.Union([t.String(), t.Null()]), position: t.String(), icon: t.String(), admin: t.Boolean({ default: false }) }, { description: `You received the tag data.` }), + 200: t.Object({ uuid: t.String(), tag: t.Union([t.String(), t.Null()]), position: t.String(), icon: t.String(), admin: t.Boolean({ default: false }), ban: t.Union([t.Object({ active: t.Boolean(), reason: t.Union([t.String(), t.Null()]) }), t.Null()]) }, { description: `You received the tag data.` }), 401: t.Object({ error: t.String() }, { description: `You're not authenticated with LabyConnect.` }), 403: t.Object({ error: t.String() }, { description: `The player is banned.` }), 404: t.Object({ error: t.String() }, { description: `The player is not in the database.` }), @@ -45,17 +47,16 @@ export default new Elysia() headers: t.Object({ authorization: config.requireSessionIds ? t.String({ error: `error.notAllowed`, description: `Your LabyConnect JWT` }) : t.Optional(t.String({ description: `Your LabyConnect JWT` })) }, { error: `error.notAllowed` }), }).post(`/`, async ({ error, params, headers, body, i18n }) => { // Change tag const uuid = params.uuid.replaceAll(`-`, ``); - const tag = body.tag; + const tag = body.tag.trim(); const { authorization } = headers; - const authenticated = authorization && validJWTSession(authorization, uuid, true); - if(authorization == `0`) return error(401, { error: i18n(`error.premiumAccount`) }); - if(!authenticated) return error(401, { error: i18n(`error.notAllowed`) }); + const session = await getJWTSession(authorization, uuid); + if(!session.equal && !session.isAdmin) return error(403, { error: i18n(`error.notAllowed`) }); const player = await players.findOne({ uuid }); if(player && player.isBanned()) return error(403, { error: i18n(`error.banned`) }); const { blacklist, watchlist } = config.validation.tag; - if(tag.trim() == '') return error(422, { error: i18n(`setTag.empty`) }); + if(tag == '') return error(422, { error: i18n(`setTag.empty`) }); const blacklistedWord = blacklist.find((word) => tag.replace(colorCodeRegex, ``).toLowerCase().includes(word)); if(blacklistedWord) return error(422, { error: i18n(`setTag.blacklisted`).replaceAll(``, blacklistedWord) }); const isWatched = (player && player.watchlist) || watchlist.some((word) => { @@ -84,7 +85,7 @@ export default new Elysia() } if(isWatched) sendMessage({ type: NotificationType.WatchlistTagUpdate, uuid, tag }); - return { message: i18n(`setTag.success`) }; + return { message: i18n(`setTag.success.${session.equal ? 'self' : 'admin'}`) }; }, { detail: { tags: ['Settings'], @@ -105,10 +106,9 @@ export default new Elysia() }).delete(`/`, async ({ error, params, headers, i18n }) => { // Delete tag const uuid = params.uuid.replaceAll(`-`, ``); const { authorization } = headers; - const authenticated = authorization && validJWTSession(authorization, uuid, true); - if(authorization == `0`) return error(401, { error: i18n(`error.premiumAccount`) }); - if(!authenticated) return error(401, { error: i18n(`error.notAllowed`) }); + const session = await getJWTSession(authorization, uuid); + if(!session.equal && !session.isAdmin) return error(403, { error: i18n(`error.notAllowed`) }); const player = await players.findOne({ uuid }); if(!player) return error(404, { error: i18n(`error.noTag`) }); @@ -118,7 +118,7 @@ export default new Elysia() player.tag = null; await player.save(); - return { message: i18n(`resetTag.success`) }; + return { message: i18n(`resetTag.success.${session.equal ? 'self' : 'admin'}`) }; }, { detail: { tags: ['Settings'], From 6e5e0d3cc3bfb6035e032e5c8240936e5a69bc79 Mon Sep 17 00:00:00 2001 From: RappyTV Date: Sun, 9 Jun 2024 13:31:27 +0200 Subject: [PATCH 3/8] Add new toggle admin route --- src/routes/root.ts | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/src/routes/root.ts b/src/routes/root.ts index e641a0c..3ba8d5f 100644 --- a/src/routes/root.ts +++ b/src/routes/root.ts @@ -54,7 +54,7 @@ export default new Elysia() if(!session.equal && !session.isAdmin) return error(403, { error: i18n(`error.notAllowed`) }); const player = await players.findOne({ uuid }); - if(player && player.isBanned()) return error(403, { error: i18n(`error.banned`) }); + if(player && player.isBanned()) return error(403, { error: i18n(`error.${session.equal ? 'b' : 'playerB'}anned`) }); const { blacklist, watchlist } = config.validation.tag; if(tag == '') return error(422, { error: i18n(`setTag.empty`) }); const blacklistedWord = blacklist.find((word) => tag.replace(colorCodeRegex, ``).toLowerCase().includes(word)); @@ -103,6 +103,37 @@ export default new Elysia() params: t.Object({ uuid: t.String({ description: `Your UUID` }) }), body: t.Object({ tag: t.String({ minLength: config.validation.tag.min, maxLength: config.validation.tag.max, error: `setTag.validation;;[["min", "${config.validation.tag.min}"], ["max", "${config.validation.tag.max}"]]` }) }, { error: `error.invalidBody`, additionalProperties: true }), headers: t.Object({ authorization: t.String({ error: `error.notAllowed`, description: `Your LabyConnect JWT` }) }, { error: `error.notAllowed` }) +}).post(`/admin`, async ({ error, params, headers, i18n }) => { // Change tag + const uuid = params.uuid.replaceAll(`-`, ``); + const { authorization } = headers; + if(authorization == `0`) return error(401, { error: i18n(`error.premiumAccount`) }); + const session = await getJWTSession(authorization, uuid); + if(!session.isAdmin) return error(403, { error: i18n(`error.notAllowed`) }); + + const player = await players.findOne({ uuid }); + if(!player) return error(404, { error: i18n(`error.playerNotFound`) }); + + player.admin = !player.admin; + await player.save(); + + return { message: i18n(`toggleAdmin.${player.admin ? 'on' : 'off'}`) }; +}, { + detail: { + tags: ['Settings'], + description: `Change your global tag` + }, + response: { + 200: t.Object({ message: t.String() }, { description: `The player's admin status has changed.` }), + 400: t.Object({ error: t.String() }, { description: `You already have this tag.` }), + 401: t.Object({ error: t.String() }, { description: `You're not authenticated with LabyConnect.` }), + 403: t.Object({ error: t.String() }, { description: `You're not an admin.` }), + 404: t.Object({ error: t.String() }, { description: `The player was not found.` }), + 422: t.Object({ error: t.String() }, { description: `You're lacking the validation requirements.` }), + 429: t.Object({ error: t.String() }, { description: `You're ratelimited.` }), + 503: t.Object({ error: t.String() }, { description: `Database is not reachable.` }) + }, + params: t.Object({ uuid: t.String({ description: `The player's UUID` }) }), + headers: t.Object({ authorization: t.String({ error: `error.notAllowed`, description: `Your LabyConnect JWT` }) }, { error: `error.notAllowed` }) }).delete(`/`, async ({ error, params, headers, i18n }) => { // Delete tag const uuid = params.uuid.replaceAll(`-`, ``); const { authorization } = headers; @@ -112,7 +143,7 @@ export default new Elysia() const player = await players.findOne({ uuid }); if(!player) return error(404, { error: i18n(`error.noTag`) }); - if(player.isBanned()) return error(403, { error: i18n(`error.banned`) }); + if(player.isBanned()) return error(403, { error: i18n(`error.${session.equal ? 'b' : 'playerB'}anned`) }); if(!player.tag) return error(404, { error: i18n(`error.noTag`) }); player.tag = null; From 309cf47f021ca9413acb6e92d3b0cd4fef23cb78 Mon Sep 17 00:00:00 2001 From: RappyTV Date: Sun, 9 Jun 2024 13:31:45 +0200 Subject: [PATCH 4/8] Add example ratelimit for toggle admin route --- config.json.example | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/config.json.example b/config.json.example index d4deeac..4ab4641 100644 --- a/config.json.example +++ b/config.json.example @@ -31,6 +31,12 @@ "max": 5, "seconds": 60 }, + { + "method": "POST", + "regex": "\/players\/\\w+\/admin/?", + "max": 2, + "seconds": 30 + }, { "method": "POST", "regex": "\/players\/\\w+/position\/?", From 8ec535cfa0c10e3020e2da7ed4af95246582a413 Mon Sep 17 00:00:00 2001 From: RappyTV Date: Wed, 19 Jun 2024 17:04:14 +0200 Subject: [PATCH 5/8] Rearrange action buttons, add button to edit tag --- src/bot/buttons/Actions.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/bot/buttons/Actions.ts b/src/bot/buttons/Actions.ts index 49010aa..f8dbca2 100644 --- a/src/bot/buttons/Actions.ts +++ b/src/bot/buttons/Actions.ts @@ -40,16 +40,20 @@ export default class Actions extends Button { .setCustomId(`ban`) .setStyle(ButtonStyle.Danger), new ButtonBuilder() - .setLabel(`Clear tag`) - .setCustomId(`clearTag`) - .setStyle(ButtonStyle.Danger) + .setLabel(`Unban`) + .setCustomId(`unban`) + .setStyle(ButtonStyle.Success) ), new ActionRowBuilder() .addComponents( new ButtonBuilder() - .setLabel(`Unban`) - .setCustomId(`unban`) - .setStyle(ButtonStyle.Success) + .setLabel(`Clear tag`) + .setCustomId(`clearTag`) + .setStyle(ButtonStyle.Danger), + new ButtonBuilder() + .setLabel(`Set tag`) + .setCustomId(`setTag`) + .setStyle(ButtonStyle.Primary) ) ] From 3a4e0c701ccb76cd72df0f714ae23f3c769554f6 Mon Sep 17 00:00:00 2001 From: RappyTV Date: Wed, 19 Jun 2024 17:04:33 +0200 Subject: [PATCH 6/8] Implement button to edit tag --- src/bot/buttons/SetTag.ts | 37 +++++++++++++++++++++++++++++++++++++ src/bot/modals/SetTag.ts | 20 ++++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 src/bot/buttons/SetTag.ts create mode 100644 src/bot/modals/SetTag.ts diff --git a/src/bot/buttons/SetTag.ts b/src/bot/buttons/SetTag.ts new file mode 100644 index 0000000..ae94bcc --- /dev/null +++ b/src/bot/buttons/SetTag.ts @@ -0,0 +1,37 @@ +import { ButtonInteraction, CacheType, Message, GuildMember, User, EmbedBuilder, ActionRowBuilder, ModalBuilder, TextInputBuilder, TextInputStyle } from "discord.js"; +import Button from "../structs/Button"; +import players from "../../database/schemas/players"; +import { validation } from "../../../config.json"; +import { colors } from "../bot"; + +export default class SetTag extends Button { + constructor() { + super("setTag"); + } + + async trigger(interaction: ButtonInteraction, message: Message, member: GuildMember, user: User) { + const player = await players.findOne({ uuid: message.embeds[0].fields[0].value.replaceAll(`\``, ``) }); + if(!player) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription(`❌ Player not found!`)], ephemeral: true }); + + const input = new TextInputBuilder() + .setLabel(`New tag`) + .setCustomId(`tag`) + .setPlaceholder(`Enter a tag`) + .setRequired(true) + .setMinLength(validation.tag.min) + .setMaxLength(validation.tag.max) + .setStyle(TextInputStyle.Short); + + if(player.tag) input.setValue(player.tag); + + const modal = new ModalBuilder() + .setTitle(`Set new tag`) + .setCustomId(`setTag`) + .addComponents( + new ActionRowBuilder() + .addComponents(input) + ) + + interaction.showModal(modal); + } +} \ No newline at end of file diff --git a/src/bot/modals/SetTag.ts b/src/bot/modals/SetTag.ts new file mode 100644 index 0000000..1472c94 --- /dev/null +++ b/src/bot/modals/SetTag.ts @@ -0,0 +1,20 @@ +import { CacheType, Message, GuildMember, User, EmbedBuilder, ModalSubmitInteraction, ModalSubmitFields } from "discord.js"; +import players from "../../database/schemas/players"; +import { colors } from "../bot"; +import Modal from "../structs/Modal"; + +export default class SetTag extends Modal { + constructor() { + super("setTag"); + } + + async submit(interaction: ModalSubmitInteraction, message: Message, fields: ModalSubmitFields, member: GuildMember, user: User) { + const player = await players.findOne({ uuid: message.embeds[0].fields[0].value.replaceAll(`\``, ``) }); + if(!player) return interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.error).setDescription(`❌ Player not found!`)], ephemeral: true }); + + player.tag = fields.getTextInputValue('tag'); + player.save(); + + interaction.reply({ embeds: [new EmbedBuilder().setColor(colors.success).setDescription(`✅ The tag was successfully set!`)], ephemeral: true }); + } +} \ No newline at end of file From 490f8f77a17e2ab524adbff5ec2bfe91fa15e7ca Mon Sep 17 00:00:00 2001 From: RappyTV Date: Wed, 19 Jun 2024 17:10:58 +0200 Subject: [PATCH 7/8] Remove unnecessary premium account check which doesn't work anymore --- src/routes/ban.ts | 3 --- src/routes/icon.ts | 1 - src/routes/position.ts | 1 - src/routes/report.ts | 1 - src/routes/root.ts | 6 +----- 5 files changed, 1 insertion(+), 11 deletions(-) diff --git a/src/routes/ban.ts b/src/routes/ban.ts index 38caef8..382e6e1 100644 --- a/src/routes/ban.ts +++ b/src/routes/ban.ts @@ -8,7 +8,6 @@ export default new Elysia({ }).use(fetchI18n).get(`/`, async ({ error, params, headers, i18n }) => { // Get ban info const uuid = params.uuid.replaceAll(`-`, ``); const { authorization } = headers; - if(authorization == `0`) return error(401, { error: i18n(`error.premiumAccount`) }); const session = await getJWTSession(authorization, uuid); if(!session.isAdmin) return error(403, { error: i18n(`error.notAllowed`) }); @@ -22,7 +21,6 @@ export default new Elysia({ }).post(`/`, async ({ error, params, headers, body, i18n }) => { // Ban player const uuid = params.uuid.replaceAll(`-`, ``); const { authorization } = headers; - if(authorization == `0`) return error(401, { error: i18n(`error.premiumAccount`) }); const session = await getJWTSession(authorization, uuid); if(!session.isAdmin) return error(403, { error: i18n(`error.notAllowed`) }); @@ -44,7 +42,6 @@ export default new Elysia({ }).delete(`/`, async ({ error, params, headers, i18n }) => { // Unban player const uuid = params.uuid.replaceAll(`-`, ``); const { authorization } = headers; - if(authorization == `0`) return error(401, { error: i18n(`error.premiumAccount`) }); const session = await getJWTSession(authorization, uuid); if(!session.isAdmin) return error(403, { error: i18n(`error.notAllowed`) }); diff --git a/src/routes/icon.ts b/src/routes/icon.ts index d1dc427..8eac919 100644 --- a/src/routes/icon.ts +++ b/src/routes/icon.ts @@ -10,7 +10,6 @@ export default new Elysia({ const uuid = params.uuid.replaceAll(`-`, ``); const icon = body.icon.toUpperCase(); const { authorization } = headers; - if(authorization == `0`) return error(401, { error: i18n(`error.premiumAccount`) }); const session = await getJWTSession(authorization, uuid); if(!session.equal && !session.isAdmin) return error(403, { error: i18n(`error.notAllowed`) }); diff --git a/src/routes/position.ts b/src/routes/position.ts index d42bcc1..2690689 100644 --- a/src/routes/position.ts +++ b/src/routes/position.ts @@ -9,7 +9,6 @@ export default new Elysia({ const uuid = params.uuid.replaceAll(`-`, ``); const position = body.position.toUpperCase(); const { authorization } = headers; - if(authorization == `0`) return error(401, { error: i18n(`error.premiumAccount`) }); const session = await getJWTSession(authorization, uuid); if(!session.equal && !session.isAdmin) return error(403, { error: i18n(`error.notAllowed`) }); diff --git a/src/routes/report.ts b/src/routes/report.ts index e52b466..9a87fb5 100644 --- a/src/routes/report.ts +++ b/src/routes/report.ts @@ -9,7 +9,6 @@ export default new Elysia({ }).use(fetchI18n).post(`/`, async ({ error, params, headers, body, i18n }) => { // Report player const uuid = params.uuid.replaceAll(`-`, ``); const { authorization } = headers; - if(authorization == `0`) return error(401, { error: i18n(`error.premiumAccount`) }); const session = await getJWTSession(authorization, uuid); if(!session.uuid) return error(403, { error: i18n(`error.notAllowed`) }); diff --git a/src/routes/root.ts b/src/routes/root.ts index 3ba8d5f..8b972a6 100644 --- a/src/routes/root.ts +++ b/src/routes/root.ts @@ -12,7 +12,6 @@ export default new Elysia() .use(fetchI18n).get(`/`, async ({ error, params, headers, i18n }) => { // Get player info const uuid = params.uuid.replaceAll(`-`, ``); const { authorization } = headers; - if(authorization == `0`) return error(401, { error: i18n(`error.premiumAccount`) }); const session = await getJWTSession(authorization, uuid); if(config.requireSessionIds && !session.uuid) return error(403, { error: i18n(`error.notAllowed`) }); @@ -49,7 +48,6 @@ export default new Elysia() const uuid = params.uuid.replaceAll(`-`, ``); const tag = body.tag.trim(); const { authorization } = headers; - if(authorization == `0`) return error(401, { error: i18n(`error.premiumAccount`) }); const session = await getJWTSession(authorization, uuid); if(!session.equal && !session.isAdmin) return error(403, { error: i18n(`error.notAllowed`) }); @@ -103,10 +101,9 @@ export default new Elysia() params: t.Object({ uuid: t.String({ description: `Your UUID` }) }), body: t.Object({ tag: t.String({ minLength: config.validation.tag.min, maxLength: config.validation.tag.max, error: `setTag.validation;;[["min", "${config.validation.tag.min}"], ["max", "${config.validation.tag.max}"]]` }) }, { error: `error.invalidBody`, additionalProperties: true }), headers: t.Object({ authorization: t.String({ error: `error.notAllowed`, description: `Your LabyConnect JWT` }) }, { error: `error.notAllowed` }) -}).post(`/admin`, async ({ error, params, headers, i18n }) => { // Change tag +}).post(`/admin`, async ({ error, params, headers, i18n }) => { // Toggle admin const uuid = params.uuid.replaceAll(`-`, ``); const { authorization } = headers; - if(authorization == `0`) return error(401, { error: i18n(`error.premiumAccount`) }); const session = await getJWTSession(authorization, uuid); if(!session.isAdmin) return error(403, { error: i18n(`error.notAllowed`) }); @@ -137,7 +134,6 @@ export default new Elysia() }).delete(`/`, async ({ error, params, headers, i18n }) => { // Delete tag const uuid = params.uuid.replaceAll(`-`, ``); const { authorization } = headers; - if(authorization == `0`) return error(401, { error: i18n(`error.premiumAccount`) }); const session = await getJWTSession(authorization, uuid); if(!session.equal && !session.isAdmin) return error(403, { error: i18n(`error.notAllowed`) }); From 6a50b93b343bc6dd4e42e8ca561cb7fda7248587 Mon Sep 17 00:00:00 2001 From: RappyTV Date: Wed, 19 Jun 2024 17:13:28 +0200 Subject: [PATCH 8/8] Bump version, remove unused packages --- bun.lockb | Bin 56722 -> 34017 bytes package.json | 4 +--- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/bun.lockb b/bun.lockb index 471b656c3d6f5ad47540bf264266b6c684a91f77..0500b3dd8b121897765db85ac3cee6ea6f26cab0 100644 GIT binary patch delta 5466 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 pr7z;yH!Z$`H(!uw!qo@*s!32 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