From 350912581c06186edd7a70939b24d347f68bac4f Mon Sep 17 00:00:00 2001 From: RappyTV Date: Tue, 17 Jun 2025 01:37:04 +0200 Subject: [PATCH 1/5] roles: Fix translations * Fix typo in translation key * Add missing translation key --- locales/en_us.json | 3 ++- src/routes/roles.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/locales/en_us.json b/locales/en_us.json index 0eb31d2..7b8880b 100644 --- a/locales/en_us.json +++ b/locales/en_us.json @@ -13,7 +13,8 @@ "playerNoTag": "This player does not have a tag!", "playerBanned": "This player is banned!", "malformedAuthHeader": "You've entered a malformed authorization header!", - "noIcon": "The icon was not found!" + "noIcon": "The icon was not found!", + "invalid_bitfield": "You provided an invalid bitfield!" }, "gift_codes": { "not_found": "Gift code not found!", diff --git a/src/routes/roles.ts b/src/routes/roles.ts index 146a4dc..ad6dc01 100644 --- a/src/routes/roles.ts +++ b/src/routes/roles.ts @@ -114,7 +114,7 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, i18n, status } updated = true; } if(permissions !== undefined && permissions !== role.permissions) { - if(permissions < 0 || permissions > 2147483647) return status(422, { error: i18n('errors.invalid_bitfield') }); + if(permissions < 0 || permissions > 2147483647) return status(422, { error: i18n('error.invalid_bitfield') }); role.permissions = permissions; updated = true; } From e5f090288e31061fe039be6b2269607f47c4ce86 Mon Sep 17 00:00:00 2001 From: RappyTV Date: Tue, 17 Jun 2025 21:31:20 +0200 Subject: [PATCH 2/5] translations: Require prefix * Add warning and sentry error if i18n call is not prefixed with $. * Prefix all translation strings with $. --- locales/en_us.json | 1 - src/index.ts | 6 +- src/libs/Ratelimiter.ts | 2 +- src/libs/i18n.ts | 8 ++ src/libs/mailer.ts | 109 ++++++++++++----------- src/middleware/database-checker.ts | 2 +- src/routes/gift-codes.ts | 38 ++++---- src/routes/index.ts | 2 +- src/routes/players/[uuid]/api-keys.ts | 52 +++++------ src/routes/players/[uuid]/bans.ts | 62 ++++++------- src/routes/players/[uuid]/connections.ts | 92 ++++++++++--------- src/routes/players/[uuid]/icon.ts | 56 ++++++------ src/routes/players/[uuid]/index.ts | 42 ++++----- src/routes/players/[uuid]/notes.ts | 30 +++---- src/routes/players/[uuid]/position.ts | 14 +-- src/routes/players/[uuid]/referral.ts | 12 +-- src/routes/players/[uuid]/reports.ts | 40 ++++----- src/routes/players/[uuid]/watchlist.ts | 18 ++-- src/routes/roles.ts | 34 +++---- src/routes/staff.ts | 82 ++++++++--------- 20 files changed, 360 insertions(+), 342 deletions(-) diff --git a/locales/en_us.json b/locales/en_us.json index 7b8880b..104e32f 100644 --- a/locales/en_us.json +++ b/locales/en_us.json @@ -12,7 +12,6 @@ "noTag": "Please set a tag first!", "playerNoTag": "This player does not have a tag!", "playerBanned": "This player is banned!", - "malformedAuthHeader": "You've entered a malformed authorization header!", "noIcon": "The icon was not found!", "invalid_bitfield": "You provided an invalid bitfield!" }, diff --git a/src/index.ts b/src/index.ts index 26d9997..6cb38cf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -101,7 +101,7 @@ const elysia = new Elysia() if(code == 'VALIDATION') { set.status = 422; error = error as ValidationError; - let errorMessage = i18n(error.message); + let errorMessage = error.message; const errorParts = errorMessage.split(';;'); errorMessage = i18n(errorParts[0]); if(errorParts.length > 1) { @@ -117,13 +117,13 @@ const elysia = new Elysia() return { error: errorMessage.trim() }; } else if(code == 'NOT_FOUND') { set.status = 404; - return { error: i18n('error.notFound') }; + return { error: i18n('$.error.notFound') }; } else { set.status = 500; captureException(error); const requestId = generateSecureCode(32); Logger.error(`An error ocurred with request ${requestId}: ${error}`); - return { error: i18n('error.unknownError'), id: requestId }; + return { error: i18n('$.error.unknownError'), id: requestId }; } }) .listen({ port: config.port, idleTimeout: 20 }); diff --git a/src/libs/Ratelimiter.ts b/src/libs/Ratelimiter.ts index 5fa870b..6758a09 100644 --- a/src/libs/Ratelimiter.ts +++ b/src/libs/Ratelimiter.ts @@ -50,7 +50,7 @@ export default class Ratelimiter { set.headers['X-RateLimit-Limit'] = String(this.maxRequests); set.headers['X-RateLimit-Remaining'] = String(ratelimitData.remaining); set.headers['X-RateLimit-Reset'] = String(ratelimitData.reset / 1000); - if(ratelimitData.limited) return error(429, { error: i18n('error.ratelimit').replaceAll('', String(Math.ceil(ratelimitData.reset / 1000))) }); + if(ratelimitData.limited) return error(429, { error: i18n('$.error.ratelimit').replaceAll('', String(Math.ceil(ratelimitData.reset / 1000))) }); } public getRatelimitData(ip: string): RatelimitData { diff --git a/src/libs/i18n.ts b/src/libs/i18n.ts index dc9e546..bd929d6 100644 --- a/src/libs/i18n.ts +++ b/src/libs/i18n.ts @@ -2,6 +2,7 @@ import { existsSync, readdirSync } from "fs"; import { join } from "path"; import Logger from "./Logger"; import players from "../database/schemas/players"; +import { captureException } from "@sentry/bun"; export type Language = Map; export type I18nFunction = (path: string) => string; @@ -44,6 +45,13 @@ export function isValidLanguage(language: string): boolean { } export function translate(path: string, language: Language): string { + if(!path.startsWith('\$\.')) { + const error = new Error(`Translation path "${path}" was not prefixed with "$."!`); + Logger.warn(error.message); + captureException(error); + } else { + path = path.slice(2); + } if(language.has(path)) return language.get(path)!; return getLanguage().get(path) || path; } diff --git a/src/libs/mailer.ts b/src/libs/mailer.ts index 7040146..dfe1423 100644 --- a/src/libs/mailer.ts +++ b/src/libs/mailer.ts @@ -26,9 +26,14 @@ const transporter = createTransport({ } } as TransportOptions); +export let enabled = mailer.enabled; + export async function verify() { transporter.verify((error) => { - if(error) Logger.error(`Invalid mailer options: ${error.message}`); + if(error) { + enabled = false; + Logger.error(`Invalid mailer options: ${error.message}`); + } else Logger.info('Mailer options verified!'); }); } @@ -55,24 +60,24 @@ export function sendBanEmail({ address, reason, duration, appealable, i18n }: { const durationOptions: MailOptions['variables'] = []; if(!permanent) { - durationOptions.push(['duration', i18n('email.banned.duration')]); - durationOptions.push(['duration_value', i18n('email.banned.until').replace('', moment(duration).format('DD.MM.YYYY HH:mm'))]); + durationOptions.push(['duration', i18n('$.email.banned.duration')]); + durationOptions.push(['duration_value', i18n('$.email.banned.until').replace('', moment(duration).format('DD.MM.YYYY HH:mm'))]); } sendEmail({ recipient: address, - subject: i18n('email.banned.subject'), + subject: i18n('$.email.banned.subject'), template: 'banned', variables: [ - ['title', i18n('email.banned.title')], - ['greeting', i18n('email.greeting')], - ['description', i18n(`email.banned.description.${permanent ? 'permanent' : 'temporary'}`)], - ['reason', i18n('email.banned.reason')], + ['title', i18n('$.email.banned.title')], + ['greeting', i18n('$.email.greeting')], + ['description', i18n(permanent ? '$.email.banned.description.permanent' : '$.email.banned.description.temporary')], + ['reason', i18n('$.email.banned.reason')], ['reason_value', reason], ['duration_visibility', permanent ? 'none' : 'initial'], ...durationOptions, - ['appeal', i18n(`email.banned.${appealable ? 'a' : 'noA'}ppeal`)], - ['footer', i18n('email.footer')], + ['appeal', i18n(appealable ? '$.email.banned.appeal' : '$.email.banned.noAppeal')], + ['footer', i18n('$.email.footer')], ] }); } @@ -80,14 +85,14 @@ export function sendBanEmail({ address, reason, duration, appealable, i18n }: { export function sendUnbanEmail(address: string, i18n: I18nFunction) { sendEmail({ recipient: address, - subject: i18n('email.unbanned.subject'), + subject: i18n('$.email.unbanned.subject'), template: 'unbanned', variables: [ - ['title', i18n('email.unbanned.title')], - ['greeting', i18n('email.greeting')], - ['unbanned', i18n('email.unbanned.unbanned')], - ['access', i18n('email.unbanned.access')], - ['footer', i18n('email.footer')], + ['title', i18n('$.email.unbanned.title')], + ['greeting', i18n('$.email.greeting')], + ['unbanned', i18n('$.email.unbanned.unbanned')], + ['access', i18n('$.email.unbanned.access')], + ['footer', i18n('$.email.footer')], ] }); } @@ -95,15 +100,15 @@ export function sendUnbanEmail(address: string, i18n: I18nFunction) { export function sendTagClearEmail(address: string, tag: string, i18n: I18nFunction) { sendEmail({ recipient: address, - subject: i18n('email.tagCleared.subject'), + subject: i18n('$.email.tagCleared.subject'), template: 'tag_cleared', variables: [ - ['title', i18n('email.tagCleared.title')], - ['greeting', i18n('email.greeting')], - ['description', i18n('email.tagCleared.description')], + ['title', i18n('$.email.tagCleared.title')], + ['greeting', i18n('$.email.greeting')], + ['description', i18n('$.email.tagCleared.description')], ['tag', `"${stripColors(tag)}"`], - ['warning', i18n('email.tagCleared.warning')], - ['footer', i18n('email.footer')], + ['warning', i18n('$.email.tagCleared.warning')], + ['footer', i18n('$.email.footer')], ] }); } @@ -111,18 +116,18 @@ export function sendTagClearEmail(address: string, tag: string, i18n: I18nFuncti export function sendTagChangeEmail(address: string, oldTag: string, newTag: string, i18n: I18nFunction) { sendEmail({ recipient: address, - subject: i18n('email.tagChanged.subject'), + subject: i18n('$.email.tagChanged.subject'), template: 'tag_changed', variables: [ - ['title', i18n('email.tagChanged.title')], - ['greeting', i18n('email.greeting')], - ['description', i18n('email.tagChanged.description')], - ['previous', i18n('email.tagChanged.previous')], + ['title', i18n('$.email.tagChanged.title')], + ['greeting', i18n('$.email.greeting')], + ['description', i18n('$.email.tagChanged.description')], + ['previous', i18n('$.email.tagChanged.previous')], ['old_tag', `"${stripColors(oldTag)}"`], - ['new', i18n('email.tagChanged.new')], + ['new', i18n('$.email.tagChanged.new')], ['new_tag', `"${stripColors(newTag)}"`], - ['warning', i18n('email.tagChanged.warning')], - ['footer', i18n('email.footer')], + ['warning', i18n('$.email.tagChanged.warning')], + ['footer', i18n('$.email.footer')], ] }); } @@ -130,18 +135,18 @@ export function sendTagChangeEmail(address: string, oldTag: string, newTag: stri export function sendPositionChangeEmail(address: string, oldPosition: string, newPosition: string, i18n: I18nFunction) { sendEmail({ recipient: address, - subject: i18n('email.positionChanged.subject'), + subject: i18n('$.email.positionChanged.subject'), template: 'position_changed', variables: [ - ['title', i18n('email.positionChanged.title')], - ['greeting', i18n('email.greeting')], - ['description', i18n('email.positionChanged.description')], - ['previous', i18n('email.positionChanged.previous')], + ['title', i18n('$.email.positionChanged.title')], + ['greeting', i18n('$.email.greeting')], + ['description', i18n('$.email.positionChanged.description')], + ['previous', i18n('$.email.positionChanged.previous')], ['old_position', capitalCase(oldPosition)], - ['new', i18n('email.positionChanged.new')], + ['new', i18n('$.email.positionChanged.new')], ['new_position', capitalCase(newPosition)], - ['warning', i18n('email.positionChanged.warning')], - ['footer', i18n('email.footer')], + ['warning', i18n('$.email.positionChanged.warning')], + ['footer', i18n('$.email.footer')], ] }); } @@ -149,18 +154,18 @@ export function sendPositionChangeEmail(address: string, oldPosition: string, ne export function sendIconTypeChangeEmail(address: string, oldIcon: string, newIcon: string, i18n: I18nFunction) { sendEmail({ recipient: address, - subject: i18n('email.iconChanged.subject'), + subject: i18n('$.email.iconChanged.subject'), template: 'icon_changed', variables: [ - ['title', i18n('email.iconChanged.title')], - ['greeting', i18n('email.greeting')], - ['description', i18n('email.iconChanged.description')], - ['previous', i18n('email.iconChanged.previous')], + ['title', i18n('$.email.iconChanged.title')], + ['greeting', i18n('$.email.greeting')], + ['description', i18n('$.email.iconChanged.description')], + ['previous', i18n('$.email.iconChanged.previous')], ['old_icon', capitalCase(oldIcon)], - ['new', i18n('email.iconChanged.new')], + ['new', i18n('$.email.iconChanged.new')], ['new_icon', capitalCase(newIcon)], - ['warning', i18n('email.iconChanged.warning')], - ['footer', i18n('email.footer')], + ['warning', i18n('$.email.iconChanged.warning')], + ['footer', i18n('$.email.footer')], ] }); } @@ -168,14 +173,14 @@ export function sendIconTypeChangeEmail(address: string, oldIcon: string, newIco export function sendIconClearEmail(address: string, i18n: I18nFunction) { sendEmail({ recipient: address, - subject: i18n('email.iconCleared.subject'), + subject: i18n('$.email.iconCleared.subject'), template: 'icon_cleared', variables: [ - ['title', i18n('email.iconCleared.title')], - ['greeting', i18n('email.greeting')], - ['description', i18n('email.iconCleared.description')], - ['warning', i18n('email.iconCleared.warning')], - ['footer', i18n('email.footer')], + ['title', i18n('$.email.iconCleared.title')], + ['greeting', i18n('$.email.greeting')], + ['description', i18n('$.email.iconCleared.description')], + ['warning', i18n('$.email.iconCleared.warning')], + ['footer', i18n('$.email.footer')], ] }); } \ No newline at end of file diff --git a/src/middleware/database-checker.ts b/src/middleware/database-checker.ts index a751ffb..94c4b9d 100644 --- a/src/middleware/database-checker.ts +++ b/src/middleware/database-checker.ts @@ -4,5 +4,5 @@ import { getI18nFunctionByLanguage } from "./fetch-i18n"; export default function checkDatabase({ error, request: { headers } }: PreContext) { const i18n = getI18nFunctionByLanguage(headers.get('x-language') || undefined); - if(!isConnected()) return error(503, { error: i18n('error.database') }); + if(!isConnected()) return error(503, { error: i18n('$.error.database') }); } \ No newline at end of file diff --git a/src/routes/gift-codes.ts b/src/routes/gift-codes.ts index 670128f..8efb2c8 100644 --- a/src/routes/gift-codes.ts +++ b/src/routes/gift-codes.ts @@ -6,7 +6,7 @@ import { formatUUID } from "../libs/game-profiles"; import giftCodes, { createGiftCode } from "../database/schemas/gift-codes"; export default (app: ElysiaApp) => app.get('/', async ({ session, i18n, status }) => { // Get gift code list - if(!session?.player?.hasPermission(Permission.ViewGiftCodes)) return status(403, { error: i18n('error.notAllowed') }); + if(!session?.player?.hasPermission(Permission.ViewGiftCodes)) return status(403, { error: i18n('$.error.notAllowed') }); const codes = await giftCodes.find(); @@ -37,12 +37,12 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, i18n, status } 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) }, - headers: t.Object({ authorization: t.String({ error: 'error.notAllowed', description: 'Your authentication token' }) }, { error: 'error.notAllowed' }) + headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) }).get('/:code', async ({ session, params, i18n, status }) => { // Get info of a specific code - if(!session?.player?.hasPermission(Permission.ViewGiftCodes)) return status(403, { error: i18n('error.notAllowed') }); + if(!session?.player?.hasPermission(Permission.ViewGiftCodes)) return status(403, { error: i18n('$.error.notAllowed') }); const code = await giftCodes.findOne({ $or: [{ id: params.code }, { code: params.code }] }); - if(!code) return status(404, { error: i18n('gift_codes.not_found') }); + if(!code) return status(404, { error: i18n('$.gift_codes.not_found') }); const { id, name, code: giftCode, uses, max_uses, gift, created_by, created_at, expires_at } = code; return { id, name, code: giftCode, uses: uses.map((uuid) => formatUUID(uuid)), max_uses, gift: { type: gift.type, value: gift.value, duration: gift.duration || null }, created_by: formatUUID(created_by), created_at: created_at.getTime(), expires_at: expires_at?.getTime() || null }; @@ -60,25 +60,25 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, i18n, status } 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) }, params: t.Object({ code: t.String({ description: 'The gift code' }) }), - headers: t.Object({ authorization: t.String({ error: 'error.notAllowed', description: 'Your authentication token' }) }, { error: 'error.notAllowed' }) + headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) }).post('/:code/redeem', async ({ session, params, i18n, status }) => { // Get info of a specific code - if(!session?.player) return status(403, { error: i18n('error.notAllowed') }); + if(!session?.player) return status(403, { error: i18n('$.error.notAllowed') }); const { player } = session; - if(!player) return status(403, { error: i18n('error.notAllowed') }); + if(!player) return status(403, { error: i18n('$.error.notAllowed') }); const code = await giftCodes.findOne({ code: params.code }); - if(!code || !code.isValid()) return status(404, { error: i18n('gift_codes.not_found') }); - if(code.uses.includes(player.uuid)) return status(422, { error: i18n('gift_codes.already_redeemed') }); + if(!code || !code.isValid()) return status(404, { error: i18n('$.gift_codes.not_found') }); + if(code.uses.includes(player.uuid)) return status(422, { error: i18n('$.gift_codes.already_redeemed') }); const { success, expiresAt } = player.addRole({ name: code.gift.value, reason: `Gift code: ${code.code}`, autoRemove: false, duration: code.gift.duration }); - if(!success) return status(409, { error: i18n('gift_codes.already_have_role') }); + if(!success) return status(409, { error: i18n('$.gift_codes.already_have_role') }); code.uses.push(player.uuid); await player.save(); await code.save(); sendGiftCodeRedeemMessage(await player.getGameProfile(), code, expiresAt); - return { message: i18n(`gift_codes.redeemed_${expiresAt ? 'temporarily' : 'permanently'}`).replace('', code.gift.value), expires_at: expiresAt?.getTime() || null }; + return { message: i18n(expiresAt ? '$.gift_codes.redeemed_temporarily' : '$.gift_codes.redeemed_permanently').replace('', code.gift.value), expires_at: expiresAt?.getTime() || null }; }, { detail: { tags: ['Gift codes'], @@ -94,9 +94,9 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, i18n, status } 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) }, params: t.Object({ code: t.String({ description: 'The gift code' }) }), - headers: t.Object({ authorization: t.String({ error: 'error.notAllowed', description: 'Your authentication token' }) }, { error: 'error.notAllowed' }) + headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) }).post('/', async ({ session, body: { name, code, role, max_uses: maxUses, code_expiration: codeExpiration, gift_duration: giftDuration }, i18n, status }) => { // Create a gift code - if(!session?.player?.hasPermission(Permission.CreateGiftCodes)) return status(403, { error: i18n('error.notAllowed') }); + if(!session?.player?.hasPermission(Permission.CreateGiftCodes)) return status(403, { error: i18n('$.error.notAllowed') }); const codeExpiresAt = codeExpiration ? new Date(codeExpiration) : null; const giftExpiresAt = giftDuration || null; @@ -148,13 +148,13 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, i18n, status } 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) }, - body: t.Object({ name: t.String({ error: 'error.wrongType;;[["field", "name"], ["type", "string"]]' }), code: t.Optional(t.String({ error: 'error.wrongType;;[["field", "code"], ["type", "string"]]' })), role: t.String({ error: 'error.wrongType;;[["field", "role"], ["type", "string"]]' }), max_uses: t.Number({ error: 'error.wrongType;;[["field", "max_uses"], ["type", "number"]]' }), code_expiration: t.Optional(t.Number({ error: 'error.wrongType;;[["field", "code_expiration"], ["type", "number"]]' })), gift_duration: t.Optional(t.Number({ error: 'error.wrongType;;[["field", "gift_duration"], ["type", "number"]]' })) }, { error: 'error.invalidBody', additionalProperties: true }), - headers: t.Object({ authorization: t.String({ error: 'error.notAllowed', description: 'Your authentication token' }) }, { error: 'error.notAllowed' }) + body: t.Object({ name: t.String({ error: '$.error.wrongType;;[["field", "name"], ["type", "string"]]' }), code: t.Optional(t.String({ error: '$.error.wrongType;;[["field", "code"], ["type", "string"]]' })), role: t.String({ error: '$.error.wrongType;;[["field", "role"], ["type", "string"]]' }), max_uses: t.Number({ error: '$.error.wrongType;;[["field", "max_uses"], ["type", "number"]]' }), code_expiration: t.Optional(t.Number({ error: '$.error.wrongType;;[["field", "code_expiration"], ["type", "number"]]' })), gift_duration: t.Optional(t.Number({ error: '$.error.wrongType;;[["field", "gift_duration"], ["type", "number"]]' })) }, { error: '$.error.invalidBody', additionalProperties: true }), + headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) }).delete('/:code', async ({ session, params, i18n, status }) => { // Delete gift code - if(!session?.player?.hasPermission(Permission.DeleteGiftCodes)) return status(403, { error: i18n('error.notAllowed') }); + if(!session?.player?.hasPermission(Permission.DeleteGiftCodes)) return status(403, { error: i18n('$.error.notAllowed') }); const code = await giftCodes.findOne({ id: params.code }); - if(!code) return status(404, { error: i18n('gift_codes.not_found') }); + if(!code) return status(404, { error: i18n('$.gift_codes.not_found') }); await code.deleteOne(); sendModLogMessage({ @@ -164,7 +164,7 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, i18n, status } code }); - return { message: i18n('gift_codes.deleted') }; + return { message: i18n('$.gift_codes.deleted') }; }, { detail: { tags: ['Gift codes'], @@ -179,5 +179,5 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, i18n, status } 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) }, params: t.Object({ code: t.String({ description: 'The gift code' }) }), - headers: t.Object({ authorization: t.String({ error: 'error.notAllowed', description: 'Your authentication token' }) }, { error: 'error.notAllowed' }) + headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) }); \ No newline at end of file diff --git a/src/routes/index.ts b/src/routes/index.ts index 136d9d4..bec3d41 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -62,7 +62,7 @@ export default (app: ElysiaApp) => app.get('/', () => ({ 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) }, query: t.Object({ - latest: t.Optional(t.String({ error: 'error.wrongType;;[["field", "element"], ["type", "string"]]' })) + latest: t.Optional(t.String({ error: '$.error.wrongType;;[["field", "element"], ["type", "string"]]' })) }, { additionalProperties: true }) }).get('/referrals', async () => { const data = await players.find(); diff --git a/src/routes/players/[uuid]/api-keys.ts b/src/routes/players/[uuid]/api-keys.ts index 2631d1d..3709f09 100644 --- a/src/routes/players/[uuid]/api-keys.ts +++ b/src/routes/players/[uuid]/api-keys.ts @@ -7,10 +7,10 @@ import { ElysiaApp } from "../../.."; import { generateSecureCode } from "../../../libs/crypto"; export default (app: ElysiaApp) => app.get('/', async ({ session, params, i18n, status }) => { // Get api key list - if(!session?.player?.hasPermission(Permission.ViewApiKeys)) return status(403, { error: i18n('error.notAllowed') }); + if(!session?.player?.hasPermission(Permission.ViewApiKeys)) return status(403, { error: i18n('$.error.notAllowed') }); const player = await players.findOne({ uuid: stripUUID(params.uuid) }); - if(!player) return status(404, { error: i18n('error.playerNotFound') }); + if(!player) return status(404, { error: i18n('$.error.playerNotFound') }); return player.api_keys.map((key) => ({ id: key.id, @@ -32,15 +32,15 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, params, i18n, 503: t.Object({ error: t.String() }, { description: 'The 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 authentication token' }) }, { error: 'error.notAllowed' }) + headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) }).get('/:id', async ({ session, params, i18n, status }) => { // Get info of specific api key - if(!session?.player?.hasPermission(Permission.ViewApiKeys)) return status(403, { error: i18n('error.notAllowed') }); + if(!session?.player?.hasPermission(Permission.ViewApiKeys)) return status(403, { error: i18n('$.error.notAllowed') }); const player = await players.findOne({ uuid: stripUUID(params.uuid) }); - if(!player) return status(404, { error: i18n('error.playerNotFound') }); + if(!player) return status(404, { error: i18n('$.error.playerNotFound') }); const key = player.getApiKey(params.id); - if(!key) return status(404, { error: i18n('api_keys.not_found') }); + if(!key) return status(404, { error: i18n('$.api_keys.not_found') }); const { name, id, created_at, last_used } = key; return { id, name, created_at: created_at.getTime(), last_used: last_used?.getTime() || null }; @@ -58,12 +58,12 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, params, i18n, 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) }, params: t.Object({ uuid: t.String({ description: 'The player\'s UUID' }), id: t.String({ description: 'The API key ID' }) }), - headers: t.Object({ authorization: t.String({ error: 'error.notAllowed', description: 'Your authentication token' }) }, { error: 'error.notAllowed' }) + headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) }).post('/', async ({ session, body: { name }, params, i18n, status }) => { // Create an API key - if(!session?.player?.hasPermission(Permission.CreateApiKeys)) return status(403, { error: i18n('error.notAllowed') }); + if(!session?.player?.hasPermission(Permission.CreateApiKeys)) return status(403, { error: i18n('$.error.notAllowed') }); const player = await players.findOne({ uuid: stripUUID(params.uuid) }); - if(!player) return status(404, { error: i18n('error.playerNotFound') }); + if(!player) return status(404, { error: i18n('$.error.playerNotFound') }); const key = player.createApiKey(name.trim()); await player.save(); @@ -96,16 +96,16 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, params, i18n, 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) }, - body: t.Object({ name: t.String({ error: 'error.wrongType;;[["field", "name"], ["type", "string"]]' }) }, { error: 'error.invalidBody', additionalProperties: true }), + body: t.Object({ name: t.String({ error: '$.error.wrongType;;[["field", "name"], ["type", "string"]]' }) }, { error: '$.error.invalidBody', additionalProperties: true }), params: t.Object({ uuid: t.String({ description: 'The player\'s UUID' }) }), - headers: t.Object({ authorization: t.String({ error: 'error.notAllowed', description: 'Your authentication token' }) }, { error: 'error.notAllowed' }) + headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) }).post('/:id/regenerate', async ({ session, params, i18n, status }) => { // Regenerate API key - if(!session?.player?.hasPermission(Permission.EditApiKeys)) return status(403, { error: i18n('error.notAllowed') }); + if(!session?.player?.hasPermission(Permission.EditApiKeys)) return status(403, { error: i18n('$.error.notAllowed') }); const player = await players.findOne({ uuid: stripUUID(params.uuid) }); - if(!player) return status(404, { error: i18n('error.playerNotFound') }); + if(!player) return status(404, { error: i18n('$.error.playerNotFound') }); const key = player.getApiKey(params.id); - if(!key) return status(404, { error: i18n('api_keys.not_found') }); + if(!key) return status(404, { error: i18n('$.api_keys.not_found') }); key.key = `sk_${generateSecureCode(32)}`; await player.save(); @@ -137,17 +137,17 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, params, i18n, 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) }, - body: t.Object({ name: t.String({ error: 'error.wrongType;;[["field", "name"], ["type", "string"]]' }) }, { error: 'error.invalidBody', additionalProperties: true }), + body: t.Object({ name: t.String({ error: '$.error.wrongType;;[["field", "name"], ["type", "string"]]' }) }, { error: '$.error.invalidBody', additionalProperties: true }), params: t.Object({ uuid: t.String({ description: 'The player\'s UUID' }), id: t.String({ description: 'The API key ID' }) }), - headers: t.Object({ authorization: t.String({ error: 'error.notAllowed', description: 'Your authentication token' }) }, { error: 'error.notAllowed' }) + headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) }).patch('/:id', async ({ session, params, body: { name }, i18n, status }) => { // Edit API key - if(!session?.player?.hasPermission(Permission.EditApiKeys)) return status(403, { error: i18n('error.notAllowed') }); + if(!session?.player?.hasPermission(Permission.EditApiKeys)) return status(403, { error: i18n('$.error.notAllowed') }); const player = await players.findOne({ uuid: stripUUID(params.uuid) }); - if(!player) return status(404, { error: i18n('error.playerNotFound') }); + if(!player) return status(404, { error: i18n('$.error.playerNotFound') }); const key = player.getApiKey(params.id); - if(!key) return status(404, { error: i18n('api_keys.not_found') }); + if(!key) return status(404, { error: i18n('$.api_keys.not_found') }); key.name = name.trim(); await player.save(); @@ -179,16 +179,16 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, params, i18n, 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) }, - body: t.Object({ name: t.String({ error: 'error.wrongType;;[["field", "name"], ["type", "string"]]' }) }, { error: 'error.invalidBody', additionalProperties: true }), + body: t.Object({ name: t.String({ error: '$.error.wrongType;;[["field", "name"], ["type", "string"]]' }) }, { error: '$.error.invalidBody', additionalProperties: true }), params: t.Object({ uuid: t.String({ description: 'The player\'s UUID' }), id: t.String({ description: 'The API key ID' }) }), - headers: t.Object({ authorization: t.String({ error: 'error.notAllowed', description: 'Your authentication token' }) }, { error: 'error.notAllowed' }) + headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) }).delete('/:id', async ({ session, params, i18n, status }) => { // Delete api key - if(!session?.player?.hasPermission(Permission.DeleteApiKeys)) return status(403, { error: i18n('error.notAllowed') }); + if(!session?.player?.hasPermission(Permission.DeleteApiKeys)) return status(403, { error: i18n('$.error.notAllowed') }); const player = await players.findOne({ uuid: stripUUID(params.uuid) }); - if(!player) return status(404, { error: i18n('error.playerNotFound') }); + if(!player) return status(404, { error: i18n('$.error.playerNotFound') }); const key = player.getApiKey(params.id); - if(!key || !player.deleteApiKey(key.id)) return status(404, { error: i18n('api_keys.not_found') }); + if(!key || !player.deleteApiKey(key.id)) return status(404, { error: i18n('$.api_keys.not_found') }); await player.save(); @@ -200,7 +200,7 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, params, i18n, key: key }); - return { message: i18n('api_keys.deleted') }; + return { message: i18n('$.api_keys.deleted') }; }, { detail: { tags: ['Admin'], @@ -215,5 +215,5 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, params, i18n, 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) }, params: t.Object({ uuid: t.String({ description: 'The player\'s UUID' }), id: t.String({ description: 'The API key ID' }) }), - headers: t.Object({ authorization: t.String({ error: 'error.notAllowed', description: 'Your authentication token' }) }, { error: 'error.notAllowed' }) + headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) }); \ No newline at end of file diff --git a/src/routes/players/[uuid]/bans.ts b/src/routes/players/[uuid]/bans.ts index b0d29d7..7ed2c0b 100644 --- a/src/routes/players/[uuid]/bans.ts +++ b/src/routes/players/[uuid]/bans.ts @@ -8,10 +8,10 @@ import { formatUUID, stripUUID } from "../../../libs/game-profiles"; import { ElysiaApp } from "../../.."; export default (app: ElysiaApp) => app.get('/', async ({ session, params, i18n, status }) => { // Get ban list - if(!session?.player?.hasPermission(Permission.ViewBans)) return status(403, { error: i18n('error.notAllowed') }); + if(!session?.player?.hasPermission(Permission.ViewBans)) return status(403, { error: i18n('$.error.notAllowed') }); const player = await players.findOne({ uuid: stripUUID(params.uuid) }); - if(!player) return status(404, { error: i18n('error.playerNotFound') }); + if(!player) return status(404, { error: i18n('$.error.playerNotFound') }); return player.bans.map((ban) => ({ appealable: ban.appeal.appealable, @@ -36,15 +36,15 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, params, i18n, 503: t.Object({ error: t.String() }, { description: 'The 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 authentication token' }) }, { error: 'error.notAllowed' }) + headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) }).get('/:id', async ({ session, params, i18n, status }) => { // Get ban info of specific ban - if(!session?.player?.hasPermission(Permission.ViewBans)) return status(403, { error: i18n('error.notAllowed') }); + if(!session?.player?.hasPermission(Permission.ViewBans)) return status(403, { error: i18n('$.error.notAllowed') }); const player = await players.findOne({ uuid: stripUUID(params.uuid) }); - if(!player) return status(404, { error: i18n('error.playerNotFound') }); + if(!player) return status(404, { error: i18n('$.error.playerNotFound') }); const ban = player.bans.find(({ id }) => id === params.id); - if(!ban) return status(404, { error: i18n('ban.not_found') }); + if(!ban) return status(404, { error: i18n('$.ban.not_found') }); const { appeal, banned_at, expires_at, id, reason, staff } = ban; return { appealable: appeal.appealable, appealed: appeal.appealable, banned_at: banned_at.getTime(), expires_at: expires_at?.getTime() || null, id, reason, staff: formatUUID(staff) }; @@ -62,13 +62,13 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, params, i18n, 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) }, params: t.Object({ uuid: t.String({ description: 'The player\'s UUID' }), id: t.String({ description: 'The ban ID' }) }), - headers: t.Object({ authorization: t.String({ error: 'error.notAllowed', description: 'Your authentication token' }) }, { error: 'error.notAllowed' }) + headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) }).post('/', async ({ session, body: { reason, appealable, duration }, params, i18n, status }) => { // Ban player - if(!session?.player?.hasPermission(Permission.CreateBans)) return status(403, { error: i18n('error.notAllowed') }); + if(!session?.player?.hasPermission(Permission.CreateBans)) return status(403, { error: i18n('$.error.notAllowed') }); const player = await players.findOne({ uuid: stripUUID(params.uuid) }); - if(!player) return status(404, { error: i18n('error.playerNotFound') }); - if(player.isBanned()) return status(409, { error: i18n('ban.already_banned') }); + if(!player) return status(404, { error: i18n('$.error.playerNotFound') }); + if(player.isBanned()) return status(409, { error: i18n('$.ban.already_banned') }); const expires = duration ? new Date(Date.now() + duration) : null; player.banPlayer({ reason, staff: session.uuid!, appealable, expiresAt: expires }); @@ -94,7 +94,7 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, params, i18n, }); } - return { message: i18n('ban.banned') }; + return { message: i18n('$.ban.banned') }; }, { detail: { tags: ['Admin'], @@ -109,15 +109,15 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, params, i18n, 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) }, - body: t.Object({ appealable: t.Optional(t.Boolean({ error: 'error.wrongType;;[["field", "appealable"], ["type", "boolean"]]' })), duration: t.Optional(t.Number({ error: 'error.wrongType;;[["field", "duration"], ["type", "number"]]' })), reason: t.String({ minLength: 1, error: 'error.wrongType;;[["field", "reason"], ["type", "string"]]' }) }, { error: 'error.invalidBody', additionalProperties: true }), + body: t.Object({ appealable: t.Optional(t.Boolean({ error: '$.error.wrongType;;[["field", "appealable"], ["type", "boolean"]]' })), duration: t.Optional(t.Number({ error: '$.error.wrongType;;[["field", "duration"], ["type", "number"]]' })), reason: t.String({ minLength: 1, error: '$.error.wrongType;;[["field", "reason"], ["type", "string"]]' }) }, { error: '$.error.invalidBody', additionalProperties: true }), params: t.Object({ uuid: t.String({ description: 'The player\'s UUID' }) }), - headers: t.Object({ authorization: t.String({ error: 'error.notAllowed', description: 'Your authentication token' }) }, { error: 'error.notAllowed' }) + headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) }).patch('/', async ({ session, body: { appealable, reason }, params, i18n, status }) => { // Update ban info - if(!session?.player?.hasPermission(Permission.EditBans)) return status(403, { error: i18n('error.notAllowed') }); + if(!session?.player?.hasPermission(Permission.EditBans)) return status(403, { error: i18n('$.error.notAllowed') }); const player = await players.findOne({ uuid: stripUUID(params.uuid) }); - if(!player) return status(404, { error: i18n('error.playerNotFound') }); - if(!player.isBanned()) return status(409, { error: i18n('ban.not_banned') }); + if(!player) return status(404, { error: i18n('$.error.playerNotFound') }); + if(!player.isBanned()) return status(409, { error: i18n('$.ban.not_banned') }); const ban = player.bans.at(0)!; ban.reason = reason; @@ -133,7 +133,7 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, params, i18n, reason }); - return { message: i18n('editBan.success') }; + return { message: i18n('$.editBan.success') }; }, { detail: { tags: ['Admin'], @@ -148,17 +148,17 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, params, i18n, 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) }, - body: t.Object({ appealable: t.Boolean({ error: 'error.wrongType;;[["field", "appealable"], ["type", "boolean"]]' }), reason: t.String({ error: 'error.wrongType;;[["field", "reason"], ["type", "string"]]' }) }, { error: 'error.invalidBody', additionalProperties: true }), + body: t.Object({ appealable: t.Boolean({ error: '$.error.wrongType;;[["field", "appealable"], ["type", "boolean"]]' }), reason: t.String({ error: '$.error.wrongType;;[["field", "reason"], ["type", "string"]]' }) }, { error: '$.error.invalidBody', additionalProperties: true }), params: t.Object({ uuid: t.String({ description: 'The player\'s UUID' }) }), - headers: t.Object({ authorization: t.String({ error: 'error.notAllowed', description: 'Your authentication token' }) }, { error: 'error.notAllowed' }) + headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) }).post('/appeal', async ({ session, body: { reason }, i18n, status }) => { // Appeal ban - if(!session?.self) return status(403, { error: i18n('error.notAllowed') }); + if(!session?.self) return status(403, { error: i18n('$.error.notAllowed') }); const { player } = session; - if(!player || !player.isBanned()) return status(404, { error: i18n('appeal.notBanned') }); + if(!player || !player.isBanned()) return status(404, { error: i18n('$.appeal.notBanned') }); const ban = player.bans.at(0)!; - if(!ban.appeal.appealable) return status(403, { error: i18n('appeal.notAppealable') }); - if(ban.appeal.appealed) return status(403, { error: i18n('appeal.alreadyAppealed') }); + if(!ban.appeal.appealable) return status(403, { error: i18n('$.appeal.notAppealable') }); + if(ban.appeal.appealed) return status(403, { error: i18n('$.appeal.alreadyAppealed') }); ban.appeal.appealed = true; ban.appeal.reason = reason; @@ -170,7 +170,7 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, params, i18n, reason ); - return { message: i18n('appeal.success') }; + return { message: i18n('$.appeal.success') }; }, { detail: { tags: ['Admin'], @@ -184,15 +184,15 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, params, i18n, 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) }, - body: t.Object({ reason: t.String({ error: 'error.wrongType;;[["field", "reason"], ["type", "string"]]' }) }, { error: 'error.invalidBody', additionalProperties: true }), + body: t.Object({ reason: t.String({ error: '$.error.wrongType;;[["field", "reason"], ["type", "string"]]' }) }, { error: '$.error.invalidBody', additionalProperties: true }), params: t.Object({ uuid: t.String({ description: 'Your UUID' }) }), - headers: t.Object({ authorization: t.String({ error: 'error.notAllowed', description: 'Your authentication token' }) }, { error: 'error.notAllowed' }) + headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) }).delete('/', async ({ session, params, i18n, status }) => { // Unban player - if(!session?.player?.hasPermission(Permission.DeleteBans)) return status(403, { error: i18n('error.notAllowed') }); + if(!session?.player?.hasPermission(Permission.DeleteBans)) return status(403, { error: i18n('$.error.notAllowed') }); const player = await players.findOne({ uuid: stripUUID(params.uuid) }); - if(!player) return status(404, { error: i18n('error.playerNotFound') }); - if(!player.isBanned()) return status(409, { error: i18n('ban.not_banned') }); + if(!player) return status(404, { error: i18n('$.error.playerNotFound') }); + if(!player.isBanned()) return status(409, { error: i18n('$.ban.not_banned') }); player.unban(); await player.save(); @@ -208,7 +208,7 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, params, i18n, sendUnbanEmail(player.connections.email.address!, getI18nFunctionByLanguage(player.last_language)); } - return { message: i18n('ban.unbanned') }; + return { message: i18n('$.ban.unbanned') }; }, { detail: { tags: ['Admin'], @@ -224,5 +224,5 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, params, i18n, 503: t.Object({ error: t.String() }, { description: 'The 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 authentication token' }) }, { error: 'error.notAllowed' }) + headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) }); \ No newline at end of file diff --git a/src/routes/players/[uuid]/connections.ts b/src/routes/players/[uuid]/connections.ts index 6a700e7..aa6a95c 100644 --- a/src/routes/players/[uuid]/connections.ts +++ b/src/routes/players/[uuid]/connections.ts @@ -1,5 +1,5 @@ import { t } from "elysia"; -import { sendEmail } from "../../../libs/mailer"; +import { enabled as isMailerEnabled, sendEmail } from "../../../libs/mailer"; import { config } from "../../../libs/config"; import { sendEmailLinkMessage } from "../../../libs/discord-notifier"; import { ElysiaApp } from "../../.."; @@ -8,13 +8,13 @@ import { generateSecureCode } from "../../../libs/crypto"; import { Permission } from "../../../types/Permission"; export default (app: ElysiaApp) => app.post('/discord', async ({ session, i18n, status }) => { // Get a discord linking code - if(!config.discordBot.notifications.accountConnections.enabled) return status(409, { error: i18n('connections.discord.disabled') }); - if(!session) return status(403, { error: i18n('error.notAllowed') }); + if(!config.discordBot.notifications.accountConnections.enabled) return status(409, { error: i18n('$.connections.discord.disabled') }); + if(!session) return status(403, { error: i18n('$.error.notAllowed') }); const { self, player } = session; - if(!self) return status(403, { error: i18n('error.notAllowed') }); + if(!self) return status(403, { error: i18n('$.error.notAllowed') }); - if(!player) return status(404, { error: i18n('error.noTag') }); - if(player.connections.discord.id) return status(409, { error: i18n('connections.discord.alreadyConnected') }); + if(!player) return status(404, { error: i18n('$.error.noTag') }); + if(player.connections.discord.id) return status(409, { error: i18n('$.connections.discord.alreadyConnected') }); if(player.connections.discord.code) return { code: player.connections.discord.code }; player.connections.discord.code = generateSecureCode(12); @@ -36,15 +36,15 @@ export default (app: ElysiaApp) => app.post('/discord', async ({ session, i18n, 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) }, params: t.Object({ uuid: t.String({ description: 'Your UUID' }) }), - headers: t.Object({ authorization: t.String({ error: 'error.notAllowed', description: 'Your authentication token' }) }, { error: 'error.notAllowed' }) + headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) }).delete('/discord', async ({ session, i18n, status }) => { // Unlink discord account - if(!config.discordBot.notifications.accountConnections.enabled) return status(409, { error: i18n('connections.discord.disabled') }); - if(!session) return status(403, { error: i18n('error.notAllowed') }); + if(!config.discordBot.notifications.accountConnections.enabled) return status(409, { error: i18n('$.connections.discord.disabled') }); + if(!session) return status(403, { error: i18n('$.error.notAllowed') }); const { self, player } = session; - if(!self && !player?.hasPermission(Permission.RemoveConnections)) return status(403, { error: i18n('error.notAllowed') }); + if(!self && !player?.hasPermission(Permission.RemoveConnections)) return status(403, { error: i18n('$.error.notAllowed') }); - if(!player) return status(404, { error: i18n('error.noTag') }); - if(!player.connections.discord.id && !player.connections.discord.code) return status(409, { error: i18n('connections.discord.notConnected') }); + if(!player) return status(404, { error: i18n('$.error.noTag') }); + if(!player.connections.discord.id && !player.connections.discord.code) return status(409, { error: i18n('$.connections.discord.notConnected') }); await onDiscordUnlink(await player.getGameProfile(), player.connections.discord.id!); @@ -52,7 +52,7 @@ export default (app: ElysiaApp) => app.post('/discord', async ({ session, i18n, player.connections.discord.code = null; await player.save(); - return { message: i18n('connections.discord.unlinked') }; + return { message: i18n('$.connections.discord.unlinked') }; }, { detail: { tags: ['Connections'], @@ -68,14 +68,15 @@ export default (app: ElysiaApp) => app.post('/discord', async ({ session, i18n, 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) }, params: t.Object({ uuid: t.String({ description: 'Your UUID' }) }), - headers: t.Object({ authorization: t.String({ error: 'error.notAllowed', description: 'Your authentication token' }) }, { error: 'error.notAllowed' }) + headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) }).post('/email', async ({ session, body: { email }, i18n, status }) => { // Send verification email - if(!session) return status(403, { error: i18n('error.notAllowed') }); + if(!isMailerEnabled) return status(409, { error: i18n('$.connections.email.disabled') }); + if(!session) return status(403, { error: i18n('$.error.notAllowed') }); const { self, player } = session; - if(!self) return status(403, { error: i18n('error.notAllowed') }); + if(!self) return status(403, { error: i18n('$.error.notAllowed') }); - if(!player) return status(404, { error: i18n('error.noTag') }); - if(player.connections.email.address) return status(409, { error: i18n('connections.email.alreadyConnected') }); + if(!player) return status(404, { error: i18n('$.error.noTag') }); + if(player.connections.email.address) return status(409, { error: i18n('$.connections.email.alreadyConnected') }); player.connections.email.address = email; player.connections.email.code = generateSecureCode(12); @@ -83,19 +84,19 @@ export default (app: ElysiaApp) => app.post('/discord', async ({ session, i18n, sendEmail({ recipient: email, - subject: i18n('email.verification.subject'), + subject: i18n('$.email.verification.subject'), template: 'verification', variables: [ - ['title', i18n('email.verification.title')], - ['greeting', i18n('email.greeting')], - ['description', i18n('email.verification.description')], + ['title', i18n('$.email.verification.title')], + ['greeting', i18n('$.email.greeting')], + ['description', i18n('$.email.verification.description')], ['code', player.connections.email.code], - ['note', i18n('email.verification.note')], - ['footer', i18n('email.footer')], + ['note', i18n('$.email.verification.note')], + ['footer', i18n('$.email.footer')], ] }); - return { message: i18n('connections.email.verificationSent') }; + return { message: i18n('$.connections.email.verificationSent') }; }, { detail: { tags: ['Connections'], @@ -110,16 +111,17 @@ export default (app: ElysiaApp) => app.post('/discord', async ({ session, i18n, 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) }, - body: t.Object({ email: t.String({ error: 'connections.email.invalidEmail', format: 'email' }) }, { error: 'error.invalidBody', additionalProperties: true }), + body: t.Object({ email: t.String({ error: '$.connections.email.invalidEmail', format: 'email' }) }, { error: '$.error.invalidBody', additionalProperties: true }), params: t.Object({ uuid: t.String({ description: 'Your UUID' }) }), - headers: t.Object({ authorization: t.String({ error: 'error.notAllowed', description: 'Your authentication token' }) }, { error: 'error.notAllowed' }) + headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) }).post('/email/:code', async ({ session, params, i18n, status }) => { // Verify email - if(!session?.self) return status(403, { error: i18n('error.notAllowed') }); + if(!isMailerEnabled) return status(409, { error: i18n('$.connections.email.disabled') }); + if(!session?.self) return status(403, { error: i18n('$.error.notAllowed') }); const { player } = session; - if(!player) return status(404, { error: i18n('error.noTag') }); - if(player.isEmailVerified()) return status(409, { error: i18n('connections.email.alreadyConnected') }); - if(player.connections.email.code != params.code) return status(403, { error: i18n('connections.email.invalidCode') }); + if(!player) return status(404, { error: i18n('$.error.noTag') }); + if(player.isEmailVerified()) return status(409, { error: i18n('$.connections.email.alreadyConnected') }); + if(player.connections.email.code != params.code) return status(403, { error: i18n('$.connections.email.invalidCode') }); player.connections.email.code = null; await player.save(); @@ -132,18 +134,18 @@ export default (app: ElysiaApp) => app.post('/discord', async ({ session, i18n, sendEmail({ recipient: player.connections.email.address!, - subject: i18n('email.verified.subject'), + subject: i18n('$.email.verified.subject'), template: 'verified', variables: [ - ['title', i18n('email.verified.title')], - ['success', i18n('email.verified.success')], - ['questions', i18n('email.verified.questions')], + ['title', i18n('$.email.verified.title')], + ['success', i18n('$.email.verified.success')], + ['questions', i18n('$.email.verified.questions')], ['link', 'https://globaltags.xyz/discord'], - ['footer', i18n('email.footer')], + ['footer', i18n('$.email.footer')], ] }); - return { message: i18n('connections.email.verified') }; + return { message: i18n('$.connections.email.verified') }; }, { detail: { tags: ['Connections'], @@ -159,14 +161,15 @@ export default (app: ElysiaApp) => app.post('/discord', async ({ session, i18n, 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) }, params: t.Object({ uuid: t.String({ description: 'Your UUID' }), code: t.String({ description: 'Your verification code' }) }), - headers: t.Object({ authorization: t.String({ error: 'error.notAllowed', description: 'Your authentication token' }) }, { error: 'error.notAllowed' }) + headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) }).delete('/email', async ({ session, i18n, status }) => { // Unlink email - if(!session) return status(403, { error: i18n('error.notAllowed') }); + if(!isMailerEnabled) return status(409, { error: i18n('$.connections.email.disabled') }); + if(!session) return status(403, { error: i18n('$.error.notAllowed') }); const { self, player } = session; - if(!self && !player?.hasPermission(Permission.RemoveConnections)) return status(403, { error: i18n('error.notAllowed') }); + if(!self && !player?.hasPermission(Permission.RemoveConnections)) return status(403, { error: i18n('$.error.notAllowed') }); - if(!player) return status(404, { error: i18n('error.noTag') }); - if(!player.connections.email.address && !player.connections.email.code) return status(400, { error: i18n('connections.email.notConnected') }); + if(!player) return status(404, { error: i18n('$.error.noTag') }); + if(!player.connections.email.address && !player.connections.email.code) return status(400, { error: i18n('$.connections.email.notConnected') }); sendEmailLinkMessage( await player.getGameProfile(), @@ -178,7 +181,7 @@ export default (app: ElysiaApp) => app.post('/discord', async ({ session, i18n, player.connections.email.code = null; await player.save(); - return { message: i18n('connections.email.unlinked') }; + return { message: i18n('$.connections.email.unlinked') }; }, { detail: { tags: ['Connections'], @@ -189,10 +192,11 @@ export default (app: ElysiaApp) => app.post('/discord', async ({ session, i18n, 400: t.Object({ error: t.String() }, { description: 'You don\'t have an email address connected' }), 403: t.Object({ error: t.String() }, { description: 'You\'re not allowed to manage connections for this player' }), 404: t.Object({ error: t.String() }, { description: 'You don\'t have an account' }), + 409: t.Object({ error: t.String() }, { description: 'Email linking is deactivated' }), 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: 'The database is not reachable' }) }, params: t.Object({ uuid: t.String({ description: 'Your UUID' }) }), - headers: t.Object({ authorization: t.String({ error: 'error.notAllowed', description: 'Your authentication token' }) }, { error: 'error.notAllowed' }) + headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) }); \ No newline at end of file diff --git a/src/routes/players/[uuid]/icon.ts b/src/routes/players/[uuid]/icon.ts index 2153095..9c73773 100644 --- a/src/routes/players/[uuid]/icon.ts +++ b/src/routes/players/[uuid]/icon.ts @@ -20,11 +20,11 @@ export function getCustomIconUrl(uuid: string, hash: string) { export default (app: ElysiaApp) => app.get('/:hash', async ({ params: { uuid, hash }, i18n, status }) => { // Get custom icon const player = await players.findOne({ uuid: stripUUID(uuid) }); - if(!player) return status(404, { error: i18n('error.noTag') }); - if(player.isBanned()) return status(403, { error: i18n('error.playerBanned') }); + if(!player) return status(404, { error: i18n('$.error.noTag') }); + if(player.isBanned()) return status(403, { error: i18n('$.error.playerBanned') }); const file = Bun.file(join('data', 'icons', player.uuid, `${hash.trim()}.png`)); - if(!(await file.exists())) return status(404, { error: i18n('error.noIcon') }); + if(!(await file.exists())) return status(404, { error: i18n('$.error.noIcon') }); return file; }, { @@ -46,13 +46,13 @@ export default (app: ElysiaApp) => app.get('/:hash', async ({ params: { uuid, ha icon = icon.toLowerCase(); const player = await getOrCreatePlayer(params.uuid); - if(session.self && player.isBanned()) return status(403, { error: i18n('error.banned') }); + if(session.self && player.isBanned()) return status(403, { error: i18n('$.error.banned') }); const isCustomIconDisallowed = session.self && snakeCase(GlobalIcon[GlobalIcon.Custom]) == icon && !session.player?.hasPermission(Permission.CustomIcon); - if(!session.player?.hasPermission(Permission.BypassValidation) && (isCustomIconDisallowed || !(capitalCase(icon) in GlobalIcon) || config.validation.icon.blacklist.includes(capitalCase(icon)))) return status(403, { error: i18n('icon.notAllowed') }); + if(!session.player?.hasPermission(Permission.BypassValidation) && (isCustomIconDisallowed || !(capitalCase(icon) in GlobalIcon) || config.validation.icon.blacklist.includes(capitalCase(icon)))) return status(403, { error: i18n('$.icon.notAllowed') }); - if(player.isBanned()) return status(403, { error: i18n('error.banned') }); - if(snakeCase(player.icon.name) == icon) return status(400, { error: i18n('icon.sameIcon') }); + if(player.isBanned()) return status(403, { error: i18n('$.error.banned') }); + if(snakeCase(player.icon.name) == icon) return status(400, { error: i18n('$.icon.sameIcon') }); const oldIcon = player.icon; player.icon.name = icon; @@ -75,7 +75,7 @@ export default (app: ElysiaApp) => app.get('/:hash', async ({ params: { uuid, ha } } - return { message: i18n(`icon.success.${session.self ? 'self' : 'admin'}`) }; + return { message: i18n(session.self ? `$.icon.success.self` : '$.icon.success.admin') }; }, { detail: { tags: ['Settings'], @@ -89,26 +89,26 @@ export default (app: ElysiaApp) => app.get('/:hash', async ({ params: { uuid, ha 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) }, - body: t.Object({ icon: t.String({ error: 'error.wrongType;;[["field", "icon"], ["type", "string"]]' }) }, { error: 'error.invalidBody', additionalProperties: true }), + body: t.Object({ icon: t.String({ error: '$.error.wrongType;;[["field", "icon"], ["type", "string"]]' }) }, { error: '$.error.invalidBody', additionalProperties: true }), params: t.Object({ uuid: t.String({ description: 'Your UUID' }) }), - headers: t.Object({ authorization: t.String({ error: 'error.notAllowed', description: 'Your authentication token' }) }, { error: 'error.notAllowed' }) + headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) }).post('/upload', async ({ session, body: { image }, params, i18n, status }) => { // Upload custom icon - if(!session || !session.self) return status(403, { error: i18n('error.notAllowed') }); + if(!session || !session.self) return status(403, { error: i18n('$.error.notAllowed') }); const player = await players.findOne({ uuid: stripUUID(params.uuid) }); - if(!player) return status(404, { error: i18n('error.noTag') }); - if(player.isBanned()) return status(403, { error: i18n('error.banned') }); - if(!player.hasPermission(Permission.CustomIcon)) return status(403, { error: i18n('icon.upload.notAllowed') }); + if(!player) return status(404, { error: i18n('$.error.noTag') }); + if(player.isBanned()) return status(403, { error: i18n('$.error.banned') }); + if(!player.hasPermission(Permission.CustomIcon)) return status(403, { error: i18n('$.icon.upload.notAllowed') }); const metadata = await sharp(await image.arrayBuffer()).metadata().catch((err: Error) => { Logger.error('Failed to read image metadata:', err.message); return null; }); - if(!metadata) return status(422, { error: i18n('icon.upload.invalidMetadata') }); - if(metadata.format != 'png') return status(422, { error: i18n('icon.upload.wrongFormat')}); - if(!metadata.height || metadata.height != metadata.width) return status(422, { error: i18n('icon.upload.wrongResolution')}); - if(metadata.height > config.validation.icon.maxResolution) return status(422, { error: i18n('icon.upload.exceedsMaxResolution').replaceAll('', config.validation.icon.maxResolution.toString()) }); + if(!metadata) return status(422, { error: i18n('$.icon.upload.invalidMetadata') }); + if(metadata.format != 'png') return status(422, { error: i18n('$.icon.upload.wrongFormat')}); + if(!metadata.height || metadata.height != metadata.width) return status(422, { error: i18n('$.icon.upload.wrongResolution')}); + if(metadata.height > config.validation.icon.maxResolution) return status(422, { error: i18n('$.icon.upload.exceedsMaxResolution').replaceAll('', config.validation.icon.maxResolution.toString()) }); player.icon.name = snakeCase(GlobalIcon[GlobalIcon.Custom]); player.icon.hash = generateSecureCode(32); @@ -120,7 +120,7 @@ export default (app: ElysiaApp) => app.get('/:hash', async ({ params: { uuid, ha player.icon.hash ); - return { message: i18n('icon.upload.success'), hash: player.icon.hash }; + return { message: i18n('$.icon.upload.success'), hash: player.icon.hash }; }, { detail: { tags: ['Settings'], @@ -134,21 +134,21 @@ export default (app: ElysiaApp) => app.get('/:hash', async ({ params: { uuid, ha 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) }, - body: t.Object({ image: t.File({ type: 'image/png', error: 'error.wrongType;;[["field", "image"], ["type", "png file"]]' }) }, { error: 'error.invalidBody', additionalProperties: true }), + body: t.Object({ image: t.File({ type: 'image/png', error: '$.error.wrongType;;[["field", "image"], ["type", "png file"]]' }) }, { error: '$.error.invalidBody', additionalProperties: true }), params: t.Object({ uuid: t.String({ description: 'Your UUID' }) }), - headers: t.Object({ authorization: t.String({ error: 'error.notAllowed', description: 'Your authentication token' }) }, { error: 'error.notAllowed' }) + headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) }).patch('/role-visibility', async ({ session, body: { visible }, params, i18n, status }) => { // Toggle role icon - if(!session || !session.self && !session.player?.hasPermission(Permission.ManagePlayerIcons)) return status(403, { error: i18n('error.notAllowed') }); + if(!session || !session.self && !session.player?.hasPermission(Permission.ManagePlayerIcons)) return status(403, { error: i18n('$.error.notAllowed') }); const player = await players.findOne({ uuid: stripUUID(params.uuid) }); - if(!player) return status(404, { error: i18n('error.noTag') }); - if(session.self && player.isBanned()) return status(403, { error: i18n('error.banned') }); - if(player.hide_role_icon == !visible) return status(409, { error: i18n(`icon.role_icon.already_${player.hide_role_icon ? 'hidden' : 'shown'}`) }); + if(!player) return status(404, { error: i18n('$.error.noTag') }); + if(session.self && player.isBanned()) return status(403, { error: i18n('$.error.banned') }); + if(player.hide_role_icon == !visible) return status(409, { error: i18n(player.hide_role_icon ? '$.icon.role_icon.already_hidden' : '$.icon.role_icon.already_shown') }); player.hide_role_icon = !visible; await player.save(); - return { message: i18n(`icon.role_icon.success.${player.hide_role_icon ? 'hidden' : 'shown'}`) }; + return { message: i18n(player.hide_role_icon ? '$.icon.role_icon.success.hidden' : '$.icon.role_icon.success.shown') }; }, { detail: { tags: ['Settings'], @@ -163,7 +163,7 @@ export default (app: ElysiaApp) => app.get('/:hash', async ({ params: { uuid, ha 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) }, - body: t.Object({ visible: t.Boolean({ error: 'error.wrongType;;[["field", "visible"], ["type", "boolean"]]' }) }, { error: 'error.invalidBody', additionalProperties: true }), + body: t.Object({ visible: t.Boolean({ error: '$.error.wrongType;;[["field", "visible"], ["type", "boolean"]]' }) }, { error: '$.error.invalidBody', additionalProperties: true }), params: t.Object({ uuid: t.String({ description: 'Your UUID' }) }), - headers: t.Object({ authorization: t.String({ error: 'error.notAllowed', description: 'Your authentication token' }) }, { error: 'error.notAllowed' }) + headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) }); \ No newline at end of file diff --git a/src/routes/players/[uuid]/index.ts b/src/routes/players/[uuid]/index.ts index 0922368..9ce4001 100644 --- a/src/routes/players/[uuid]/index.ts +++ b/src/routes/players/[uuid]/index.ts @@ -21,13 +21,13 @@ const multipleSpaces = /\s{2,}/g; export default (app: ElysiaApp) => app.get('/', async ({ session, language, params, i18n, status }) => { // Get player info if(!!session?.uuid && !!language) saveLastLanguage(session.uuid, language); if(strictAuth) { - if(!session?.uuid) return status(403, { error: i18n('error.notAllowed') }); + if(!session?.uuid) return status(403, { error: i18n('$.error.notAllowed') }); } const showBan = session?.self || session?.player?.hasPermission(Permission.ViewBans) || false; const showRoleIconVisibility = session?.self || session?.player?.hasPermission(Permission.ManagePlayerIcons) || false; const player = await players.findOne({ uuid: stripUUID(params.uuid) }); - if(!player) return status(404, { error: i18n('error.playerNoTag') }); + if(!player) return status(404, { error: i18n('$.error.playerNoTag') }); const playerIcon = snakeCase(player.icon.name); @@ -81,12 +81,12 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, language, para 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) }, params: t.Object({ uuid: t.String({ description: 'The uuid of the player you want to fetch the info of' }) }), - headers: t.Object({ authorization: strictAuth ? t.String({ error: 'error.notAllowed', description: 'Your authentication token' }) : t.Optional(t.String({ description: 'Your authentication token' })) }, { error: 'error.notAllowed' }), + headers: t.Object({ authorization: strictAuth ? t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) : t.Optional(t.String({ description: 'Your authentication token' })) }, { error: '$.error.notAllowed' }), }).get('/history', async ({ session, params, i18n, status }) => { // Get player's tag history - if(!session || session?.self && !session.player?.hasPermission(Permission.ViewTagHistory)) return status(403, { error: i18n('error.notAllowed') }); + if(!session || session?.self && !session.player?.hasPermission(Permission.ViewTagHistory)) return status(403, { error: i18n('$.error.notAllowed') }); const player = await players.findOne({ uuid: stripUUID(params.uuid) }); - if(!player) return status(404, { error: i18n('error.playerNoTag') }); + if(!player) return status(404, { error: i18n('$.error.playerNoTag') }); return player.history.map((tag) => ({ tag, @@ -105,12 +105,12 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, language, para 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) }, params: t.Object({ uuid: t.String({ description: 'The uuid of the player you want to fetch the info of' }) }), - headers: t.Object({ authorization: t.String({ error: 'error.notAllowed', description: 'Your authentication token' }) }, { error: 'error.notAllowed' }), + headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }), }).post('/', async ({ session, body: { tag }, params, i18n, status }) => { // Change tag - if(!session || !session.self && !session.player?.hasPermission(Permission.ManagePlayerTags)) return status(403, { error: i18n('error.notAllowed') }); + if(!session || !session.self && !session.player?.hasPermission(Permission.ManagePlayerTags)) return status(403, { error: i18n('$.error.notAllowed') }); const player = await getOrCreatePlayer(params.uuid); - if(session.self && player.isBanned()) return status(403, { error: i18n('error.banned') }); + if(session.self && player.isBanned()) return status(403, { error: i18n('$.error.banned') }); let isWatched = false; let isWatchedInitially = false; @@ -118,10 +118,10 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, language, para if(!session.player?.hasPermission(Permission.BypassValidation)) { tag = tag.trim().replace(multipleSpaces, ' ').replace(colorCodesWithSpaces, '').replace(hexColorCodesWithSpaces, ''); const strippedTag = stripColors(tag); - if(strippedTag == '') return status(422, { error: i18n('setTag.empty') }); - if(strippedTag.length < min || strippedTag.length > max) return status(422, { error: i18n('setTag.validation').replace('', String(min)).replace('', String(max)) }); + if(strippedTag == '') return status(422, { error: i18n('$.setTag.empty') }); + if(strippedTag.length < min || strippedTag.length > max) return status(422, { error: i18n('$.setTag.validation').replace('', String(min)).replace('', String(max)) }); const blacklistedWord = blacklist.find((word) => strippedTag.toLowerCase().includes(word)); - if(blacklistedWord) return status(422, { error: i18n('setTag.blacklisted').replaceAll('', blacklistedWord) }); + if(blacklistedWord) return status(422, { error: i18n('$.setTag.blacklisted').replaceAll('', blacklistedWord) }); isWatched = (player && player.watchlist) || watchlist.some((word) => { if(strippedTag.toLowerCase().includes(word)) { Logger.warn(`Now watching ${player.uuid} for matching "${word}" in "${tag}".`); @@ -133,7 +133,7 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, language, para }); } - if(player.tag == tag) return status(409, { error: i18n('setTag.sameTag') }); + if(player.tag == tag) return status(409, { error: i18n('$.setTag.sameTag') }); const oldTag = player.tag; player.tag = tag; @@ -159,7 +159,7 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, language, para } if(isWatched && !isWatchedInitially) sendWatchlistTagUpdateMessage(gameProfile, tag); - return { message: i18n(`setTag.success.${session.self ? 'self' : 'admin'}`) }; + return { message: i18n(session.self ? '$.setTag.success.self' : '$.setTag.success.admin') }; }, { detail: { tags: ['Settings'], @@ -174,15 +174,15 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, language, para 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) }, params: t.Object({ uuid: t.String({ description: 'Your UUID' }) }), - body: t.Object({ tag: t.String({ error: 'error.wrongType;;[["field", "tag"], ["type", "string"]]' }) }, { error: 'error.invalidBody', additionalProperties: true }), - headers: t.Object({ authorization: t.String({ error: 'error.notAllowed', description: 'Your authentication token' }) }, { error: 'error.notAllowed' }) + body: t.Object({ tag: t.String({ error: '$.error.wrongType;;[["field", "tag"], ["type", "string"]]' }) }, { error: '$.error.invalidBody', additionalProperties: true }), + headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) }).delete('/', async ({ session, params, i18n, status }) => { // Delete tag - if(!session || !session.self && !session.player?.hasPermission(Permission.ManagePlayerTags)) return status(403, { error: i18n('error.notAllowed') }); + if(!session || !session.self && !session.player?.hasPermission(Permission.ManagePlayerTags)) return status(403, { error: i18n('$.error.notAllowed') }); const player = await players.findOne({ uuid: stripUUID(params.uuid) }); - if(!player) return status(404, { error: i18n('error.noTag') }); - if(session.self && player.isBanned()) return status(403, { error: i18n('error.banned') }); - if(!player.tag) return status(404, { error: i18n('error.noTag') }); + if(!player) return status(404, { error: i18n('$.error.noTag') }); + if(session.self && player.isBanned()) return status(403, { error: i18n('$.error.banned') }); + if(!player.tag) return status(404, { error: i18n('$.error.noTag') }); if(!session.self && session.player) { sendModLogMessage({ @@ -200,7 +200,7 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, language, para } await player.save(); - return { message: i18n(`resetTag.success.${session.self ? 'self' : 'admin'}`) }; + return { message: i18n(session.self ? '$.resetTag.success.self' : '$.resetTag.success.admin') }; }, { detail: { tags: ['Settings'], @@ -214,5 +214,5 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, language, para 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) }, params: t.Object({ uuid: t.String({ description: 'Your UUID' }) }), - headers: t.Object({ authorization: t.String({ error: 'error.notAllowed', description: 'Your authentication token' }) }, { error: 'error.notAllowed' }) + headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) }); \ No newline at end of file diff --git a/src/routes/players/[uuid]/notes.ts b/src/routes/players/[uuid]/notes.ts index 2792e73..4f596ac 100644 --- a/src/routes/players/[uuid]/notes.ts +++ b/src/routes/players/[uuid]/notes.ts @@ -9,10 +9,10 @@ import { ElysiaApp } from "../../.."; const { validation } = config; export default (app: ElysiaApp) => app.get('/', async ({ session, params, i18n, status }) => { // Get notes - if(!session?.player?.hasPermission(Permission.ViewNotes)) return status(403, { error: i18n('error.notAllowed') }); + if(!session?.player?.hasPermission(Permission.ViewNotes)) return status(403, { error: i18n('$.error.notAllowed') }); const player = await players.findOne({ uuid: stripUUID(params.uuid) }); - if(!player) return status(404, { error: i18n('error.playerNotFound') }); + if(!player) return status(404, { error: i18n('$.error.playerNotFound') }); return player.notes.map((note) => ({ id: note.id, @@ -34,15 +34,15 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, params, i18n, 503: t.Object({ error: t.String() }, { description: 'The 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 authentication token' }) }, { error: 'error.notAllowed' }) + headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) }).get('/:id', async ({ session, params: { uuid, id }, i18n, status }) => { // Get specific note - if(!session?.player?.hasPermission(Permission.ViewNotes)) return status(403, { error: i18n('error.notAllowed') }); + if(!session?.player?.hasPermission(Permission.ViewNotes)) return status(403, { error: i18n('$.error.notAllowed') }); const player = await players.findOne({ uuid: stripUUID(uuid) }); - if(!player) return status(404, { error: i18n('error.playerNotFound') }); + if(!player) return status(404, { error: i18n('$.error.playerNotFound') }); const note = player.notes.find((note) => note.id == id); - if(!note) return status(404, { error: i18n('notes.delete.not_found') }); + if(!note) return status(404, { error: i18n('$.notes.delete.not_found') }); return { id: note.id, @@ -64,13 +64,13 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, params, i18n, 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) }, params: t.Object({ uuid: t.String({ description: 'The player\'s UUID' }), id: t.String({ description: 'The note ID' }) }), - headers: t.Object({ authorization: t.String({ error: 'error.notAllowed', description: 'Your authentication token' }) }, { error: 'error.notAllowed' }) + headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) }).post('/', async ({ session, body: { note }, params, i18n, status }) => { // Add note to player - if(!session?.player?.hasPermission(Permission.CreateNotes)) return status(403, { error: i18n('error.notAllowed') }); + if(!session?.player?.hasPermission(Permission.CreateNotes)) return status(403, { error: i18n('$.error.notAllowed') }); const uuid = stripUUID(params.uuid); const player = await players.findOne({ uuid }); - if(!player) return status(404, { error: i18n('error.playerNotFound') }); + if(!player) return status(404, { error: i18n('$.error.playerNotFound') }); player.createNote({ text: note, author: session.uuid! }); await player.save(); @@ -83,7 +83,7 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, params, i18n, note }); - return { message: i18n('notes.create.success') }; + return { message: i18n('$.notes.create.success') }; }, { detail: { tags: ['Admin'], @@ -97,11 +97,11 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, params, i18n, 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) }, - body: t.Object({ note: t.String({ maxLength: validation.notes.maxLength, error: `note.create.max_length;;[["max", "${validation.notes.maxLength}"]]` }) }, { error: 'error.invalidBody', additionalProperties: true }), + body: t.Object({ note: t.String({ maxLength: validation.notes.maxLength, error: `$.notes.create.max_length;;[["max", "${validation.notes.maxLength}"]]` }) }, { error: '$.error.invalidBody', additionalProperties: true }), params: t.Object({ uuid: t.String({ description: 'The player\'s UUID' }) }), - headers: t.Object({ authorization: t.String({ error: 'error.notAllowed', description: 'Your authentication token' }) }, { error: 'error.notAllowed' }) + headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) }).delete('/:id', async ({ session, params: { uuid, id }, i18n, status }) => { // Delete note - if(!session?.player?.hasPermission(Permission.DeleteNotes)) return status(403, { error: i18n('error.notAllowed') }); + if(!session?.player?.hasPermission(Permission.DeleteNotes)) return status(403, { error: i18n('$.error.notAllowed') }); const player = await players.findOne({ uuid: stripUUID(uuid) }); if(!player) return status(404, { error: i18n(`error.playerNotFound`) }); @@ -120,7 +120,7 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, params, i18n, note: note.text }); - return { message: i18n(`notes.delete.success`) }; + return { message: i18n('$.notes.delete.success') }; }, { detail: { tags: ['Admin'], @@ -135,5 +135,5 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, params, i18n, 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) }, params: t.Object({ uuid: t.String({ description: 'The player\'s UUID' }), id: t.String({ description: 'The note ID' }) }), - headers: t.Object({ authorization: t.String({ error: 'error.notAllowed', description: 'Your authentication token' }) }, { error: 'error.notAllowed' }) + headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) }); \ No newline at end of file diff --git a/src/routes/players/[uuid]/position.ts b/src/routes/players/[uuid]/position.ts index ecb20cf..57d12f4 100644 --- a/src/routes/players/[uuid]/position.ts +++ b/src/routes/players/[uuid]/position.ts @@ -10,14 +10,14 @@ import { sendTagChangeEmail } from "../../../libs/mailer"; import { getI18nFunctionByLanguage } from "../../../middleware/fetch-i18n"; export default (app: ElysiaApp) => app.post('/', async ({ session, body: { position }, params, i18n, status }) => { // Change tag position - if(!session || !session.self && !session.player?.hasPermission(Permission.ManagePlayerPositions)) return status(403, { error: i18n('error.notAllowed') }); + if(!session || !session.self && !session.player?.hasPermission(Permission.ManagePlayerPositions)) return status(403, { error: i18n('$.error.notAllowed') }); position = position.toLowerCase(); - if(!(capitalCase(position) in GlobalPosition)) return status(422, { error: i18n('position.invalid') }); + if(!(capitalCase(position) in GlobalPosition)) return status(422, { error: i18n('$.position.invalid') }); const player = await getOrCreatePlayer(params.uuid); - if(session.self && player.isBanned()) return status(403, { error: i18n('error.banned') }); - if(snakeCase(player.position) == position) return status(400, { error: i18n('position.samePosition') }); + if(session.self && player.isBanned()) return status(403, { error: i18n('$.error.banned') }); + if(snakeCase(player.position) == position) return status(400, { error: i18n('$.position.samePosition') }); const oldPosition = player.position; player.position = position; @@ -40,7 +40,7 @@ export default (app: ElysiaApp) => app.post('/', async ({ session, body: { posit } } - return { message: i18n(`position.success.${session.self ? 'self' : 'admin'}`) }; + return { message: i18n(session.self ? '$.position.success.self' : '$.position.success.admin') }; }, { detail: { tags: ['Settings'], @@ -55,7 +55,7 @@ export default (app: ElysiaApp) => app.post('/', async ({ session, body: { posit 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) }, - body: t.Object({ position: t.String({ error: 'error.wrongType;;[["field", "position"], ["type", "string"]]' }) }, { error: 'error.invalidBody', additionalProperties: true }), + body: t.Object({ position: t.String({ error: '$.error.wrongType;;[["field", "position"], ["type", "string"]]' }) }, { error: '$.error.invalidBody', additionalProperties: true }), params: t.Object({ uuid: t.String({ description: 'Your UUID' }) }), - headers: t.Object({ authorization: t.String({ error: 'error.notAllowed', description: 'Your authentication token' }) }, { error: 'error.notAllowed' }) + headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) }); \ No newline at end of file diff --git a/src/routes/players/[uuid]/referral.ts b/src/routes/players/[uuid]/referral.ts index 2339e9a..b0e4402 100644 --- a/src/routes/players/[uuid]/referral.ts +++ b/src/routes/players/[uuid]/referral.ts @@ -5,14 +5,14 @@ import { stripUUID } from "../../../libs/game-profiles"; import { ElysiaApp } from "../../.."; export default (app: ElysiaApp) => app.post('/', async ({ session, params, i18n, status }) => { // Mark player as referrer - if(!session?.uuid) return status(403, { error: i18n('error.notAllowed') }); - if(session.self) return status(403, { error: i18n('referral.self') }); + if(!session?.uuid) return status(403, { error: i18n('$.error.notAllowed') }); + if(session.self) return status(403, { error: i18n('$.referral.self') }); const player = await players.findOne({ uuid: stripUUID(params.uuid) }); - if(!player) return status(404, { error: i18n('error.playerNotFound') }); + if(!player) return status(404, { error: i18n('$.error.playerNotFound') }); const executor = await getOrCreatePlayer(session.uuid); - if(executor.referrals.has_referred) return status(409, { error: i18n('referral.alreadyReferred') }); + if(executor.referrals.has_referred) return status(409, { error: i18n('$.referral.alreadyReferred') }); player.addReferral(session.uuid); await player.save(); @@ -21,7 +21,7 @@ export default (app: ElysiaApp) => app.post('/', async ({ session, params, i18n, executor.save(); sendReferralMessage(await player.getGameProfile(), await executor.getGameProfile()); - return { message: i18n('referral.success') }; + return { message: i18n('$.referral.success') }; }, { detail: { tags: ['Interactions'], @@ -37,5 +37,5 @@ export default (app: ElysiaApp) => app.post('/', async ({ session, params, i18n, 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) }, params: t.Object({ uuid: t.String({ description: 'The UUID of the player you want to refer to' }) }), - headers: t.Object({ authorization: t.String({ error: 'error.notAllowed', description: 'Your authentication token' }) }, { error: 'error.notAllowed' }) + headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) }); \ No newline at end of file diff --git a/src/routes/players/[uuid]/reports.ts b/src/routes/players/[uuid]/reports.ts index 8eb0e15..2bd6961 100644 --- a/src/routes/players/[uuid]/reports.ts +++ b/src/routes/players/[uuid]/reports.ts @@ -6,10 +6,10 @@ import { formatUUID, stripUUID } from "../../../libs/game-profiles"; import { ElysiaApp } from "../../.."; export default (app: ElysiaApp) => app.get('/', async ({ session, params, i18n, status }) => { // Get reports - if(!session?.player?.hasPermission(Permission.ViewReports)) return status(403, { error: i18n('error.notAllowed') }); + if(!session?.player?.hasPermission(Permission.ViewReports)) return status(403, { error: i18n('$.error.notAllowed') }); const player = await players.findOne({ uuid: stripUUID(params.uuid) }); - if(!player) return status(404, { error: i18n('error.playerNotFound') }); + if(!player) return status(404, { error: i18n('$.error.playerNotFound') }); return player.reports.map((report) => ({ id: report.id, @@ -32,21 +32,21 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, params, i18n, 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) }, params: t.Object({ uuid: t.String({ description: 'The UUID of the player you want to get the reports of' }) }), - headers: t.Object({ authorization: t.String({ error: 'error.notAllowed', description: 'Your authentication token' }) }, { error: 'error.notAllowed' }) + headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) }).post('/', async ({ session, body: { reason }, params, i18n, status }) => { // Report player - if(!session?.player) return status(403, { error: i18n('error.notAllowed') }); + if(!session?.player) return status(403, { error: i18n('$.error.notAllowed') }); - if(session.self) return status(403, { error: i18n('report.self') }); + if(session.self) return status(403, { error: i18n('$.report.self') }); const player = await players.findOne({ uuid: stripUUID(params.uuid) }); - if(!player) return status(404, { error: i18n('error.playerNoTag') }); - if(player.isBanned()) return status(403, { error: i18n('ban.already_banned') }); - if(player.hasPermission(Permission.ReportImmunity)) return status(403, { error: i18n('report.immune') }); - if(!player.tag) return status(404, { error: i18n('error.playerNoTag') }); + if(!player) return status(404, { error: i18n('$.error.playerNoTag') }); + if(player.isBanned()) return status(403, { error: i18n('$.ban.already_banned') }); + if(player.hasPermission(Permission.ReportImmunity)) return status(403, { error: i18n('$.report.immune') }); + if(!player.tag) return status(404, { error: i18n('$.error.playerNoTag') }); const reporter = await getOrCreatePlayer(session.uuid!); - if(reporter.isBanned()) return status(403, { error: i18n('error.banned') }); - if(player.reports.some((report) => report.by == reporter.uuid && report.reported_tag == player.tag)) return status(409, { error: i18n('report.alreadyReported') }); - if(reason.trim() == '') return status(422, { error: i18n('report.invalidReason') }); + if(reporter.isBanned()) return status(403, { error: i18n('$.error.banned') }); + if(player.reports.some((report) => report.by == reporter.uuid && report.reported_tag == player.tag)) return status(409, { error: i18n('$.report.alreadyReported') }); + if(reason.trim() == '') return status(422, { error: i18n('$.report.invalidReason') }); player.createReport({ by: reporter.uuid, @@ -61,7 +61,7 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, params, i18n, tag: player.tag, reason }); - return { message: i18n('report.success') }; + return { message: i18n('$.report.success') }; }, { detail: { tags: ['Interactions'], @@ -76,17 +76,17 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, params, i18n, 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) }, - body: t.Object({ reason: t.String({ minLength: 2, maxLength: 200, error: 'report.validation;;[["min", "2"], ["max", "200"]]', description: 'The report reason' }) }, { error: 'error.invalidBody', additionalProperties: true }), + body: t.Object({ reason: t.String({ minLength: 2, maxLength: 200, error: '$.report.validation;;[["min", "2"], ["max", "200"]]', description: 'The report reason' }) }, { error: '$.error.invalidBody', additionalProperties: true }), params: t.Object({ uuid: t.String({ description: 'The UUID of the player you want to report' }) }), - headers: t.Object({ authorization: t.String({ error: 'error.notAllowed', description: 'Your authentication token' }) }, { error: 'error.notAllowed' }) + headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) }).delete('/:id', async ({ session, params: { uuid, id }, i18n, status }) => { // Delete report - if(!session?.player?.hasPermission(Permission.DeleteReports)) return status(403, { error: i18n('error.notAllowed') }); + if(!session?.player?.hasPermission(Permission.DeleteReports)) return status(403, { error: i18n('$.error.notAllowed') }); const player = await players.findOne({ uuid: stripUUID(uuid) }); - if(!player) return status(404, { error: i18n(`error.playerNotFound`) }); + if(!player) return status(404, { error: i18n(`$.error.playerNotFound`) }); const report = player.reports.find((report) => report.id == id.trim()); - if(!report) return status(404, { error: i18n(`report.delete.not_found`) }); + if(!report) return status(404, { error: i18n(`$.report.delete.not_found`) }); player.deleteReport(report.id); await player.save(); @@ -99,7 +99,7 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, params, i18n, report: `\`${report.reason}\` (\`#${report.id}\`)` }); - return { message: i18n(`report.delete.success`) }; + return { message: i18n(`$.report.delete.success`) }; }, { detail: { tags: ['Admin'], @@ -114,5 +114,5 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, params, i18n, 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) }, params: t.Object({ uuid: t.String({ description: 'The player\'s UUID' }), id: t.String({ description: 'The report ID' }) }), - headers: t.Object({ authorization: t.String({ error: 'error.notAllowed', description: 'Your authentication token' }) }, { error: 'error.notAllowed' }) + headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) });; \ No newline at end of file diff --git a/src/routes/players/[uuid]/watchlist.ts b/src/routes/players/[uuid]/watchlist.ts index 0462ffc..3f3c337 100644 --- a/src/routes/players/[uuid]/watchlist.ts +++ b/src/routes/players/[uuid]/watchlist.ts @@ -6,10 +6,10 @@ import { stripUUID } from "../../../libs/game-profiles"; import { Permission } from "../../../types/Permission"; export default (app: ElysiaApp) => app.get('/', async ({ session, params, i18n, status }) => { // Watch player - if(!session?.player?.hasPermission(Permission.ManageWatchlistEntries)) return status(403, { error: i18n('error.notAllowed') }); + if(!session?.player?.hasPermission(Permission.ManageWatchlistEntries)) return status(403, { error: i18n('$.error.notAllowed') }); const player = await players.findOne({ uuid: stripUUID(params.uuid) }); - if(!player) return status(404, { error: i18n('error.playerNotFound') }); + if(!player) return status(404, { error: i18n('$.error.playerNotFound') }); return { watched: player.watchlist }; }, { @@ -26,13 +26,13 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, params, i18n, 503: t.Object({ error: t.String() }, { description: 'The 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 authentication token' }) }, { error: 'error.notAllowed' }) + headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) }).patch('/', async ({ session, body: { watched }, params, i18n, status }) => { // Watch player - if(!session?.player?.hasPermission(Permission.ManageWatchlistEntries)) return status(403, { error: i18n('error.notAllowed') }); + if(!session?.player?.hasPermission(Permission.ManageWatchlistEntries)) return status(403, { error: i18n('$.error.notAllowed') }); const player = await players.findOne({ uuid: stripUUID(params.uuid) }); - if(!player) return status(404, { error: i18n('error.playerNotFound') }); - if(player.watchlist == watched) return status(409, { error: i18n(`watchlist.${player.watchlist ? 'already' : 'not'}_watched`) }); + if(!player) return status(404, { error: i18n('$.error.playerNotFound') }); + if(player.watchlist == watched) return status(409, { error: i18n(player.watchlist ? '$.watchlist.already_watched' : '$.watchlist.not_watched') }); player.watchlist = watched; await player.save(); @@ -44,7 +44,7 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, params, i18n, discord: false }); - return { message: i18n(`watchlist.success.${player.watchlist ? 'watch' : 'unwatch'}`) }; + return { message: i18n(player.watchlist ? `$.watchlist.success.watch` : '$.watchlist.success.unwatch') }; }, { detail: { tags: ['Admin'], @@ -59,7 +59,7 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, params, i18n, 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) }, - body: t.Object({ watched: t.Boolean({ error: 'error.wrongType;;[["field", "watched"], ["type", "boolean"]]' }) }, { error: 'error.invalidBody', additionalProperties: true }), + body: t.Object({ watched: t.Boolean({ error: '$.error.wrongType;;[["field", "watched"], ["type", "boolean"]]' }) }, { error: '$.error.invalidBody', additionalProperties: true }), params: t.Object({ uuid: t.String({ description: 'The player\'s UUID' }) }), - headers: t.Object({ authorization: t.String({ error: 'error.notAllowed', description: 'Your authentication token' }) }, { error: 'error.notAllowed' }) + headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) }); \ No newline at end of file diff --git a/src/routes/roles.ts b/src/routes/roles.ts index ad6dc01..2fe76c4 100644 --- a/src/routes/roles.ts +++ b/src/routes/roles.ts @@ -5,7 +5,7 @@ import roles, { getCachedRoles, getNextPosition, updateRoleCache } from "../data import { ElysiaApp } from ".."; export default (app: ElysiaApp) => app.get('/', async ({ session, i18n, status }) => { // Get roles - if(!session?.player?.hasPermission(Permission.ViewRoles)) return status(403, { error: i18n('error.notAllowed') }); + if(!session?.player?.hasPermission(Permission.ViewRoles)) return status(403, { error: i18n('$.error.notAllowed') }); return getCachedRoles().map((role) => ({ id: role.id, @@ -27,12 +27,12 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, i18n, status } 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) }, - headers: t.Object({ authorization: t.String({ error: 'error.notAllowed', description: 'Your authentication token' }) }, { error: 'error.notAllowed' }) + headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) }).get('/:id', async ({ session, params: { id }, i18n, status }) => { // Get specific role - if(!session?.player?.hasPermission(Permission.ViewRoles)) return status(403, { error: i18n('error.notAllowed') }); + if(!session?.player?.hasPermission(Permission.ViewRoles)) return status(403, { error: i18n('$.error.notAllowed') }); const role = getCachedRoles().find((role) => role.id == id); - if(!role) return status(404, { error: i18n('roles.not_found') }); + if(!role) return status(404, { error: i18n('$.roles.not_found') }); return { id: role.id, @@ -56,9 +56,9 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, i18n, status } 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) }, params: t.Object({ id: t.String({ description: 'The role ID' }) }), - headers: t.Object({ authorization: t.String({ error: 'error.notAllowed', description: 'Your authentication token' }) }, { error: 'error.notAllowed' }) + headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) }).post('/', async ({ session, body, i18n, status }) => { // Create role - if(!session?.player?.hasPermission(Permission.CreateRoles)) return status(403, { error: i18n('error.notAllowed') }); + if(!session?.player?.hasPermission(Permission.CreateRoles)) return status(403, { error: i18n('$.error.notAllowed') }); const role = await roles.insertOne({ name: body.name.trim(), @@ -96,13 +96,13 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, i18n, status } 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) }, - body: t.Object({ name: t.String({ error: 'error.wrongType;;[["field", "name"], ["type", "string"]]' }), color: t.Optional(t.Nullable(t.String({ minLength: 6, maxLength: 6, error: 'error.wrongType;;[["field", "color"], ["type", "string"]]' }))), permissions: t.Optional(t.Integer({ error: 'error.wrongType;;[["field", "permissions"], ["type", "integer"]]' })) }, { error: 'error.invalidBody', additionalProperties: true }), - headers: t.Object({ authorization: t.String({ error: 'error.notAllowed', description: 'Your authentication token' }) }, { error: 'error.notAllowed' }) + body: t.Object({ name: t.String({ error: '$.error.wrongType;;[["field", "name"], ["type", "string"]]' }), color: t.Optional(t.Nullable(t.String({ minLength: 6, maxLength: 6, error: '$.error.wrongType;;[["field", "color"], ["type", "string"]]' }))), permissions: t.Optional(t.Integer({ error: '$.error.wrongType;;[["field", "permissions"], ["type", "integer"]]' })) }, { error: '$.error.invalidBody', additionalProperties: true }), + headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) }).patch('/:id', async ({ session, params, body: { name, color, permissions }, i18n, status }) => { // Edit role - if(!session?.player?.hasPermission(Permission.DeleteRoles)) return status(403, { error: i18n('error.notAllowed') }); + if(!session?.player?.hasPermission(Permission.DeleteRoles)) return status(403, { error: i18n('$.error.notAllowed') }); const role = await roles.findOne({ id: params.id }); - if(!role) return status(404, { error: i18n('roles.not_found') }); + if(!role) return status(404, { error: i18n('$.roles.not_found') }); let updated = false; if(name && name.trim() !== role.name) { @@ -114,7 +114,7 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, i18n, status } updated = true; } if(permissions !== undefined && permissions !== role.permissions) { - if(permissions < 0 || permissions > 2147483647) return status(422, { error: i18n('error.invalid_bitfield') }); + if(permissions < 0 || permissions > 2147483647) return status(422, { error: i18n('$.error.invalid_bitfield') }); role.permissions = permissions; updated = true; } @@ -152,15 +152,15 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, i18n, status } 429: t.Object({ error: t.String() }, { description: 'You\'re ratelimited' }), 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) }, - body: t.Object({ name: t.Optional(t.String({ error: 'error.wrongType;;[["field", "name"], ["type", "string"]]' })), color: t.Optional(t.Nullable(t.String({ minLength: 6, maxLength: 6, error: 'error.wrongType;;[["field", "color"], ["type", "string"]]' }))), permissions: t.Optional(t.Integer({ error: 'error.wrongType;;[["field", "permissions"], ["type", "integer"]]' })) }, { error: 'error.invalidBody', additionalProperties: true }), + body: t.Object({ name: t.Optional(t.String({ error: '$.error.wrongType;;[["field", "name"], ["type", "string"]]' })), color: t.Optional(t.Nullable(t.String({ minLength: 6, maxLength: 6, error: '$.error.wrongType;;[["field", "color"], ["type", "string"]]' }))), permissions: t.Optional(t.Integer({ error: '$.error.wrongType;;[["field", "permissions"], ["type", "integer"]]' })) }, { error: '$.error.invalidBody', additionalProperties: true }), params: t.Object({ id: t.String({ description: 'The role ID' }) }), - headers: t.Object({ authorization: t.String({ error: 'error.notAllowed', description: 'Your authentication token' }) }, { error: 'error.notAllowed' }) + headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) }) // TODO: Implement route to patch all roles at once .delete('/:id', async ({ session, params, i18n, status }) => { // Delete role - if(!session?.player?.hasPermission(Permission.DeleteRoles)) return status(403, { error: i18n('error.notAllowed') }); + if(!session?.player?.hasPermission(Permission.DeleteRoles)) return status(403, { error: i18n('$.error.notAllowed') }); const role = await roles.findOne({ id: params.id }); - if(!role) return status(404, { error: i18n('roles.not_found') }); + if(!role) return status(404, { error: i18n('$.roles.not_found') }); sendModLogMessage({ logType: ModLogType.DeleteRole, @@ -172,7 +172,7 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, i18n, status } await role.deleteOne(); updateRoleCache(); - return { message: i18n('roles.delete.success') }; + return { message: i18n('$.roles.delete.success') }; }, { detail: { tags: ['Roles'], @@ -187,5 +187,5 @@ export default (app: ElysiaApp) => app.get('/', async ({ session, i18n, status } 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) }, params: t.Object({ id: t.String({ description: 'The role ID' }) }), - headers: t.Object({ authorization: t.String({ error: 'error.notAllowed', description: 'Your authentication token' }) }, { error: 'error.notAllowed' }) + headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) }); \ No newline at end of file diff --git a/src/routes/staff.ts b/src/routes/staff.ts index 13c8731..5688607 100644 --- a/src/routes/staff.ts +++ b/src/routes/staff.ts @@ -39,7 +39,7 @@ export default (app: ElysiaApp) => app.get('/', async () => { } }).group('/categories', (app) => app.get('/', async ({ session, i18n, status }) => { - if(!session?.player?.hasPermission(Permission.ViewStaffCategories)) return status(403, { error: i18n('error.notAllowed') }); + if(!session?.player?.hasPermission(Permission.ViewStaffCategories)) return status(403, { error: i18n('$.error.notAllowed') }); return Promise.all((await staffCategories.find().sort({ position: 1 }).lean()).map(async (category) => ({ id: category.id, @@ -58,10 +58,10 @@ export default (app: ElysiaApp) => app.get('/', async () => { 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) } }).get('/:category', async ({ session, params: { category: id }, i18n, status }) => { - if(!session?.player?.hasPermission(Permission.ViewStaffCategories)) return status(403, { error: i18n('error.notAllowed') }); + if(!session?.player?.hasPermission(Permission.ViewStaffCategories)) return status(403, { error: i18n('$.error.notAllowed') }); const category = await staffCategories.findOne({ id }).lean(); - if(!category) return status(404, { error: i18n('staff.categories.not_found') }); + if(!category) return status(404, { error: i18n('$.staff.categories.not_found') }); const members = await staffMembers.find({ category: category.id }).sort({ joinedAt: 1 }).lean(); return { @@ -82,9 +82,9 @@ export default (app: ElysiaApp) => app.get('/', async () => { 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) }, params: t.Object({ category: t.String({ description: 'The category ID' }) }), - headers: t.Object({ authorization: t.String({ error: 'error.notAllowed', description: 'Your authentication token' }) }, { error: 'error.notAllowed' }), + headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }), }).post('/', async ({ session, body: { name }, i18n, status }) => { - if(!session?.player?.hasPermission(Permission.CreateStaffCategories)) return status(403, { error: i18n('error.notAllowed') }); + if(!session?.player?.hasPermission(Permission.CreateStaffCategories)) return status(403, { error: i18n('$.error.notAllowed') }); const category = await staffCategories.insertOne({ id: generateSecureCode(), @@ -107,13 +107,15 @@ export default (app: ElysiaApp) => app.get('/', async () => { 403: t.Object({ error: t.String() }, { description: 'You\'re not allowed to manage staff categories' }), 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) }, - body: t.Object({ name: t.String({ minLength: 1, error: 'error.wrongType;;[["field", "name"], ["type", "string"]]' }) }, { additionalProperties: true, error: 'error.invalidBody' }), - headers: t.Object({ authorization: t.String({ error: 'error.notAllowed', description: 'Your authentication token' }) }, { error: 'error.notAllowed' }) + body: t.Object({ + name: t.String({ minLength: 1, error: '$.error.wrongType;;[["field", "name"], ["type", "string"]]' }) + }, { error: '$.error.invalidBody', additionalProperties: true }), + headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) }).patch('/:id', async ({ session, body: { name }, params: { id }, i18n, status }) => { // Edit category - if(!session?.player?.hasPermission(Permission.EditStaffMembers)) return status(403, { error: i18n('error.notAllowed') }); + if(!session?.player?.hasPermission(Permission.EditStaffMembers)) return status(403, { error: i18n('$.error.notAllowed') }); const category = await staffCategories.findOne({ id }); - if(!category) return status(404, { error: i18n('staff.categories.not_found') }); + if(!category) return status(404, { error: i18n('$.staff.categories.not_found') }); if(name && category.name !== name.trim()) { category.name = name.trim(); @@ -139,20 +141,20 @@ export default (app: ElysiaApp) => app.get('/', async () => { 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) }, body: t.Object({ - name: t.Optional(t.String({ error: 'error.wrongType;;[["field", "name"], ["type", "string"]]' })) - }, { additionalProperties: true, error: 'error.invalidBody' }), + name: t.Optional(t.String({ error: '$.error.wrongType;;[["field", "name"], ["type", "string"]]' })) + }, { error: '$.error.invalidBody', additionalProperties: true }), params: t.Object({ id: t.String({ description: 'The category ID' }) }), - headers: t.Object({ authorization: t.String({ error: 'error.notAllowed', description: 'Your authentication token' }) }, { error: 'error.notAllowed' }) + headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) }) // TODO: Implement route to patch all categories at once .delete('/:category', async ({ session, params: { category: id }, i18n, status }) => { - if(!session?.player?.hasPermission(Permission.DeleteStaffCategories)) return status(403, { error: i18n('error.notAllowed') }); + if(!session?.player?.hasPermission(Permission.DeleteStaffCategories)) return status(403, { error: i18n('$.error.notAllowed') }); const category = await staffCategories.findOne({ id }); - if(!category) return status(404, { error: i18n('staff.categories.not_found') }); + if(!category) return status(404, { error: i18n('$.staff.categories.not_found') }); await category.deleteOne(); - return { message: i18n('staff.categories.delete.success').replace('', category.name) }; + return { message: i18n('$.staff.categories.delete.success').replace('', category.name) }; }, { detail: { tags: ['API'], @@ -165,11 +167,11 @@ export default (app: ElysiaApp) => app.get('/', async () => { 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) }, params: t.Object({ category: t.String({ description: 'The category ID' }) }), - headers: t.Object({ authorization: t.String({ error: 'error.notAllowed', description: 'Your authentication token' }) }, { error: 'error.notAllowed' }), + headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }), }) ).group('/members', (app) => app.get('/', async ({ session, i18n, status }) => { - if(!session?.player?.hasPermission(Permission.ViewStaffMembers)) return status(403, { error: i18n('error.notAllowed') }); + if(!session?.player?.hasPermission(Permission.ViewStaffMembers)) return status(403, { error: i18n('$.error.notAllowed') }); return Promise.all((await staffMembers.find().sort({ joinedAt: 1 }).lean()).map(async member => ({ uuid: formatUUID(member.uuid), @@ -189,10 +191,10 @@ export default (app: ElysiaApp) => app.get('/', async () => { 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) } }).get('/:uuid', async ({ session, params: { uuid }, i18n, status }) => { - if(!session?.player?.hasPermission(Permission.ViewStaffMembers)) return status(403, { error: i18n('error.notAllowed') }); + if(!session?.player?.hasPermission(Permission.ViewStaffMembers)) return status(403, { error: i18n('$.error.notAllowed') }); const member = await staffMembers.findOne({ uuid: stripUUID(uuid) }); - if(!member) return status(404, { error: i18n('staff.members.not_found') }); + if(!member) return status(404, { error: i18n('$.staff.members.not_found') }); return { uuid: formatUUID(member.uuid), @@ -213,18 +215,18 @@ export default (app: ElysiaApp) => app.get('/', async () => { 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) }, params: t.Object({ uuid: t.String({ description: 'The member UUID' }) }), - headers: t.Object({ authorization: t.String({ error: 'error.notAllowed', description: 'Your authentication token' }) }, { error: 'error.notAllowed' }), + headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }), }).post('/', async ({ session, body: { uuid, category, description }, i18n, status }) => { - if(!session?.player?.hasPermission(Permission.CreateStaffMembers)) return status(403, { error: i18n('error.notAllowed') }); + if(!session?.player?.hasPermission(Permission.CreateStaffMembers)) return status(403, { error: i18n('$.error.notAllowed') }); uuid = stripUUID(uuid.trim()); - if(!uuidRegex.test(uuid)) return status(400, { error: i18n('staff.members.invalid_uuid') }); + if(!uuidRegex.test(uuid)) return status(400, { error: i18n('$.staff.members.invalid_uuid') }); const existingMember = await staffMembers.findOne({ uuid }); - if(existingMember) return status(409, { error: i18n('staff.members.already_exists') }); + if(existingMember) return status(409, { error: i18n('$.staff.members.already_exists') }); - if(!(await staffCategories.exists({ id: category }))) return status(404, { error: i18n('staff.categories.not_found') }); + if(!(await staffCategories.exists({ id: category }))) return status(404, { error: i18n('$.staff.categories.not_found') }); const joinedAt = new Date(); joinedAt.setHours(0, 0, 0, 0); @@ -257,20 +259,20 @@ export default (app: ElysiaApp) => app.get('/', async () => { 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) }, body: t.Object({ - uuid: t.String({ error: 'error.wrongType;;[["field", "uuid"], ["type", "string"]]' }), - category: t.String({ error: 'error.wrongType;;[["field", "category"], ["type", "string"]]' }), - description: t.Union([t.String(), t.Null()], { error: 'error.wrongType;;[["field", "description"], ["type", "string"]]' }) - }, { additionalProperties: true, error: 'error.invalidBody' }), - headers: t.Object({ authorization: t.String({ error: 'error.notAllowed', description: 'Your authentication token' }) }, { error: 'error.notAllowed' }) + uuid: t.String({ error: '$.error.wrongType;;[["field", "uuid"], ["type", "string"]]' }), + category: t.String({ error: '$.error.wrongType;;[["field", "category"], ["type", "string"]]' }), + description: t.Union([t.String(), t.Null()], { error: '$.error.wrongType;;[["field", "description"], ["type", "string"]]' }) + }, { error: '$.error.invalidBody', additionalProperties: true }), + headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) }).patch('/:uuid', async ({ session, body: { category, description }, params: { uuid }, i18n, status }) => { - if(!session?.player?.hasPermission(Permission.EditStaffMembers)) return status(403, { error: i18n('error.notAllowed') }); + if(!session?.player?.hasPermission(Permission.EditStaffMembers)) return status(403, { error: i18n('$.error.notAllowed') }); const member = await staffMembers.findOne({ uuid: stripUUID(uuid) }); - if(!member) return status(404, { error: i18n('staff.members.not_found') }); + if(!member) return status(404, { error: i18n('$.staff.members.not_found') }); let updated = false; if(category !== undefined && member.category !== category) { - if(!(await staffCategories.exists({ id: category }))) return status(404, { error: i18n('staff.categories.not_found') }); + if(!(await staffCategories.exists({ id: category }))) return status(404, { error: i18n('$.staff.categories.not_found') }); member.category = category; updated = true; } @@ -299,20 +301,20 @@ export default (app: ElysiaApp) => app.get('/', async () => { 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) }, body: t.Object({ - category: t.Optional(t.String({ error: 'error.wrongType;;[["field", "category"], ["type", "string"]]' })), - description: t.Optional(t.Union([t.String(), t.Null()], { error: 'error.wrongType;;[["field", "description"], ["type", "string"]]' })) - }, { additionalProperties: true, error: 'error.invalidBody' }), + category: t.Optional(t.String({ error: '$.error.wrongType;;[["field", "category"], ["type", "string"]]' })), + description: t.Optional(t.Union([t.String(), t.Null()], { error: '$.error.wrongType;;[["field", "description"], ["type", "string"]]' })) + }, { error: 'error.invalidBody', additionalProperties: true }), params: t.Object({ uuid: t.String({ description: 'The member UUID' }) }), - headers: t.Object({ authorization: t.String({ error: 'error.notAllowed', description: 'Your authentication token' }) }, { error: 'error.notAllowed' }) + headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) }).delete('/:uuid', async ({ session, params: { uuid }, i18n, status }) => { - if(!session?.player?.hasPermission(Permission.DeleteStaffMembers)) return status(403, { error: i18n('error.notAllowed') }); + if(!session?.player?.hasPermission(Permission.DeleteStaffMembers)) return status(403, { error: i18n('$.error.notAllowed') }); const member = await staffMembers.findOne({ uuid: stripUUID(uuid) }); - if(!member) return status(404, { error: i18n('staff.members.not_found') }); + if(!member) return status(404, { error: i18n('$.staff.members.not_found') }); await member.deleteOne(); - return { message: i18n('staff.members.delete.success').replace('', (await GameProfile.getProfileByUUID(uuid)).username || 'Unknown') }; + return { message: i18n('$.staff.members.delete.success').replace('', (await GameProfile.getProfileByUUID(uuid)).username || 'Unknown') }; }, { detail: { tags: ['API'], @@ -325,6 +327,6 @@ export default (app: ElysiaApp) => app.get('/', async () => { 503: t.Object({ error: t.String() }, { description: 'The database is not reachable' }) }, params: t.Object({ uuid: t.String({ description: 'The member UUID' }) }), - headers: t.Object({ authorization: t.String({ error: 'error.notAllowed', description: 'Your authentication token' }) }, { error: 'error.notAllowed' }) + headers: t.Object({ authorization: t.String({ error: '$.error.notAllowed', description: 'Your authentication token' }) }, { error: '$.error.notAllowed' }) }) ); \ No newline at end of file From 569fd11f594e415565c991497a7df8f19a447aeb Mon Sep 17 00:00:00 2001 From: RappyTV Date: Tue, 17 Jun 2025 21:32:19 +0200 Subject: [PATCH 3/5] translations: Add validation workflow --- .github/scripts/validate-translations.js | 66 +++++++++++++++++++++ .github/workflows/validate-translations.yml | 29 +++++++++ 2 files changed, 95 insertions(+) create mode 100644 .github/scripts/validate-translations.js create mode 100644 .github/workflows/validate-translations.yml diff --git a/.github/scripts/validate-translations.js b/.github/scripts/validate-translations.js new file mode 100644 index 0000000..7a7934d --- /dev/null +++ b/.github/scripts/validate-translations.js @@ -0,0 +1,66 @@ +const glob = require('fast-glob'); +const fs = require('fs'); +const path = require('path'); + +const KEY_REGEX = /(['`])\$\.(\w+(?:\.\w+)+)(?=;;|\1|$)/g; +const LOCALE_FILE = path.resolve('locales/en_us.json'); +const SRC_FILES = glob.sync(['src/**/*.ts'], { dot: false }); + +const localeData = JSON.parse(fs.readFileSync(LOCALE_FILE, 'utf-8')); +const flatLocaleKeys = flattenKeys(localeData); +const usedKeysMap = new Map(); + +for (const file of SRC_FILES) { + const content = fs.readFileSync(file, 'utf-8'); + let match; + while ((match = KEY_REGEX.exec(content)) !== null) { + const key = match[2]; + if (!usedKeysMap.has(key)) usedKeysMap.set(key, new Set()); + usedKeysMap.get(key).add(file); + } +} + +const usedKeys = [...usedKeysMap.keys()]; +const missingKeys = usedKeys.filter((key) => !flatLocaleKeys.has(key)); +const unusedKeys = [...flatLocaleKeys].filter((key) => !usedKeysMap.has(key)); + +console.log('\nšŸ” Localization Check Results:\n'); + +if (missingKeys.length) { + console.log('āŒ Missing keys in en_us.json:'); + missingKeys.forEach((key) => { + const files = [...usedKeysMap.get(key)]; + files.forEach((file) => { + console.log(` - ${key} (${file})`); + }); + }); +} else { + console.log('āœ… No missing keys.'); +} + +if (unusedKeys.length) { + console.log('\nāš ļø Unused keys in en_us.json:'); + unusedKeys.forEach((key) => console.log(` - ${key}`)); + console.log(flatLocaleKeys.size, unusedKeys.length) +} else { + console.log('āœ… No unused keys.'); +} + +if (missingKeys.length || unusedKeys.length) { + process.exit(1); // Fail the action +} + +function flattenKeys(obj, prefix = '') { + const keys = new Set(); + for (const key in obj) { + const fullKey = prefix ? `${prefix}.${key}` : key; + if (typeof obj[key] === 'object' && obj[key] !== null) { + for (const subKey of flattenKeys(obj[key], fullKey)) { + keys.add(subKey); + } + } else { + keys.add(fullKey); + } + } + return keys; +} \ No newline at end of file diff --git a/.github/workflows/validate-translations.yml b/.github/workflows/validate-translations.yml new file mode 100644 index 0000000..74f3508 --- /dev/null +++ b/.github/workflows/validate-translations.yml @@ -0,0 +1,29 @@ +name: Validate Localization Keys + +on: + push: + paths: + - 'src/**/*.ts' + - 'locales/en_us.json' + - '.github/scripts/validate-translations.js' + - '.github/workflows/validate-translations.yml' + pull_request: + +jobs: + validate-translations: + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Install dependencies + run: npm install fast-glob + + - name: Run localization key check + run: node .github/scripts/validate-translations.js From 37d8250490c5a31a4c451e2232fd7f647c7e59a0 Mon Sep 17 00:00:00 2001 From: RappyTV Date: Tue, 17 Jun 2025 21:36:55 +0200 Subject: [PATCH 4/5] ci: Use ts with bun --- ...anslations.js => validate-translations.ts} | 52 +++++++++++-------- .github/workflows/validate-translations.yml | 12 ++--- 2 files changed, 37 insertions(+), 27 deletions(-) rename .github/scripts/{validate-translations.js => validate-translations.ts} (54%) diff --git a/.github/scripts/validate-translations.js b/.github/scripts/validate-translations.ts similarity index 54% rename from .github/scripts/validate-translations.js rename to .github/scripts/validate-translations.ts index 7a7934d..3c5b62f 100644 --- a/.github/scripts/validate-translations.js +++ b/.github/scripts/validate-translations.ts @@ -1,22 +1,28 @@ -const glob = require('fast-glob'); -const fs = require('fs'); -const path = require('path'); +import { sync as globSync } from 'fast-glob'; +import { readFileSync } from 'fs'; +import { resolve } from 'path'; const KEY_REGEX = /(['`])\$\.(\w+(?:\.\w+)+)(?=;;|\1|$)/g; -const LOCALE_FILE = path.resolve('locales/en_us.json'); -const SRC_FILES = glob.sync(['src/**/*.ts'], { dot: false }); +const LOCALE_FILE = resolve('locales/en_us.json'); +const SRC_FILES = globSync(['src/**/*.ts'], { dot: false }); -const localeData = JSON.parse(fs.readFileSync(LOCALE_FILE, 'utf-8')); +type LocaleObject = Record; + +const localeData: LocaleObject = JSON.parse(readFileSync(LOCALE_FILE, 'utf-8')); const flatLocaleKeys = flattenKeys(localeData); -const usedKeysMap = new Map(); + +const usedKeysMap: Map> = new Map(); for (const file of SRC_FILES) { - const content = fs.readFileSync(file, 'utf-8'); - let match; + const content = readFileSync(file, 'utf-8'); + let match: RegExpExecArray | null; + while ((match = KEY_REGEX.exec(content)) !== null) { const key = match[2]; - if (!usedKeysMap.has(key)) usedKeysMap.set(key, new Set()); - usedKeysMap.get(key).add(file); + if (!usedKeysMap.has(key)) { + usedKeysMap.set(key, new Set()); + } + usedKeysMap.get(key)!.add(file); } } @@ -28,32 +34,35 @@ console.log('\nšŸ” Localization Check Results:\n'); if (missingKeys.length) { console.log('āŒ Missing keys in en_us.json:'); - missingKeys.forEach((key) => { - const files = [...usedKeysMap.get(key)]; - files.forEach((file) => { + for (const key of missingKeys) { + const files = [...(usedKeysMap.get(key) ?? [])]; + for (const file of files) { console.log(` - ${key} (${file})`); - }); - }); + } + } } else { console.log('āœ… No missing keys.'); } if (unusedKeys.length) { console.log('\nāš ļø Unused keys in en_us.json:'); - unusedKeys.forEach((key) => console.log(` - ${key}`)); - console.log(flatLocaleKeys.size, unusedKeys.length) + for (const key of unusedKeys) { + console.log(` - ${key}`); + } } else { console.log('āœ… No unused keys.'); } if (missingKeys.length || unusedKeys.length) { - process.exit(1); // Fail the action + process.exit(1); } -function flattenKeys(obj, prefix = '') { - const keys = new Set(); +function flattenKeys(obj: LocaleObject, prefix = ''): Set { + const keys = new Set(); + for (const key in obj) { const fullKey = prefix ? `${prefix}.${key}` : key; + if (typeof obj[key] === 'object' && obj[key] !== null) { for (const subKey of flattenKeys(obj[key], fullKey)) { keys.add(subKey); @@ -62,5 +71,6 @@ function flattenKeys(obj, prefix = '') { keys.add(fullKey); } } + return keys; } \ No newline at end of file diff --git a/.github/workflows/validate-translations.yml b/.github/workflows/validate-translations.yml index 74f3508..8fc577d 100644 --- a/.github/workflows/validate-translations.yml +++ b/.github/workflows/validate-translations.yml @@ -5,7 +5,7 @@ on: paths: - 'src/**/*.ts' - 'locales/en_us.json' - - '.github/scripts/validate-translations.js' + - '.github/scripts/validate-translations.ts' - '.github/workflows/validate-translations.yml' pull_request: @@ -17,13 +17,13 @@ jobs: - name: Checkout repo uses: actions/checkout@v4 - - name: Set up Node.js - uses: actions/setup-node@v4 + - name: Set up bun + uses: oven-sh/setup-bun@v4 with: - node-version: 22 + bun-version: latest - name: Install dependencies - run: npm install fast-glob + run: bun i fast-glob - name: Run localization key check - run: node .github/scripts/validate-translations.js + run: bun .github/scripts/validate-translations.ts From 639befb18e2c551bcc01e155374b27eaa26a7eeb Mon Sep 17 00:00:00 2001 From: RappyTV Date: Tue, 17 Jun 2025 21:41:06 +0200 Subject: [PATCH 5/5] ci: Use correct bun action version --- .github/workflows/validate-translations.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/validate-translations.yml b/.github/workflows/validate-translations.yml index 8fc577d..7994e7a 100644 --- a/.github/workflows/validate-translations.yml +++ b/.github/workflows/validate-translations.yml @@ -18,7 +18,7 @@ jobs: uses: actions/checkout@v4 - name: Set up bun - uses: oven-sh/setup-bun@v4 + uses: oven-sh/setup-bun@v2 with: bun-version: latest