From 4f90f7c75cc2cd11207702781220200b487cbaf7 Mon Sep 17 00:00:00 2001 From: Mrgaton Date: Wed, 7 Feb 2024 17:47:26 +0100 Subject: [PATCH 01/10] initial changes --- src/classes/DocumentHandler.ts | 23 +++++++++++++-- src/utils/StringUtils.ts | 2 +- src/utils/constants.ts | 54 ++++++++++++++++++++++++---------- 3 files changed, 59 insertions(+), 20 deletions(-) diff --git a/src/classes/DocumentHandler.ts b/src/classes/DocumentHandler.ts index 387f0d6..3409e7f 100644 --- a/src/classes/DocumentHandler.ts +++ b/src/classes/DocumentHandler.ts @@ -8,7 +8,8 @@ import { JSPErrorMessage, maxDocLength, ServerVersion, - viewDocumentPath + viewDocumentPath, + type Range } from '../utils/constants.ts'; import { ErrorSender } from './ErrorSender.ts'; import { StringUtils } from '../utils/StringUtils.ts'; @@ -38,6 +39,8 @@ interface HandlePublish { selectedSecret?: string; lifetime?: number; password?: string; + selectedKeyLength?: number; + selectedKey?: string; } interface HandleRemove { @@ -114,7 +117,7 @@ export class DocumentHandler { } public static async handlePublish( - { errorSender, body, selectedSecret, lifetime, password }: HandlePublish, + { errorSender, body, selectedSecret, lifetime, password, selectedKeyLength, selectedKey }: HandlePublish, version: ServerVersion ) { const buffer = Buffer.from(body as ArrayBuffer); @@ -127,6 +130,16 @@ export class DocumentHandler { if (!ValidatorUtils.isStringLengthBetweenLimits(secret || '', 1, 255)) return errorSender.sendError(400, JSPErrorMessage['jsp.document.invalid_secret_length']); + if (selectedKey && !ValidatorUtils.isStringLengthBetweenLimits(selectedKey, 2, 32)) + return errorSender.sendError(400, JSPErrorMessage['jsp.document.invalid_key_length']); + + console.log(selectedKeyLength); + + console.log('( ' + (selectedKeyLength || 0 > 32) + '||' + (selectedKeyLength || 8 < 2) + ')'); + + if (selectedKeyLength && (selectedKeyLength > 32 || selectedKeyLength < 2)) + return errorSender.sendError(400, JSPErrorMessage['jsp.document.invalid_key_length']); + if (password && !ValidatorUtils.isStringLengthBetweenLimits(password, 0, 255)) return errorSender.sendError(400, JSPErrorMessage['jsp.document.invalid_password_length']); @@ -145,7 +158,11 @@ export class DocumentHandler { password }; - const key = await StringUtils.createKey(); + const key = selectedKey || (await StringUtils.createKey((selectedKeyLength as Range<2, 32>) || 8)); + + if (await Bun.file(basePath + key).exists()) { + return errorSender.sendError(400, JSPErrorMessage['jsp.document.document_already_exist']); + } await DocumentManager.write(basePath + key, newDoc); diff --git a/src/utils/StringUtils.ts b/src/utils/StringUtils.ts index 036bb6b..d5bb1e9 100644 --- a/src/utils/StringUtils.ts +++ b/src/utils/StringUtils.ts @@ -9,7 +9,7 @@ export class StringUtils { return string; } - public static async createKey(length: Range<6, 16> = 8): Promise { + public static async createKey(length: Range<2, 32> = 8): Promise { const key = StringUtils.random(length, 64); const exists = await Bun.file(basePath + key).exists(); diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 5b4e8c8..970792e 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -1,17 +1,23 @@ import type { ServerOptions } from '../interfaces/ServerOptions.ts'; import type { ZlibCompressionOptions } from 'bun'; import type { JSPError } from '../classes/ErrorSender.ts'; -import * as env from 'env-var'; // interface Bun.env declare module 'bun' { interface Env { PORT: number; - DOCS_ENABLED: boolean; - DOCS_PATH: string; - DOCS_PLAYGROUND_HTTPS: boolean; - DOCS_PLAYGROUND_DOMAIN: string; - DOCS_PLAYGROUND_PORT: number; + DOCS: { + ENABLED: boolean; + PATH: string; + PLAYGROUND: { + HTTPS: boolean; + DOMAIN: string; + PORT: number; + }; + }; + GZIP: { + LEVEL: Range<0, 9>; + }; } } @@ -33,25 +39,31 @@ export enum JSPErrorCode { documentInvalidPassword = 'jsp.document.invalid_password', documentInvalidLength = 'jsp.document.invalid_length', documentInvalidSecret = 'jsp.document.invalid_secret', - documentInvalidSecretLength = 'jsp.document.invalid_secret_length' + documentInvalidSecretLength = 'jsp.document.invalid_secret_length', + documentInvalidKeyLength = 'jsp.document.invalid_key_length', + documentAlreadyExist = 'jsp.document.document_already_exist' } -export const serverConfig: Required = { - port: env.get('PORT').default(4000).asPortNumber(), +export const serverConfig: ServerOptions = { + port: Bun.env.PORT || 4000, versions: [ServerVersion.v1, ServerVersion.v2], docs: { - enabled: env.get('DOCS_ENABLED').asBoolStrict() ?? true, - path: env.get('DOCS_PATH').default('/docs').asString(), + enabled: Bun.env.DOCS?.ENABLED || true, + path: Bun.env.DOCS?.PATH || '/docs', playground: { - https: env.get('DOCS_PLAYGROUND_HTTPS').asBoolStrict() ?? true, - domain: env.get('DOCS_PLAYGROUND_DOMAIN').default('jspaste.eu').asString(), - port: env.get('DOCS_PLAYGROUND_PORT').default(443).asPortNumber() + domain: + Bun.env.DOCS?.PLAYGROUND?.DOMAIN === undefined + ? 'https://jspaste.eu' + : (Bun.env.DOCS?.PLAYGROUND?.HTTPS ? 'https://' : 'http://').concat( + Bun.env.DOCS?.PLAYGROUND?.DOMAIN + ), + port: Bun.env.DOCS?.PLAYGROUND?.PORT || 443 } } -} as const; +} as const satisfies Required; export const zlibConfig: ZlibCompressionOptions = { - level: 6 + level: Bun.env.GZIP?.LEVEL || 6 } as const; // FIXME(inetol): Migrate to new config system @@ -126,6 +138,16 @@ export const JSPErrorMessage: Record = { type: 'error', errorCode: JSPErrorCode.documentInvalidSecretLength, message: 'The provided secret length is invalid' + }, + [JSPErrorCode.documentInvalidKeyLength]: { + type: 'error', + errorCode: JSPErrorCode.documentInvalidKeyLength, + message: 'The provided key length is invalid' + }, + [JSPErrorCode.documentAlreadyExist]: { + type: 'error', + errorCode: JSPErrorCode.documentAlreadyExist, + message: 'The provided key already exist' } }; From 6c5e95ddb6ec36718a9147b534a7843ba2be8206 Mon Sep 17 00:00:00 2001 From: Mrgaton Date: Wed, 7 Feb 2024 18:04:45 +0100 Subject: [PATCH 02/10] remove console logs --- src/classes/DocumentHandler.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/classes/DocumentHandler.ts b/src/classes/DocumentHandler.ts index 3409e7f..44d4f2a 100644 --- a/src/classes/DocumentHandler.ts +++ b/src/classes/DocumentHandler.ts @@ -133,10 +133,6 @@ export class DocumentHandler { if (selectedKey && !ValidatorUtils.isStringLengthBetweenLimits(selectedKey, 2, 32)) return errorSender.sendError(400, JSPErrorMessage['jsp.document.invalid_key_length']); - console.log(selectedKeyLength); - - console.log('( ' + (selectedKeyLength || 0 > 32) + '||' + (selectedKeyLength || 8 < 2) + ')'); - if (selectedKeyLength && (selectedKeyLength > 32 || selectedKeyLength < 2)) return errorSender.sendError(400, JSPErrorMessage['jsp.document.invalid_key_length']); From dd92fa66cc76d0a5e334a09705ae48dade47410d Mon Sep 17 00:00:00 2001 From: tnfAngel <57068341+tnfAngel@users.noreply.github.com> Date: Sat, 10 Feb 2024 11:43:44 +0000 Subject: [PATCH 03/10] fix & update --- src/classes/DocumentHandler.ts | 7 +++-- src/routes/v2/publish.route.ts | 22 +++++++++++++--- src/utils/StringUtils.ts | 10 ++++--- src/utils/constants.ts | 48 ++++++++++++++-------------------- 4 files changed, 48 insertions(+), 39 deletions(-) diff --git a/src/classes/DocumentHandler.ts b/src/classes/DocumentHandler.ts index 44d4f2a..bf3c85c 100644 --- a/src/classes/DocumentHandler.ts +++ b/src/classes/DocumentHandler.ts @@ -154,11 +154,10 @@ export class DocumentHandler { password }; - const key = selectedKey || (await StringUtils.createKey((selectedKeyLength as Range<2, 32>) || 8)); + const key = selectedKey || (await StringUtils.generateKey((selectedKeyLength as Range<2, 32>) || 8)); - if (await Bun.file(basePath + key).exists()) { - return errorSender.sendError(400, JSPErrorMessage['jsp.document.document_already_exist']); - } + if (selectedKey && (await StringUtils.keyExists(key))) + return errorSender.sendError(400, JSPErrorMessage['jsp.document.key_already_exists']); await DocumentManager.write(basePath + key, newDoc); diff --git a/src/routes/v2/publish.route.ts b/src/routes/v2/publish.route.ts index da8fd5d..0d52acf 100644 --- a/src/routes/v2/publish.route.ts +++ b/src/routes/v2/publish.route.ts @@ -15,6 +15,8 @@ export default new Elysia({ { errorSender, body, + selectedKey: request.headers.get('key') || '', + selectedKeyLength: parseInt(request.headers.get('key-length') ?? '') || undefined, selectedSecret: request.headers.get('secret') || '', lifetime: parseInt(request.headers.get('lifetime') || defaultDocumentLifetime.toString()), password: request.headers.get('password') || query['password'] || '' @@ -28,22 +30,36 @@ export default new Elysia({ }), headers: t.Optional( t.Object({ + key: t.Optional( + t.String({ + description: 'A custom key, if null, a new key will be generated', + examples: ['abc123'] + }) + ), + ['key-length']: t.Optional( + t.Numeric({ + description: + 'If a custom key is not set, this will determine the key length of the automatically generated key', + examples: ['20', '4'] + }) + ), secret: t.Optional( t.String({ - description: 'The selected secret, if null a new secret will be generated', + description: 'A custom secret, if null, a new secret will be generated', examples: ['aaaaa-bbbbb-ccccc-ddddd'] }) ), password: t.Optional( t.String({ - description: 'The document password, can be null', + description: + 'A custom password for the document, if null, anyone who has the key will be able to see the content of the document', examples: ['abc123'] }) ), lifetime: t.Optional( t.Numeric({ description: `Number in seconds that the document will exist before it is automatically removed. Set to 0 to make the document permanent. If nothing is set, the default period is: ${defaultDocumentLifetime}`, - examples: [60, 0] + examples: ['60', '0'] }) ) }) diff --git a/src/utils/StringUtils.ts b/src/utils/StringUtils.ts index d5bb1e9..a477af9 100644 --- a/src/utils/StringUtils.ts +++ b/src/utils/StringUtils.ts @@ -6,14 +6,18 @@ export class StringUtils { let string = ''; while (length--) string += baseSet.charAt(Math.floor(Math.random() * baseSet.length)); + return string; } - public static async createKey(length: Range<2, 32> = 8): Promise { + public static async generateKey(length: Range<2, 32> = 8): Promise { const key = StringUtils.random(length, 64); - const exists = await Bun.file(basePath + key).exists(); - return exists ? StringUtils.createKey() : key; + return (await StringUtils.keyExists(key)) ? StringUtils.generateKey() : key; + } + + public static async keyExists(key: string): Promise { + return Bun.file(basePath + key).exists(); } public static createSecret(chunkLength: number = 5, chunks: number = 4): string { diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 970792e..b3bc68d 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -1,23 +1,17 @@ import type { ServerOptions } from '../interfaces/ServerOptions.ts'; import type { ZlibCompressionOptions } from 'bun'; import type { JSPError } from '../classes/ErrorSender.ts'; +import * as env from 'env-var'; // interface Bun.env declare module 'bun' { interface Env { PORT: number; - DOCS: { - ENABLED: boolean; - PATH: string; - PLAYGROUND: { - HTTPS: boolean; - DOMAIN: string; - PORT: number; - }; - }; - GZIP: { - LEVEL: Range<0, 9>; - }; + DOCS_ENABLED: boolean; + DOCS_PATH: string; + DOCS_PLAYGROUND_HTTPS: boolean; + DOCS_PLAYGROUND_DOMAIN: string; + DOCS_PLAYGROUND_PORT: number; } } @@ -41,29 +35,25 @@ export enum JSPErrorCode { documentInvalidSecret = 'jsp.document.invalid_secret', documentInvalidSecretLength = 'jsp.document.invalid_secret_length', documentInvalidKeyLength = 'jsp.document.invalid_key_length', - documentAlreadyExist = 'jsp.document.document_already_exist' + documentKeyAlreadyExists = 'jsp.document.key_already_exists' } -export const serverConfig: ServerOptions = { - port: Bun.env.PORT || 4000, +export const serverConfig: Required = { + port: env.get('PORT').default(4000).asPortNumber(), versions: [ServerVersion.v1, ServerVersion.v2], docs: { - enabled: Bun.env.DOCS?.ENABLED || true, - path: Bun.env.DOCS?.PATH || '/docs', + enabled: env.get('DOCS_ENABLED').asBoolStrict() ?? true, + path: env.get('DOCS_PATH').default('/docs').asString(), playground: { - domain: - Bun.env.DOCS?.PLAYGROUND?.DOMAIN === undefined - ? 'https://jspaste.eu' - : (Bun.env.DOCS?.PLAYGROUND?.HTTPS ? 'https://' : 'http://').concat( - Bun.env.DOCS?.PLAYGROUND?.DOMAIN - ), - port: Bun.env.DOCS?.PLAYGROUND?.PORT || 443 + https: env.get('DOCS_PLAYGROUND_HTTPS').asBoolStrict() ?? true, + domain: env.get('DOCS_PLAYGROUND_DOMAIN').default('jspaste.eu').asString(), + port: env.get('DOCS_PLAYGROUND_PORT').default(443).asPortNumber() } } -} as const satisfies Required; +} as const; export const zlibConfig: ZlibCompressionOptions = { - level: Bun.env.GZIP?.LEVEL || 6 + level: 6 } as const; // FIXME(inetol): Migrate to new config system @@ -144,10 +134,10 @@ export const JSPErrorMessage: Record = { errorCode: JSPErrorCode.documentInvalidKeyLength, message: 'The provided key length is invalid' }, - [JSPErrorCode.documentAlreadyExist]: { + [JSPErrorCode.documentKeyAlreadyExists]: { type: 'error', - errorCode: JSPErrorCode.documentAlreadyExist, - message: 'The provided key already exist' + errorCode: JSPErrorCode.documentKeyAlreadyExists, + message: 'The provided key already exists' } }; From fa0fc6317b939a07138eddd677eb276ef85f0ac4 Mon Sep 17 00:00:00 2001 From: tnfAngel <57068341+tnfAngel@users.noreply.github.com> Date: Sat, 10 Feb 2024 11:54:20 +0000 Subject: [PATCH 04/10] update StringUtils --- src/classes/DocumentHandler.ts | 2 +- src/utils/StringUtils.ts | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/classes/DocumentHandler.ts b/src/classes/DocumentHandler.ts index bf3c85c..4fd8f67 100644 --- a/src/classes/DocumentHandler.ts +++ b/src/classes/DocumentHandler.ts @@ -154,7 +154,7 @@ export class DocumentHandler { password }; - const key = selectedKey || (await StringUtils.generateKey((selectedKeyLength as Range<2, 32>) || 8)); + const key = selectedKey || (await StringUtils.createKey((selectedKeyLength as Range<2, 32>) || 8)); if (selectedKey && (await StringUtils.keyExists(key))) return errorSender.sendError(400, JSPErrorMessage['jsp.document.key_already_exists']); diff --git a/src/utils/StringUtils.ts b/src/utils/StringUtils.ts index a477af9..289a2b2 100644 --- a/src/utils/StringUtils.ts +++ b/src/utils/StringUtils.ts @@ -3,6 +3,7 @@ import { basePath, characters, type Range } from './constants.ts'; export class StringUtils { public static random(length: number, base: Range<2, 64> = 62): string { const baseSet = characters.slice(0, base); + let string = ''; while (length--) string += baseSet.charAt(Math.floor(Math.random() * baseSet.length)); @@ -10,16 +11,20 @@ export class StringUtils { return string; } - public static async generateKey(length: Range<2, 32> = 8): Promise { - const key = StringUtils.random(length, 64); - - return (await StringUtils.keyExists(key)) ? StringUtils.generateKey() : key; + public static generateKey(length: Range<2, 32> = 8): string { + return StringUtils.random(length, 64); } public static async keyExists(key: string): Promise { return Bun.file(basePath + key).exists(); } + public static async createKey(length: Range<2, 32> = 8): Promise { + const key = StringUtils.generateKey(length); + + return (await StringUtils.keyExists(key)) ? StringUtils.createKey((length + 1) as Range<2, 32>) : key; + } + public static createSecret(chunkLength: number = 5, chunks: number = 4): string { return Array.from({ length: chunks }, () => StringUtils.random(chunkLength)).join('-'); } From 6850e037c426e780911b7fef9aa8978182f0297e Mon Sep 17 00:00:00 2001 From: tnfAngel <57068341+tnfAngel@users.noreply.github.com> Date: Sat, 10 Feb 2024 12:27:56 +0000 Subject: [PATCH 05/10] Fix #53 --- src/classes/DocumentHandler.ts | 37 +++++++++++++++++----------------- src/classes/Server.ts | 10 ++++----- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/src/classes/DocumentHandler.ts b/src/classes/DocumentHandler.ts index 4fd8f67..ba45220 100644 --- a/src/classes/DocumentHandler.ts +++ b/src/classes/DocumentHandler.ts @@ -5,6 +5,7 @@ import type { DocumentDataStruct } from '../structures/documentStruct.ts'; import { basePath, defaultDocumentLifetime, + JSPErrorCode, JSPErrorMessage, maxDocLength, ServerVersion, @@ -83,22 +84,22 @@ export class DocumentHandler { public static async handleEdit({ errorSender, key, newBody, secret }: HandleEdit) { if (!ValidatorUtils.isStringLengthBetweenLimits(key, 1, 255) || !ValidatorUtils.isAlphanumeric(key)) - return errorSender.sendError(400, JSPErrorMessage['jsp.input.invalid']); + return errorSender.sendError(400, JSPErrorMessage[JSPErrorCode.inputInvalid]); const file = Bun.file(basePath + key); const fileExists = await file.exists(); - if (!fileExists) return errorSender.sendError(404, JSPErrorMessage['jsp.document.not_found']); + if (!fileExists) return errorSender.sendError(404, JSPErrorMessage[JSPErrorCode.documentNotFound]); const buffer = Buffer.from(newBody as ArrayBuffer); if (!ValidatorUtils.isLengthBetweenLimits(buffer, 1, maxDocLength)) - return errorSender.sendError(400, JSPErrorMessage['jsp.document.invalid_length']); + return errorSender.sendError(400, JSPErrorMessage[JSPErrorCode.documentInvalidLength]); const doc = await DocumentManager.read(file); if (doc.secret && doc.secret !== secret) - return errorSender.sendError(403, JSPErrorMessage['jsp.document.invalid_secret']); + return errorSender.sendError(403, JSPErrorMessage[JSPErrorCode.documentInvalidSecret]); doc.rawFileData = buffer; @@ -111,7 +112,7 @@ export class DocumentHandler { public static async handleExists({ errorSender, key }: HandleExists) { if (!ValidatorUtils.isStringLengthBetweenLimits(key, 1, 255) || !ValidatorUtils.isAlphanumeric(key)) - return errorSender.sendError(400, JSPErrorMessage['jsp.input.invalid']); + return errorSender.sendError(400, JSPErrorMessage[JSPErrorCode.inputInvalid]); return await Bun.file(basePath + key).exists(); } @@ -123,21 +124,21 @@ export class DocumentHandler { const buffer = Buffer.from(body as ArrayBuffer); if (!ValidatorUtils.isLengthBetweenLimits(buffer, 1, maxDocLength)) - return errorSender.sendError(400, JSPErrorMessage['jsp.document.invalid_length']); + return errorSender.sendError(400, JSPErrorMessage[JSPErrorCode.documentInvalidLength]); const secret = selectedSecret || StringUtils.createSecret(); if (!ValidatorUtils.isStringLengthBetweenLimits(secret || '', 1, 255)) - return errorSender.sendError(400, JSPErrorMessage['jsp.document.invalid_secret_length']); + return errorSender.sendError(400, JSPErrorMessage[JSPErrorCode.documentInvalidSecretLength]); if (selectedKey && !ValidatorUtils.isStringLengthBetweenLimits(selectedKey, 2, 32)) - return errorSender.sendError(400, JSPErrorMessage['jsp.document.invalid_key_length']); + return errorSender.sendError(400, JSPErrorMessage[JSPErrorCode.documentInvalidKeyLength]); if (selectedKeyLength && (selectedKeyLength > 32 || selectedKeyLength < 2)) - return errorSender.sendError(400, JSPErrorMessage['jsp.document.invalid_key_length']); + return errorSender.sendError(400, JSPErrorMessage[JSPErrorCode.documentInvalidKeyLength]); if (password && !ValidatorUtils.isStringLengthBetweenLimits(password, 0, 255)) - return errorSender.sendError(400, JSPErrorMessage['jsp.document.invalid_password_length']); + return errorSender.sendError(400, JSPErrorMessage[JSPErrorCode.documentInvalidPasswordLength]); lifetime = lifetime ?? defaultDocumentLifetime; @@ -157,7 +158,7 @@ export class DocumentHandler { const key = selectedKey || (await StringUtils.createKey((selectedKeyLength as Range<2, 32>) || 8)); if (selectedKey && (await StringUtils.keyExists(key))) - return errorSender.sendError(400, JSPErrorMessage['jsp.document.key_already_exists']); + return errorSender.sendError(400, JSPErrorMessage[JSPErrorCode.documentKeyAlreadyExists]); await DocumentManager.write(basePath + key, newDoc); @@ -177,17 +178,17 @@ export class DocumentHandler { public static async handleRemove({ errorSender, key, secret }: HandleRemove) { if (!ValidatorUtils.isStringLengthBetweenLimits(key, 1, 255) || !ValidatorUtils.isAlphanumeric(key)) - return errorSender.sendError(400, JSPErrorMessage['jsp.input.invalid']); + return errorSender.sendError(400, JSPErrorMessage[JSPErrorCode.inputInvalid]); const file = Bun.file(basePath + key); const fileExists = await file.exists(); - if (!fileExists) return errorSender.sendError(404, JSPErrorMessage['jsp.document.not_found']); + if (!fileExists) return errorSender.sendError(404, JSPErrorMessage[JSPErrorCode.documentNotFound]); const doc = await DocumentManager.read(file); if (doc.secret && doc.secret !== secret) - return errorSender.sendError(403, JSPErrorMessage['jsp.document.invalid_secret']); + return errorSender.sendError(403, JSPErrorMessage[JSPErrorCode.documentInvalidSecret]); return { // TODO: Use optimized Bun.unlink when available -> https://bun.sh/docs/api/file-io#writing-files-bun-write @@ -199,7 +200,7 @@ export class DocumentHandler { private static async handleGetDocument({ errorSender, key, password }: HandleGetDocument) { if (!ValidatorUtils.isStringLengthBetweenLimits(key, 1, 255) || !ValidatorUtils.isAlphanumeric(key)) - return errorSender.sendError(400, JSPErrorMessage['jsp.input.invalid']); + return errorSender.sendError(400, JSPErrorMessage[JSPErrorCode.inputInvalid]); const file = Bun.file(basePath + key); const fileExists = await file.exists(); @@ -209,14 +210,14 @@ export class DocumentHandler { // TODO: Use optimized Bun.unlink when available -> https://bun.sh/docs/api/file-io#writing-files-bun-write if (fileExists) await unlink(basePath + key).catch(() => null); - return errorSender.sendError(404, JSPErrorMessage['jsp.document.not_found']); + return errorSender.sendError(404, JSPErrorMessage[JSPErrorCode.documentNotFound]); } if (doc.password && !password) - return errorSender.sendError(401, JSPErrorMessage['jsp.document.needs_password']); + return errorSender.sendError(401, JSPErrorMessage[JSPErrorCode.documentPasswordNeeded]); if (doc.password && doc.password !== password) - return errorSender.sendError(403, JSPErrorMessage['jsp.document.invalid_password']); + return errorSender.sendError(403, JSPErrorMessage[JSPErrorCode.documentInvalidPassword]); return doc; } diff --git a/src/classes/Server.ts b/src/classes/Server.ts index 9b86fa7..f2631c7 100644 --- a/src/classes/Server.ts +++ b/src/classes/Server.ts @@ -1,6 +1,6 @@ import { Elysia } from 'elysia'; import type { ServerOptions } from '../interfaces/ServerOptions.ts'; -import { JSPErrorMessage, serverConfig } from '../utils/constants.ts'; +import { JSPErrorCode, JSPErrorMessage, serverConfig } from '../utils/constants.ts'; import swagger from '@elysiajs/swagger'; import { join } from 'path'; import { errorSenderPlugin } from '../plugins/errorSender.ts'; @@ -87,19 +87,19 @@ export class Server { case 'VALIDATION': console.error(error); - return errorSender.sendError(400, JSPErrorMessage['jsp.validation_failed']); + return errorSender.sendError(400, JSPErrorMessage[JSPErrorCode.validation]); case 'INTERNAL_SERVER_ERROR': console.error(error); - return errorSender.sendError(500, JSPErrorMessage['jsp.internal_server_error']); + return errorSender.sendError(500, JSPErrorMessage[JSPErrorCode.internalServerError]); case 'PARSE': console.error(error); - return errorSender.sendError(400, JSPErrorMessage['jsp.parse_failed']); + return errorSender.sendError(400, JSPErrorMessage[JSPErrorCode.parseFailed]); default: console.error(error); - return errorSender.sendError(400, JSPErrorMessage['jsp.unknown']); + return errorSender.sendError(400, JSPErrorMessage[JSPErrorCode.unknown]); } }); } From f28f739ea81c707afbee0d084c147fee3c11c19f Mon Sep 17 00:00:00 2001 From: tnfAngel <57068341+tnfAngel@users.noreply.github.com> Date: Sat, 10 Feb 2024 12:34:37 +0000 Subject: [PATCH 06/10] remove error hint --- src/classes/ErrorSender.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/classes/ErrorSender.ts b/src/classes/ErrorSender.ts index f2640ef..a362a77 100644 --- a/src/classes/ErrorSender.ts +++ b/src/classes/ErrorSender.ts @@ -5,7 +5,6 @@ export interface JSPError { type: 'error'; message: string; errorCode: JSPErrorCode; - hint?: any; } export class ErrorSender { @@ -24,8 +23,7 @@ export class ErrorSender { { type: t.String({ description: 'The error type' }), message: t.String({ description: 'The error message' }), - errorCode: t.String({ description: 'The error code' }), - hint: t.Optional(t.Any({ description: 'The error hint' })) + errorCode: t.String({ description: 'The error code' }) }, { description: 'An object representing an error' } ); From 28b8d8aede7f88ad7e2374f1584e81736ee40984 Mon Sep 17 00:00:00 2001 From: tnfAngel <57068341+tnfAngel@users.noreply.github.com> Date: Sat, 10 Feb 2024 13:22:05 +0000 Subject: [PATCH 07/10] fix key not being validated against isAlphanumeric --- src/classes/DocumentHandler.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/classes/DocumentHandler.ts b/src/classes/DocumentHandler.ts index ba45220..7ce8775 100644 --- a/src/classes/DocumentHandler.ts +++ b/src/classes/DocumentHandler.ts @@ -131,6 +131,9 @@ export class DocumentHandler { if (!ValidatorUtils.isStringLengthBetweenLimits(secret || '', 1, 255)) return errorSender.sendError(400, JSPErrorMessage[JSPErrorCode.documentInvalidSecretLength]); + if (selectedKey && !ValidatorUtils.isAlphanumeric(selectedKey)) + return errorSender.sendError(400, JSPErrorMessage[JSPErrorCode.inputInvalid]); + if (selectedKey && !ValidatorUtils.isStringLengthBetweenLimits(selectedKey, 2, 32)) return errorSender.sendError(400, JSPErrorMessage[JSPErrorCode.documentInvalidKeyLength]); From 59891f286d78b83d1d9f57a735b03afe60f1e9e2 Mon Sep 17 00:00:00 2001 From: tnfAngel <57068341+tnfAngel@users.noreply.github.com> Date: Sat, 10 Feb 2024 13:25:11 +0000 Subject: [PATCH 08/10] update publish validation --- src/classes/DocumentHandler.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/classes/DocumentHandler.ts b/src/classes/DocumentHandler.ts index 7ce8775..7443230 100644 --- a/src/classes/DocumentHandler.ts +++ b/src/classes/DocumentHandler.ts @@ -131,12 +131,13 @@ export class DocumentHandler { if (!ValidatorUtils.isStringLengthBetweenLimits(secret || '', 1, 255)) return errorSender.sendError(400, JSPErrorMessage[JSPErrorCode.documentInvalidSecretLength]); - if (selectedKey && !ValidatorUtils.isAlphanumeric(selectedKey)) + if ( + selectedKey && + (!ValidatorUtils.isStringLengthBetweenLimits(selectedKey, 2, 32) || + !ValidatorUtils.isAlphanumeric(selectedKey)) + ) return errorSender.sendError(400, JSPErrorMessage[JSPErrorCode.inputInvalid]); - if (selectedKey && !ValidatorUtils.isStringLengthBetweenLimits(selectedKey, 2, 32)) - return errorSender.sendError(400, JSPErrorMessage[JSPErrorCode.documentInvalidKeyLength]); - if (selectedKeyLength && (selectedKeyLength > 32 || selectedKeyLength < 2)) return errorSender.sendError(400, JSPErrorMessage[JSPErrorCode.documentInvalidKeyLength]); From 13df453a902156a76db1dba6b1de52bba7c7f132 Mon Sep 17 00:00:00 2001 From: Ivan Gabaldon Date: Tue, 13 Feb 2024 06:37:01 +0100 Subject: [PATCH 09/10] Some changes (#54) * replace protobuf-ts to protobufjs * cleanup & fixes --- .env.example | 18 +- bun.lockb | Bin 56921 -> 84501 bytes compileStructures.cmd | 6 - compileStructures.sh | 6 - package.json | 13 +- src/classes/DocumentHandler.ts | 14 +- src/classes/DocumentManager.ts | 8 +- src/classes/Server.ts | 2 +- src/interfaces/ServerOptions.ts | 5 +- src/structures/Structures.d.ts | 108 +++++++++++ src/structures/Structures.js | 309 +++++++++++++++++++++++++++++++ src/structures/documentStruct.ts | 105 ----------- src/utils/StringUtils.ts | 4 +- src/utils/constants.ts | 22 +-- 14 files changed, 461 insertions(+), 159 deletions(-) delete mode 100644 compileStructures.cmd delete mode 100755 compileStructures.sh create mode 100644 src/structures/Structures.d.ts create mode 100644 src/structures/Structures.js delete mode 100644 src/structures/documentStruct.ts diff --git a/.env.example b/.env.example index f04fd7d..5725af7 100644 --- a/.env.example +++ b/.env.example @@ -1,21 +1,27 @@ +# Enable HTTPS for document "url" parameter domain? [false]:boolean +#TLS=false + +# Domain for document "url" parameter domain [localhost]:string +#DOMAIN=localhost + # Port for the server [4000]:number # (Don't expose the server to the internet, use a reverse proxy) -PORT=4000 +#PORT=4000 ### Documentation: # Enable documentation? [true]:boolean # (Disabling this will also disable the playground) -DOCS_ENABLED=true +#DOCS_ENABLED=true # Path to documentation [/docs]:string -DOCS_PATH=/docs +#DOCS_PATH=/docs ### Documentation playground: # Enable HTTPS for domain? [true]:boolean -DOCS_PLAYGROUND_HTTPS=true +#DOCS_PLAYGROUND_TLS=true # Domain for documentation playground [jspaste.eu]:string -DOCS_PLAYGROUND_DOMAIN=jspaste.eu +#DOCS_PLAYGROUND_DOMAIN=jspaste.eu # Port for documentation playground [443]:number -DOCS_PLAYGROUND_PORT=443 +#DOCS_PLAYGROUND_PORT=443 diff --git a/bun.lockb b/bun.lockb index 4e44c87f42c8e851814fd30bd6b925cc996539f7..7f55aee6899792248bcab8c0cc03859f0c747df9 100755 GIT binary patch delta 28149 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 bn;-I99rRjLZ>Y9~LEK`EJ;4lbDbM{s&ig{7 delta 12225 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 0 ? BigInt(Date.now() + msLifetime) : undefined; - const newDoc: DocumentDataStruct = { + const newDoc: IDocumentDataStruct = { rawFileData: buffer, secret, expirationTimestamp, @@ -174,7 +174,7 @@ export class DocumentHandler { return { key, secret, - url: viewDocumentPath + key, + url: (serverConfig.tls ? 'https://' : 'http://').concat(serverConfig.domain + '/') + key, expirationTimestamp: Number(expirationTimestamp ?? 0) }; } diff --git a/src/classes/DocumentManager.ts b/src/classes/DocumentManager.ts index 309c4c2..30d2201 100644 --- a/src/classes/DocumentManager.ts +++ b/src/classes/DocumentManager.ts @@ -1,13 +1,13 @@ import type { BunFile } from 'bun'; -import { DocumentDataStruct } from '../structures/documentStruct'; import { zlibConfig } from '../utils/constants.ts'; +import { DocumentDataStruct, type IDocumentDataStruct } from '../structures/Structures'; export class DocumentManager { public static async read(file: BunFile): Promise { - return DocumentDataStruct.fromBinary(Bun.inflateSync(Buffer.from(await file.arrayBuffer()))); + return DocumentDataStruct.decode(Bun.inflateSync(Buffer.from(await file.arrayBuffer()))); } - public static async write(filePath: string, document: DocumentDataStruct): Promise { - await Bun.write(filePath, Bun.deflateSync(DocumentDataStruct.toBinary(document), zlibConfig)); + public static async write(filePath: string, document: IDocumentDataStruct): Promise { + await Bun.write(filePath, Bun.deflateSync(DocumentDataStruct.encode(document).finish(), zlibConfig)); } } diff --git a/src/classes/Server.ts b/src/classes/Server.ts index f2631c7..a8e9ee5 100644 --- a/src/classes/Server.ts +++ b/src/classes/Server.ts @@ -49,7 +49,7 @@ export class Server { documentation: { servers: [ { - url: (this.serverConfig.docs.playground.https ? 'https://' : 'http://').concat( + url: (this.serverConfig.docs.playground.tls ? 'https://' : 'http://').concat( this.serverConfig.docs.playground.domain, ':', this.serverConfig.docs.playground.port.toString() diff --git a/src/interfaces/ServerOptions.ts b/src/interfaces/ServerOptions.ts index 46332e4..74526bf 100644 --- a/src/interfaces/ServerOptions.ts +++ b/src/interfaces/ServerOptions.ts @@ -1,13 +1,16 @@ import type { ServerVersion } from '../utils/constants'; export interface ServerOptions { + tls: boolean; + domain: string; port: number; versions: ServerVersion[]; + files: {}; docs: { enabled: boolean; path: string; playground: { - https: boolean; + tls: boolean; domain: string; port: number; }; diff --git a/src/structures/Structures.d.ts b/src/structures/Structures.d.ts new file mode 100644 index 0000000..b42c27c --- /dev/null +++ b/src/structures/Structures.d.ts @@ -0,0 +1,108 @@ +import * as $protobuf from "protobufjs"; +import Long = require("long"); +/** Properties of a DocumentDataStruct. */ +export interface IDocumentDataStruct { + + /** DocumentDataStruct rawFileData */ + rawFileData?: (Uint8Array|null); + + /** DocumentDataStruct secret */ + secret?: (string|null); + + /** DocumentDataStruct expirationTimestamp */ + expirationTimestamp?: (number|Long|null); + + /** DocumentDataStruct password */ + password?: (string|null); +} + +/** Represents a DocumentDataStruct. */ +export class DocumentDataStruct implements IDocumentDataStruct { + + /** + * Constructs a new DocumentDataStruct. + * @param [properties] Properties to set + */ + constructor(properties?: IDocumentDataStruct); + + /** DocumentDataStruct rawFileData. */ + public rawFileData: Uint8Array; + + /** DocumentDataStruct secret. */ + public secret: string; + + /** DocumentDataStruct expirationTimestamp. */ + public expirationTimestamp?: (number|Long|null); + + /** DocumentDataStruct password. */ + public password?: (string|null); + + /** DocumentDataStruct _expirationTimestamp. */ + public _expirationTimestamp?: "expirationTimestamp"; + + /** DocumentDataStruct _password. */ + public _password?: "password"; + + /** + * Encodes the specified DocumentDataStruct message. Does not implicitly {@link DocumentDataStruct.verify|verify} messages. + * @param message DocumentDataStruct message or plain object to encode + * @param [writer] Writer to encode to + * @returns Writer + */ + public static encode(message: IDocumentDataStruct, writer?: $protobuf.Writer): $protobuf.Writer; + + /** + * Encodes the specified DocumentDataStruct message, length delimited. Does not implicitly {@link DocumentDataStruct.verify|verify} messages. + * @param message DocumentDataStruct message or plain object to encode + * @param [writer] Writer to encode to + * @returns Writer + */ + public static encodeDelimited(message: IDocumentDataStruct, writer?: $protobuf.Writer): $protobuf.Writer; + + /** + * Decodes a DocumentDataStruct message from the specified reader or buffer. + * @param reader Reader or buffer to decode from + * @param [length] Message length if known beforehand + * @returns DocumentDataStruct + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + public static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): DocumentDataStruct; + + /** + * Decodes a DocumentDataStruct message from the specified reader or buffer, length delimited. + * @param reader Reader or buffer to decode from + * @returns DocumentDataStruct + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + public static decodeDelimited(reader: ($protobuf.Reader|Uint8Array)): DocumentDataStruct; + + /** + * Verifies a DocumentDataStruct message. + * @param message Plain object to verify + * @returns `null` if valid, otherwise the reason why it is not + */ + public static verify(message: { [k: string]: any }): (string|null); + + /** + * Creates a DocumentDataStruct message from a plain object. Also converts values to their respective internal types. + * @param object Plain object + * @returns DocumentDataStruct + */ + public static fromObject(object: { [k: string]: any }): DocumentDataStruct; + + /** + * Creates a plain object from a DocumentDataStruct message. Also converts values to other types if specified. + * @param message DocumentDataStruct + * @param [options] Conversion options + * @returns Plain object + */ + public static toObject(message: DocumentDataStruct, options?: $protobuf.IConversionOptions): { [k: string]: any }; + + /** + * Converts this DocumentDataStruct to JSON. + * @returns JSON object + */ + public toJSON(): { [k: string]: any }; +} diff --git a/src/structures/Structures.js b/src/structures/Structures.js new file mode 100644 index 0000000..918d60a --- /dev/null +++ b/src/structures/Structures.js @@ -0,0 +1,309 @@ +/*eslint-disable block-scoped-var, id-length, no-control-regex, no-magic-numbers, no-prototype-builtins, no-redeclare, no-shadow, no-var, sort-vars*/ +import * as $protobuf from "protobufjs/minimal"; + +// Common aliases +const $Reader = $protobuf.Reader, $Writer = $protobuf.Writer, $util = $protobuf.util; + +// Exported root namespace +const $root = $protobuf.roots["default"] || ($protobuf.roots["default"] = {}); + +export const DocumentDataStruct = $root.DocumentDataStruct = (() => { + + /** + * Properties of a DocumentDataStruct. + * @exports IDocumentDataStruct + * @interface IDocumentDataStruct + * @property {Uint8Array|null} [rawFileData] DocumentDataStruct rawFileData + * @property {string|null} [secret] DocumentDataStruct secret + * @property {number|Long|null} [expirationTimestamp] DocumentDataStruct expirationTimestamp + * @property {string|null} [password] DocumentDataStruct password + */ + + /** + * Constructs a new DocumentDataStruct. + * @exports DocumentDataStruct + * @classdesc Represents a DocumentDataStruct. + * @implements IDocumentDataStruct + * @constructor + * @param {IDocumentDataStruct=} [properties] Properties to set + */ + function DocumentDataStruct(properties) { + if (properties) + for (let keys = Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null) + this[keys[i]] = properties[keys[i]]; + } + + /** + * DocumentDataStruct rawFileData. + * @member {Uint8Array} rawFileData + * @memberof DocumentDataStruct + * @instance + */ + DocumentDataStruct.prototype.rawFileData = $util.newBuffer([]); + + /** + * DocumentDataStruct secret. + * @member {string} secret + * @memberof DocumentDataStruct + * @instance + */ + DocumentDataStruct.prototype.secret = ""; + + /** + * DocumentDataStruct expirationTimestamp. + * @member {number|Long|null|undefined} expirationTimestamp + * @memberof DocumentDataStruct + * @instance + */ + DocumentDataStruct.prototype.expirationTimestamp = null; + + /** + * DocumentDataStruct password. + * @member {string|null|undefined} password + * @memberof DocumentDataStruct + * @instance + */ + DocumentDataStruct.prototype.password = null; + + // OneOf field names bound to virtual getters and setters + let $oneOfFields; + + /** + * DocumentDataStruct _expirationTimestamp. + * @member {"expirationTimestamp"|undefined} _expirationTimestamp + * @memberof DocumentDataStruct + * @instance + */ + Object.defineProperty(DocumentDataStruct.prototype, "_expirationTimestamp", { + get: $util.oneOfGetter($oneOfFields = ["expirationTimestamp"]), + set: $util.oneOfSetter($oneOfFields) + }); + + /** + * DocumentDataStruct _password. + * @member {"password"|undefined} _password + * @memberof DocumentDataStruct + * @instance + */ + Object.defineProperty(DocumentDataStruct.prototype, "_password", { + get: $util.oneOfGetter($oneOfFields = ["password"]), + set: $util.oneOfSetter($oneOfFields) + }); + + /** + * Encodes the specified DocumentDataStruct message. Does not implicitly {@link DocumentDataStruct.verify|verify} messages. + * @function encode + * @memberof DocumentDataStruct + * @static + * @param {IDocumentDataStruct} message DocumentDataStruct message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + DocumentDataStruct.encode = function encode(message, writer) { + if (!writer) + writer = $Writer.create(); + if (message.rawFileData != null && Object.hasOwnProperty.call(message, "rawFileData")) + writer.uint32(/* id 1, wireType 2 =*/10).bytes(message.rawFileData); + if (message.secret != null && Object.hasOwnProperty.call(message, "secret")) + writer.uint32(/* id 2, wireType 2 =*/18).string(message.secret); + if (message.expirationTimestamp != null && Object.hasOwnProperty.call(message, "expirationTimestamp")) + writer.uint32(/* id 3, wireType 0 =*/24).uint64(message.expirationTimestamp); + if (message.password != null && Object.hasOwnProperty.call(message, "password")) + writer.uint32(/* id 4, wireType 2 =*/34).string(message.password); + return writer; + }; + + /** + * Encodes the specified DocumentDataStruct message, length delimited. Does not implicitly {@link DocumentDataStruct.verify|verify} messages. + * @function encodeDelimited + * @memberof DocumentDataStruct + * @static + * @param {IDocumentDataStruct} message DocumentDataStruct message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + DocumentDataStruct.encodeDelimited = function encodeDelimited(message, writer) { + return this.encode(message, writer).ldelim(); + }; + + /** + * Decodes a DocumentDataStruct message from the specified reader or buffer. + * @function decode + * @memberof DocumentDataStruct + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @param {number} [length] Message length if known beforehand + * @returns {DocumentDataStruct} DocumentDataStruct + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + DocumentDataStruct.decode = function decode(reader, length) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + let end = length === undefined ? reader.len : reader.pos + length, message = new $root.DocumentDataStruct(); + while (reader.pos < end) { + let tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + message.rawFileData = reader.bytes(); + break; + } + case 2: { + message.secret = reader.string(); + break; + } + case 3: { + message.expirationTimestamp = reader.uint64(); + break; + } + case 4: { + message.password = reader.string(); + break; + } + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }; + + /** + * Decodes a DocumentDataStruct message from the specified reader or buffer, length delimited. + * @function decodeDelimited + * @memberof DocumentDataStruct + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @returns {DocumentDataStruct} DocumentDataStruct + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + DocumentDataStruct.decodeDelimited = function decodeDelimited(reader) { + if (!(reader instanceof $Reader)) + reader = new $Reader(reader); + return this.decode(reader, reader.uint32()); + }; + + /** + * Verifies a DocumentDataStruct message. + * @function verify + * @memberof DocumentDataStruct + * @static + * @param {Object.} message Plain object to verify + * @returns {string|null} `null` if valid, otherwise the reason why it is not + */ + DocumentDataStruct.verify = function verify(message) { + if (typeof message !== "object" || message === null) + return "object expected"; + let properties = {}; + if (message.rawFileData != null && message.hasOwnProperty("rawFileData")) + if (!(message.rawFileData && typeof message.rawFileData.length === "number" || $util.isString(message.rawFileData))) + return "rawFileData: buffer expected"; + if (message.secret != null && message.hasOwnProperty("secret")) + if (!$util.isString(message.secret)) + return "secret: string expected"; + if (message.expirationTimestamp != null && message.hasOwnProperty("expirationTimestamp")) { + properties._expirationTimestamp = 1; + if (!$util.isInteger(message.expirationTimestamp) && !(message.expirationTimestamp && $util.isInteger(message.expirationTimestamp.low) && $util.isInteger(message.expirationTimestamp.high))) + return "expirationTimestamp: integer|Long expected"; + } + if (message.password != null && message.hasOwnProperty("password")) { + properties._password = 1; + if (!$util.isString(message.password)) + return "password: string expected"; + } + return null; + }; + + /** + * Creates a DocumentDataStruct message from a plain object. Also converts values to their respective internal types. + * @function fromObject + * @memberof DocumentDataStruct + * @static + * @param {Object.} object Plain object + * @returns {DocumentDataStruct} DocumentDataStruct + */ + DocumentDataStruct.fromObject = function fromObject(object) { + if (object instanceof $root.DocumentDataStruct) + return object; + let message = new $root.DocumentDataStruct(); + if (object.rawFileData != null) + if (typeof object.rawFileData === "string") + $util.base64.decode(object.rawFileData, message.rawFileData = $util.newBuffer($util.base64.length(object.rawFileData)), 0); + else if (object.rawFileData.length >= 0) + message.rawFileData = object.rawFileData; + if (object.secret != null) + message.secret = String(object.secret); + if (object.expirationTimestamp != null) + if ($util.Long) + (message.expirationTimestamp = $util.Long.fromValue(object.expirationTimestamp)).unsigned = true; + else if (typeof object.expirationTimestamp === "string") + message.expirationTimestamp = parseInt(object.expirationTimestamp, 10); + else if (typeof object.expirationTimestamp === "number") + message.expirationTimestamp = object.expirationTimestamp; + else if (typeof object.expirationTimestamp === "object") + message.expirationTimestamp = new $util.LongBits(object.expirationTimestamp.low >>> 0, object.expirationTimestamp.high >>> 0).toNumber(true); + if (object.password != null) + message.password = String(object.password); + return message; + }; + + /** + * Creates a plain object from a DocumentDataStruct message. Also converts values to other types if specified. + * @function toObject + * @memberof DocumentDataStruct + * @static + * @param {DocumentDataStruct} message DocumentDataStruct + * @param {$protobuf.IConversionOptions} [options] Conversion options + * @returns {Object.} Plain object + */ + DocumentDataStruct.toObject = function toObject(message, options) { + if (!options) + options = {}; + let object = {}; + if (options.defaults) { + if (options.bytes === String) + object.rawFileData = ""; + else { + object.rawFileData = []; + if (options.bytes !== Array) + object.rawFileData = $util.newBuffer(object.rawFileData); + } + object.secret = ""; + } + if (message.rawFileData != null && message.hasOwnProperty("rawFileData")) + object.rawFileData = options.bytes === String ? $util.base64.encode(message.rawFileData, 0, message.rawFileData.length) : options.bytes === Array ? Array.prototype.slice.call(message.rawFileData) : message.rawFileData; + if (message.secret != null && message.hasOwnProperty("secret")) + object.secret = message.secret; + if (message.expirationTimestamp != null && message.hasOwnProperty("expirationTimestamp")) { + if (typeof message.expirationTimestamp === "number") + object.expirationTimestamp = options.longs === String ? String(message.expirationTimestamp) : message.expirationTimestamp; + else + object.expirationTimestamp = options.longs === String ? $util.Long.prototype.toString.call(message.expirationTimestamp) : options.longs === Number ? new $util.LongBits(message.expirationTimestamp.low >>> 0, message.expirationTimestamp.high >>> 0).toNumber(true) : message.expirationTimestamp; + if (options.oneofs) + object._expirationTimestamp = "expirationTimestamp"; + } + if (message.password != null && message.hasOwnProperty("password")) { + object.password = message.password; + if (options.oneofs) + object._password = "password"; + } + return object; + }; + + /** + * Converts this DocumentDataStruct to JSON. + * @function toJSON + * @memberof DocumentDataStruct + * @instance + * @returns {Object.} JSON object + */ + DocumentDataStruct.prototype.toJSON = function toJSON() { + return this.constructor.toObject(this, $protobuf.util.toJSONOptions); + }; + + return DocumentDataStruct; +})(); + +export { $root as default }; diff --git a/src/structures/documentStruct.ts b/src/structures/documentStruct.ts deleted file mode 100644 index e771534..0000000 --- a/src/structures/documentStruct.ts +++ /dev/null @@ -1,105 +0,0 @@ -// @generated by protobuf-ts 2.9.3 -// @generated from protobuf file "documentStruct.proto" (syntax proto3) -// tslint:disable -// -//npx protoc --proto_path=".\src\structures" --ts_out=".\src\structures" "documentStruct.proto" -// -import type { BinaryWriteOptions } from "@protobuf-ts/runtime"; -import type { IBinaryWriter } from "@protobuf-ts/runtime"; -import { WireType } from "@protobuf-ts/runtime"; -import type { BinaryReadOptions } from "@protobuf-ts/runtime"; -import type { IBinaryReader } from "@protobuf-ts/runtime"; -import { UnknownFieldHandler } from "@protobuf-ts/runtime"; -import type { PartialMessage } from "@protobuf-ts/runtime"; -import { reflectionMergePartial } from "@protobuf-ts/runtime"; -import { MessageType } from "@protobuf-ts/runtime"; -/** - * @generated from protobuf message DocumentDataStruct - */ -export interface DocumentDataStruct { - /** - * @generated from protobuf field: bytes raw_file_data = 1; - */ - rawFileData: Uint8Array; - /** - * @generated from protobuf field: string secret = 2; - */ - secret: string; - /** - * @generated from protobuf field: optional uint64 expiration_timestamp = 3; - */ - expirationTimestamp?: bigint; - /** - * @generated from protobuf field: optional string password = 4; - */ - password?: string; -} -// @generated message type with reflection information, may provide speed optimized methods -class DocumentDataStruct$Type extends MessageType { - constructor() { - super("DocumentDataStruct", [ - { no: 1, name: "raw_file_data", kind: "scalar", T: 12 /*ScalarType.BYTES*/ }, - { no: 2, name: "secret", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, - { no: 3, name: "expiration_timestamp", kind: "scalar", opt: true, T: 4 /*ScalarType.UINT64*/, L: 0 /*LongType.BIGINT*/ }, - { no: 4, name: "password", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ } - ]); - } - create(value?: PartialMessage): DocumentDataStruct { - const message = globalThis.Object.create((this.messagePrototype!)); - message.rawFileData = new Uint8Array(0); - message.secret = ""; - if (value !== undefined) - reflectionMergePartial(this, message, value); - return message; - } - internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: DocumentDataStruct): DocumentDataStruct { - let message = target ?? this.create(), end = reader.pos + length; - while (reader.pos < end) { - let [fieldNo, wireType] = reader.tag(); - switch (fieldNo) { - case /* bytes raw_file_data */ 1: - message.rawFileData = reader.bytes(); - break; - case /* string secret */ 2: - message.secret = reader.string(); - break; - case /* optional uint64 expiration_timestamp */ 3: - message.expirationTimestamp = reader.uint64().toBigInt(); - break; - case /* optional string password */ 4: - message.password = reader.string(); - break; - default: - let u = options.readUnknownField; - if (u === "throw") - throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); - let d = reader.skip(wireType); - if (u !== false) - (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); - } - } - return message; - } - internalBinaryWrite(message: DocumentDataStruct, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { - /* bytes raw_file_data = 1; */ - if (message.rawFileData.length) - writer.tag(1, WireType.LengthDelimited).bytes(message.rawFileData); - /* string secret = 2; */ - if (message.secret !== "") - writer.tag(2, WireType.LengthDelimited).string(message.secret); - /* optional uint64 expiration_timestamp = 3; */ - if (message.expirationTimestamp !== undefined) - writer.tag(3, WireType.Varint).uint64(message.expirationTimestamp); - /* optional string password = 4; */ - if (message.password !== undefined) - writer.tag(4, WireType.LengthDelimited).string(message.password); - let u = options.writeUnknownFields; - if (u !== false) - (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); - return writer; - } -} -/** - * @generated MessageType for protobuf message DocumentDataStruct - */ -export const DocumentDataStruct = new DocumentDataStruct$Type(); diff --git a/src/utils/StringUtils.ts b/src/utils/StringUtils.ts index 289a2b2..7a65d01 100644 --- a/src/utils/StringUtils.ts +++ b/src/utils/StringUtils.ts @@ -1,8 +1,8 @@ -import { basePath, characters, type Range } from './constants.ts'; +import { base64URL, basePath, type Range } from './constants.ts'; export class StringUtils { public static random(length: number, base: Range<2, 64> = 62): string { - const baseSet = characters.slice(0, base); + const baseSet = base64URL.slice(0, base); let string = ''; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index b3bc68d..89a6ef1 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -3,18 +3,6 @@ import type { ZlibCompressionOptions } from 'bun'; import type { JSPError } from '../classes/ErrorSender.ts'; import * as env from 'env-var'; -// interface Bun.env -declare module 'bun' { - interface Env { - PORT: number; - DOCS_ENABLED: boolean; - DOCS_PATH: string; - DOCS_PLAYGROUND_HTTPS: boolean; - DOCS_PLAYGROUND_DOMAIN: string; - DOCS_PLAYGROUND_PORT: number; - } -} - export enum ServerVersion { v1 = 1, v2 = 2 @@ -39,13 +27,16 @@ export enum JSPErrorCode { } export const serverConfig: Required = { + tls: env.get('TLS').asBoolStrict() ?? false, + domain: env.get('DOMAIN').default('localhost').asString(), port: env.get('PORT').default(4000).asPortNumber(), versions: [ServerVersion.v1, ServerVersion.v2], + files: {}, docs: { enabled: env.get('DOCS_ENABLED').asBoolStrict() ?? true, path: env.get('DOCS_PATH').default('/docs').asString(), playground: { - https: env.get('DOCS_PLAYGROUND_HTTPS').asBoolStrict() ?? true, + tls: env.get('DOCS_PLAYGROUND_TLS').asBoolStrict() ?? true, domain: env.get('DOCS_PLAYGROUND_DOMAIN').default('jspaste.eu').asString(), port: env.get('DOCS_PLAYGROUND_PORT').default(443).asPortNumber() } @@ -60,8 +51,7 @@ export const zlibConfig: ZlibCompressionOptions = { export const basePath = process.env['DOCUMENTS_PATH'] || 'documents/'; export const maxDocLength = parseInt(process.env['MAX_FILE_LENGTH'] || '2000000'); export const defaultDocumentLifetime = parseInt(process.env['DEFAULT_DOCUMENT_LIFETIME'] || '86400'); -export const viewDocumentPath = process.env['VIEW_DOCUMENTS_PATH'] || 'https://jspaste.eu/'; -export const characters = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_'; +export const base64URL = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_' as const; export const JSPErrorMessage: Record = { [JSPErrorCode.unknown]: { @@ -139,7 +129,7 @@ export const JSPErrorMessage: Record = { errorCode: JSPErrorCode.documentKeyAlreadyExists, message: 'The provided key already exists' } -}; +} as const; // https://github.com/microsoft/TypeScript/issues/43505 export type Range< From c23d5839a0dfd6b16fa26dcdcfafe9e21c81549d Mon Sep 17 00:00:00 2001 From: Ivan Gabaldon Date: Thu, 15 Feb 2024 23:17:05 +0100 Subject: [PATCH 10/10] Rework routes/plugins (#55) --- .env.example | 4 +- .github/workflows/security-dependencies.yml | 27 ---- .github/workflows/security.yml | 4 +- .github/workflows/test.yml | 80 ++++++++++ .prettierignore | 4 +- Containerfile | 10 +- README.md | 6 +- bun.lockb | Bin 84501 -> 84501 bytes package.json | 26 +-- src/classes/AbstractPlugin.ts | 11 ++ src/classes/AbstractRoute.ts | 11 ++ src/classes/DocumentHandler.ts | 2 +- src/classes/ErrorSender.ts | 4 +- src/classes/Server.ts | 97 +++++------- src/index.ts | 14 +- src/interfaces/ServerOptions.ts | 2 +- src/plugins/ErrorSenderPlugin.ts | 44 ++++++ src/plugins/errorSender.ts | 10 -- src/routes/AccessRawV1.ts | 50 ++++++ src/routes/AccessRawV2.ts | 79 ++++++++++ src/routes/AccessV1.ts | 47 ++++++ src/routes/AccessV2.ts | 91 +++++++++++ src/routes/{v2/edit.route.ts => EditV2.ts} | 46 +++--- src/routes/ExistsV2.ts | 32 ++++ src/routes/IndexV1.ts | 20 +++ src/routes/IndexV2.ts | 20 +++ src/routes/PublishV1.ts | 39 +++++ .../{v2/publish.route.ts => PublishV2.ts} | 60 +++---- .../{v1/remove.route.ts => RemoveV1.ts} | 44 +++--- .../{v2/remove.route.ts => RemoveV2.ts} | 44 +++--- src/routes/v1/access.route.ts | 75 --------- src/routes/v1/index.route.ts | 17 -- src/routes/v1/publish.route.ts | 29 ---- src/routes/v2/access.route.ts | 148 ------------------ src/routes/v2/exists.route.ts | 27 ---- src/routes/v2/index.route.ts | 17 -- src/structures/documentStruct.proto | 2 - tsconfig.json | 4 +- 38 files changed, 718 insertions(+), 529 deletions(-) delete mode 100644 .github/workflows/security-dependencies.yml create mode 100644 .github/workflows/test.yml create mode 100644 src/classes/AbstractPlugin.ts create mode 100644 src/classes/AbstractRoute.ts create mode 100644 src/plugins/ErrorSenderPlugin.ts delete mode 100644 src/plugins/errorSender.ts create mode 100644 src/routes/AccessRawV1.ts create mode 100644 src/routes/AccessRawV2.ts create mode 100644 src/routes/AccessV1.ts create mode 100644 src/routes/AccessV2.ts rename src/routes/{v2/edit.route.ts => EditV2.ts} (51%) create mode 100644 src/routes/ExistsV2.ts create mode 100644 src/routes/IndexV1.ts create mode 100644 src/routes/IndexV2.ts create mode 100644 src/routes/PublishV1.ts rename src/routes/{v2/publish.route.ts => PublishV2.ts} (67%) rename src/routes/{v1/remove.route.ts => RemoveV1.ts} (50%) rename src/routes/{v2/remove.route.ts => RemoveV2.ts} (50%) delete mode 100644 src/routes/v1/access.route.ts delete mode 100644 src/routes/v1/index.route.ts delete mode 100644 src/routes/v1/publish.route.ts delete mode 100644 src/routes/v2/access.route.ts delete mode 100644 src/routes/v2/exists.route.ts delete mode 100644 src/routes/v2/index.route.ts diff --git a/.env.example b/.env.example index 5725af7..c543642 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,7 @@ -# Enable HTTPS for document "url" parameter domain? [false]:boolean +# Enable HTTPS for document "url" parameter? [false]:boolean #TLS=false -# Domain for document "url" parameter domain [localhost]:string +# Domain for document "url" parameter [localhost]:string #DOMAIN=localhost # Port for the server [4000]:number diff --git a/.github/workflows/security-dependencies.yml b/.github/workflows/security-dependencies.yml deleted file mode 100644 index 045a91e..0000000 --- a/.github/workflows/security-dependencies.yml +++ /dev/null @@ -1,27 +0,0 @@ -# TODO: Experimental feature -name: 'CI -> Security-Dependencies' -on: - - pull_request - -permissions: - contents: read - -jobs: - dependency-review: - name: 'Dependency analysis' - runs-on: ubuntu-latest - steps: - - name: 'Harden Runner' - uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 - with: - egress-policy: audit - - - name: 'Checkout' - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - with: - persist-credentials: false - - - name: 'Dependency Review' - uses: actions/dependency-review-action@4cd9eb2d23752464a87e00499c30d256a59a01b4 # v4 - with: - fail-on-severity: moderate diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 4f60884..973950c 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -21,7 +21,7 @@ permissions: read-all jobs: codeql: - name: 'CodeQL analysis' + name: 'CodeQL' runs-on: ubuntu-latest strategy: fail-fast: false @@ -53,7 +53,7 @@ jobs: category: '/language:${{ matrix.language }}' scoreboard: - name: 'Scorecard analysis' + name: 'Scorecard' runs-on: ubuntu-latest permissions: security-events: write diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..b594593 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,80 @@ +# TODO: Experimental feature +name: 'CI -> Test' +on: + workflow_dispatch: + push: + branches: + - dev + paths-ignore: + - '*.md' + - '.*ignore' + + pull_request: + branches: + - dev + paths-ignore: + - '*.md' + - '.*ignore' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + lint: + name: 'Lint' + runs-on: ubuntu-latest + permissions: + id-token: write + + steps: + - name: 'Harden Runner' + uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 + with: + egress-policy: audit + + - name: 'Checkout' + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: 'Setup Bun' + uses: oven-sh/setup-bun@9b21598af8d4dfc302e5a30250007de0fda92acc # v1.1.1 + + - name: 'Setup dependencies' + run: bun install --production --frozen-lockfile --ignore-scripts + + # FIXME: Lint failing + - name: 'Run lint' + continue-on-error: true + run: bun run lint + + build: + name: 'Build' + runs-on: ubuntu-latest + permissions: + id-token: write + + steps: + - name: 'Harden Runner' + uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0 + with: + egress-policy: audit + + - name: 'Checkout' + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: 'Setup Node.js' + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + with: + node-version: current + + - name: 'Setup Bun' + uses: oven-sh/setup-bun@9b21598af8d4dfc302e5a30250007de0fda92acc # v1.1.1 + + - name: 'Setup dependencies' + run: bun install --production --frozen-lockfile --ignore-scripts + + - name: 'Run build' + run: bun run build:standalone diff --git a/.prettierignore b/.prettierignore index c65a96e..b1e5f4c 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,6 +1,6 @@ -**/documents/ **/node_modules/ -**/src/structures/ +documents/ +src/structures/ .prettierrc.json LICENSE tsconfig.json \ No newline at end of file diff --git a/Containerfile b/Containerfile index 69591c0..56d36e1 100644 --- a/Containerfile +++ b/Containerfile @@ -1,16 +1,16 @@ # Builder -FROM docker.io/oven/bun:1.0-slim AS builder +FROM docker.io/imbios/bun-node:1.0-21-alpine AS builder WORKDIR /build/ COPY . ./ RUN bun install --production --frozen-lockfile --ignore-scripts +RUN bun run production:build # Runner -FROM docker.io/oven/bun:1.0-distroless AS runner -WORKDIR /home/nonroot/ +FROM gcr.io/distroless/base-nossl-debian12:nonroot AS runner -COPY --from=builder /build/. ./ +COPY --from=builder /build/dist/jspaste ./ ENV DOCS_ENABLED=false @@ -24,4 +24,4 @@ LABEL org.opencontainers.image.licenses="EUPL-1.2" VOLUME /home/nonroot/documents EXPOSE 4000/tcp -CMD ["./src/index.ts"] \ No newline at end of file +CMD ["./jspaste"] \ No newline at end of file diff --git a/README.md b/README.md index 749dd39..98f140e 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ # Backend -The backend for [JSPaste](https://jspaste.eu), built with [Bun](https://bun.sh) and [ElysiaJS](https://elysiajs.com) +This repository contains the backend code for [JSPaste](https://jspaste.eu). It is built using [Bun](https://bun.sh) and [ElysiaJS](https://elysiajs.com) + +## License + +This project is licensed under the EUPL License. See the [`LICENSE`](LICENSE) file for more details. diff --git a/bun.lockb b/bun.lockb index 7f55aee6899792248bcab8c0cc03859f0c747df9..17d889e1e0beb9e44f106eab3d3052ec602b8435 100755 GIT binary patch delta 13045 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 G+WcS7Ai<*m delta 13006 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 diff --git a/package.json b/package.json index b2dd7cd..ad3d391 100644 --- a/package.json +++ b/package.json @@ -5,33 +5,35 @@ "license": "EUPL-1.2", "type": "module", "scripts": { - "build": "bun run build:structures && bun run build:structures:dts", + "build": "bun run build:structures && bun run build:structures:dts && bun run build:bundle", + "build:bundle": "bun build --target bun --format esm --minify --outdir ./dist/ ./src/index.ts", + "build:standalone": "bun build --compile --target bun --format esm --minify --outfile ./dist/jspaste ./src/index.ts", "build:structures": "bunx pbjs -t static-module -w es6 --no-create --no-typeurl --no-service -o ./src/structures/Structures.js ./src/structures/**/*.proto", "build:structures:dts": "bunx pbts -o ./src/structures/Structures.d.ts ./src/structures/Structures.js", - "dev": "bun run start:watch", - "fix": "bun run prettier", + "fix": "bun run fix:prettier", + "fix:prettier": "bunx prettier . --write", "lint": "bun run lint:tsc", "lint:tsc": "bunx tsc --noEmit", "prepare": "husky", - "prettier": "bunx prettier . --write", - "start": "bun ./src/index.ts", - "start:watch": "bun ./src/index.ts --watch" + "production:build": "bun run build:structures && bun run build:structures:dts && bun run build:standalone", + "start": "bun run build && bun ./dist/index.js", + "start:dev": "bun ./src/index.ts --watch" }, "dependencies": { "@elysiajs/cors": "^0.8.0", "@elysiajs/swagger": "^0.8.5", - "elysia": "^0.8.16", + "@types/bun": "^1.0.5", + "elysia": "^0.8.17", "env-var": "^7.4.1", "protobufjs": "~7.2.6", - "protobufjs-cli": "^1.1.2" + "protobufjs-cli": "^1.1.2", + "typescript": "~5.3.3" }, "devDependencies": { - "@types/bun": "^1.0.5", - "husky": "^9.0.10", + "husky": "^9.0.11", "lint-staged": "^15.2.2", "prettier": "^3.2.5", "prettier-plugin-jsdoc": "^1.3.0", - "prettier-plugin-packagejson": "^2.4.10", - "typescript": "~5.3.3" + "prettier-plugin-packagejson": "^2.4.10" } } diff --git a/src/classes/AbstractPlugin.ts b/src/classes/AbstractPlugin.ts new file mode 100644 index 0000000..58f6536 --- /dev/null +++ b/src/classes/AbstractPlugin.ts @@ -0,0 +1,11 @@ +import type { Elysia } from 'elysia'; + +export abstract class AbstractPlugin { + protected readonly server: Elysia; + + protected constructor(server: Elysia) { + this.server = server; + } + + protected abstract load(): Elysia; +} diff --git a/src/classes/AbstractRoute.ts b/src/classes/AbstractRoute.ts new file mode 100644 index 0000000..d2335fa --- /dev/null +++ b/src/classes/AbstractRoute.ts @@ -0,0 +1,11 @@ +import type { Elysia } from 'elysia'; + +export abstract class AbstractRoute { + protected readonly server: Elysia; + + protected constructor(server: Elysia) { + this.server = server; + } + + protected abstract register(path: string): void; +} diff --git a/src/classes/DocumentHandler.ts b/src/classes/DocumentHandler.ts index c347ee0..2f6dc0a 100644 --- a/src/classes/DocumentHandler.ts +++ b/src/classes/DocumentHandler.ts @@ -1,6 +1,6 @@ import { unlink } from 'node:fs/promises'; import { ValidatorUtils } from '../utils/ValidatorUtils.ts'; -import { DocumentManager } from './DocumentManager'; +import { DocumentManager } from './DocumentManager.ts'; import { basePath, defaultDocumentLifetime, diff --git a/src/classes/ErrorSender.ts b/src/classes/ErrorSender.ts index a362a77..3d3a213 100644 --- a/src/classes/ErrorSender.ts +++ b/src/classes/ErrorSender.ts @@ -1,5 +1,5 @@ import { type Context, t } from 'elysia'; -import { JSPErrorCode } from '../utils/constants'; +import { JSPErrorCode } from '../utils/constants.ts'; export interface JSPError { type: 'error'; @@ -8,7 +8,7 @@ export interface JSPError { } export class ErrorSender { - context: Context; + private readonly context: Context; public constructor(context: Context) { this.context = context; diff --git a/src/classes/Server.ts b/src/classes/Server.ts index a8e9ee5..33a1a4b 100644 --- a/src/classes/Server.ts +++ b/src/classes/Server.ts @@ -1,10 +1,21 @@ import { Elysia } from 'elysia'; import type { ServerOptions } from '../interfaces/ServerOptions.ts'; -import { JSPErrorCode, JSPErrorMessage, serverConfig } from '../utils/constants.ts'; +import { serverConfig, ServerVersion } from '../utils/constants.ts'; import swagger from '@elysiajs/swagger'; -import { join } from 'path'; -import { errorSenderPlugin } from '../plugins/errorSender.ts'; import cors from '@elysiajs/cors'; +import { IndexV1 } from '../routes/IndexV1.ts'; +import { AccessV1 } from '../routes/AccessV1.ts'; +import { AccessRawV1 } from '../routes/AccessRawV1.ts'; +import { PublishV1 } from '../routes/PublishV1.ts'; +import { RemoveV1 } from '../routes/RemoveV1.ts'; +import { ErrorSenderPlugin } from '../plugins/ErrorSenderPlugin.ts'; +import { EditV2 } from '../routes/EditV2.ts'; +import { ExistsV2 } from '../routes/ExistsV2.ts'; +import { IndexV2 } from '../routes/IndexV2.ts'; +import { PublishV2 } from '../routes/PublishV2.ts'; +import { RemoveV2 } from '../routes/RemoveV2.ts'; +import { AccessV2 } from '../routes/AccessV2.ts'; +import { AccessRawV2 } from '../routes/AccessRawV2.ts'; export class Server { private readonly server: Elysia; @@ -12,20 +23,20 @@ export class Server { public constructor(options: Partial = {}) { this.serverConfig = { ...serverConfig, ...options }; - this.server = this.initServer(); + this.server = this.createServer(); } public get self(): Elysia { return this.server; } - private initServer(): Elysia { + private createServer(): Elysia { const server = new Elysia(); + this.initCORS(server); this.serverConfig.docs.enabled && this.initDocs(server); - this.initErrorHandler(server); + this.initPlugins(server); this.initRoutes(server); - this.initCORS(server); server.listen(this.serverConfig.port, (server) => console.info('Listening on port', server.port, `-> http://localhost:${server.port}`) @@ -76,68 +87,38 @@ export class Server { ); } - private initErrorHandler(server: Elysia): void { - server.use(errorSenderPlugin).onError(({ errorSender, path, set, code, error }) => { - switch (code) { - // Redirect to the frontend 404 page - case 'NOT_FOUND': - if (path === '/404') return 'Not found'; - set.redirect = '/404'; - return; - - case 'VALIDATION': - console.error(error); - return errorSender.sendError(400, JSPErrorMessage[JSPErrorCode.validation]); + private initPlugins(server: Elysia): void { + const plugins = [ErrorSenderPlugin]; - case 'INTERNAL_SERVER_ERROR': - console.error(error); - return errorSender.sendError(500, JSPErrorMessage[JSPErrorCode.internalServerError]); - - case 'PARSE': - console.error(error); - return errorSender.sendError(400, JSPErrorMessage[JSPErrorCode.parseFailed]); - - default: - console.error(error); - return errorSender.sendError(400, JSPErrorMessage[JSPErrorCode.unknown]); - } - }); + plugins.forEach((Plugin) => server.use(new Plugin(server).load())); } private initRoutes(server: Elysia): void { - const routes = './src/routes'; const apiVersions = this.serverConfig.versions.toReversed(); + const routes = { + [ServerVersion.v1]: { + endpoints: [AccessRawV1, AccessV1, IndexV1, PublishV1, RemoveV1], + prefixes: ['/api/v1/documents'] + }, + [ServerVersion.v2]: { + endpoints: [AccessRawV2, AccessV2, EditV2, ExistsV2, IndexV2, PublishV2, RemoveV2], + prefixes: ['/api/v2/documents', '/documents'] + } + }; - console.info('Registering routes for', apiVersions.length, 'versions...'); + for (const [i, version] of apiVersions.entries()) { + routes[version].endpoints.forEach((Endpoint) => { + const endpoint = new Endpoint(server); - for (const [i, apiVersion] of apiVersions.entries()) { - const isLatestVersion = i === 0; - const routesGlob = new Bun.Glob(`v${apiVersion}/**/*.route.ts`); - const routesArray = Array.from(routesGlob.scanSync({ cwd: routes })).map((route) => { - try { - return require(join('../routes', route)).default; - } catch (err) { - console.error('Unable to import route', err); - return null; - } + routes[version].prefixes.forEach(endpoint.register.bind(endpoint)); }); - for (const resolvedRoute of routesArray) { - if (!resolvedRoute) continue; - - server.group(`/api/v${apiVersion as number}/documents`, (prefix) => prefix.use(resolvedRoute)); - - if (isLatestVersion) { - server.group('/documents', (prefix) => prefix.use(resolvedRoute)); - } - } - console.info( 'Registered', - routesArray.length, - 'routes for API version', - apiVersion, - isLatestVersion ? '(latest)' : '' + routes[version].endpoints.length, + 'routes for version', + version, + i === 0 ? '(latest)' : '' ); } } diff --git a/src/index.ts b/src/index.ts index 479166d..5e25a21 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,8 +2,12 @@ import { Server } from './classes/Server.ts'; const server = new Server(); -// FIXME(inetol): Handle exit properly (Docker) -process.on('exit', () => { - console.log('Bye'); - server.self.stop(); -}); +process + .on('SIGTERM', () => { + server.self.stop(); + process.exit(0); + }) + .on('SIGINT', () => { + server.self.stop(); + process.exit(0); + }); diff --git a/src/interfaces/ServerOptions.ts b/src/interfaces/ServerOptions.ts index 74526bf..4ae4009 100644 --- a/src/interfaces/ServerOptions.ts +++ b/src/interfaces/ServerOptions.ts @@ -1,4 +1,4 @@ -import type { ServerVersion } from '../utils/constants'; +import type { ServerVersion } from '../utils/constants.ts'; export interface ServerOptions { tls: boolean; diff --git a/src/plugins/ErrorSenderPlugin.ts b/src/plugins/ErrorSenderPlugin.ts new file mode 100644 index 0000000..49a16d2 --- /dev/null +++ b/src/plugins/ErrorSenderPlugin.ts @@ -0,0 +1,44 @@ +import Elysia from 'elysia'; +import { ErrorSender } from '../classes/ErrorSender.ts'; +import { AbstractPlugin } from '../classes/AbstractPlugin.ts'; +import { JSPErrorCode, JSPErrorMessage } from '../utils/constants.ts'; + +export class ErrorSenderPlugin extends AbstractPlugin { + public constructor(server: Elysia) { + super(server); + } + + public override load(): Elysia { + return this.server + .derive((context) => { + return { + errorSender: new ErrorSender(context) + }; + }) + .onError(({ errorSender, path, set, code, error }) => { + switch (code) { + // Redirect to the frontend 404 page + case 'NOT_FOUND': + if (path === '/404') return 'Not found'; + set.redirect = '/404'; + return; + + case 'VALIDATION': + console.error(error); + return errorSender.sendError(400, JSPErrorMessage[JSPErrorCode.validation]); + + case 'INTERNAL_SERVER_ERROR': + console.error(error); + return errorSender.sendError(500, JSPErrorMessage[JSPErrorCode.internalServerError]); + + case 'PARSE': + console.error(error); + return errorSender.sendError(400, JSPErrorMessage[JSPErrorCode.parseFailed]); + + default: + console.error(error); + return errorSender.sendError(400, JSPErrorMessage[JSPErrorCode.unknown]); + } + }); + } +} diff --git a/src/plugins/errorSender.ts b/src/plugins/errorSender.ts deleted file mode 100644 index 6f1cd3d..0000000 --- a/src/plugins/errorSender.ts +++ /dev/null @@ -1,10 +0,0 @@ -import Elysia from 'elysia'; -import { ErrorSender } from '../classes/ErrorSender'; - -export const errorSenderPlugin = new Elysia({ - name: 'plugins:errorHandler' -}).derive((context) => { - return { - errorSender: new ErrorSender(context) - }; -}); diff --git a/src/routes/AccessRawV1.ts b/src/routes/AccessRawV1.ts new file mode 100644 index 0000000..70fde82 --- /dev/null +++ b/src/routes/AccessRawV1.ts @@ -0,0 +1,50 @@ +import { AbstractRoute } from '../classes/AbstractRoute.ts'; +import { type Elysia, t } from 'elysia'; +import { DocumentHandler } from '../classes/DocumentHandler.ts'; +import { ServerVersion } from '../utils/constants.ts'; +import { ErrorSender } from '../classes/ErrorSender.ts'; + +export class AccessRawV1 extends AbstractRoute { + public constructor(server: Elysia) { + super(server); + } + + public override register(path: string): void { + const hook = { + params: t.Object( + { + key: t.String({ + description: 'The document key', + examples: ['abc123'] + }) + }, + { + description: 'The request parameters', + examples: [{ key: 'abc123' }] + } + ), + response: { + 200: t.Any({ + description: 'The raw document', + examples: ['Hello world'] + }), + 400: ErrorSender.errorType(), + 404: ErrorSender.errorType() + }, + detail: { + summary: 'Get raw document', + tags: ['v1'] + } + }; + + this.server.get( + path.concat('/:key/raw'), + async ({ errorSender, set, params: { key } }) => { + set.headers['Content-Type'] = 'text/plain'; + + return DocumentHandler.handleAccess({ errorSender, key: key, raw: true }, ServerVersion.v1); + }, + hook + ); + } +} diff --git a/src/routes/AccessRawV2.ts b/src/routes/AccessRawV2.ts new file mode 100644 index 0000000..eeb1e6b --- /dev/null +++ b/src/routes/AccessRawV2.ts @@ -0,0 +1,79 @@ +import { AbstractRoute } from '../classes/AbstractRoute.ts'; +import { type Elysia, t } from 'elysia'; +import { DocumentHandler } from '../classes/DocumentHandler.ts'; +import { ServerVersion } from '../utils/constants.ts'; +import { ErrorSender } from '../classes/ErrorSender.ts'; + +export class AccessRawV2 extends AbstractRoute { + public constructor(server: Elysia) { + super(server); + } + + public override register(path: string): void { + const hook = { + params: t.Object( + { + key: t.String({ + description: 'The document key', + examples: ['abc123'] + }) + }, + { + description: 'The request parameters', + examples: [{ key: 'abc123' }] + } + ), + headers: t.Optional( + t.Object({ + password: t.Optional( + t.String({ + description: 'The document password if aplicable', + examples: ['aaaaa-bbbbb-ccccc-ddddd'] + }) + ) + }) + ), + query: t.Optional( + t.Object({ + p: t.Optional( + t.String({ + description: + 'The document password if aplicable, It is preferred to pass the password through headers, only use this method for support of web browsers.', + examples: ['aaaaa-bbbbb-ccccc-ddddd'] + }) + ) + }) + ), + response: { + 200: t.Any({ + description: 'The raw document', + examples: ['Hello world'] + }), + 400: ErrorSender.errorType(), + 404: ErrorSender.errorType() + }, + detail: { + summary: 'Get raw document', + tags: ['v2'] + } + }; + + this.server.get( + path.concat('/:key/raw'), + async ({ errorSender, set, request, query: { p }, params: { key } }) => { + set.headers['Content-Type'] = 'text/plain'; + + return DocumentHandler.handleAccess( + { + errorSender, + key, + password: request.headers.get('password') || p || '', + raw: true + }, + ServerVersion.v2 + ); + }, + hook + ); + } +} diff --git a/src/routes/AccessV1.ts b/src/routes/AccessV1.ts new file mode 100644 index 0000000..e7fe9b2 --- /dev/null +++ b/src/routes/AccessV1.ts @@ -0,0 +1,47 @@ +import { AbstractRoute } from '../classes/AbstractRoute.ts'; +import { type Elysia, t } from 'elysia'; +import { DocumentHandler } from '../classes/DocumentHandler.ts'; +import { ServerVersion } from '../utils/constants.ts'; +import { ErrorSender } from '../classes/ErrorSender.ts'; + +export class AccessV1 extends AbstractRoute { + public constructor(server: Elysia) { + super(server); + } + + public override register(path: string): void { + const hook = { + params: t.Object({ + key: t.String({ + description: 'The document key', + examples: ['abc123'] + }) + }), + response: { + 200: t.Object( + { + key: t.String({ + description: 'The key of the document', + examples: ['abc123'] + }), + data: t.String({ + description: 'The document', + examples: ['Hello world'] + }) + }, + { description: 'The document object' } + ), + 400: ErrorSender.errorType(), + 404: ErrorSender.errorType() + }, + detail: { summary: 'Get document', tags: ['v1'] } + }; + + this.server.get( + path.concat('/:key'), + async ({ errorSender, params: { key } }) => + DocumentHandler.handleAccess({ errorSender, key: key }, ServerVersion.v1), + hook + ); + } +} diff --git a/src/routes/AccessV2.ts b/src/routes/AccessV2.ts new file mode 100644 index 0000000..9bd0fff --- /dev/null +++ b/src/routes/AccessV2.ts @@ -0,0 +1,91 @@ +import { AbstractRoute } from '../classes/AbstractRoute.ts'; +import { type Elysia, t } from 'elysia'; +import { DocumentHandler } from '../classes/DocumentHandler.ts'; +import { ServerVersion } from '../utils/constants.ts'; +import { ErrorSender } from '../classes/ErrorSender.ts'; + +export class AccessV2 extends AbstractRoute { + public constructor(server: Elysia) { + super(server); + } + + public override register(path: string): void { + const hook = { + params: t.Object({ + key: t.String({ + description: 'The document key', + examples: ['abc123'] + }) + }), + headers: t.Optional( + t.Object({ + password: t.Optional( + t.String({ + description: 'The document password if aplicable', + examples: ['abc123'] + }) + ) + }) + ), + query: t.Optional( + t.Object({ + p: t.Optional( + t.String({ + description: + 'The document password if aplicable, It is preferred to pass the password through headers, only use this method for support of web browsers.', + examples: ['aaaaa-bbbbb-ccccc-ddddd'] + }) + ) + }) + ), + response: { + 200: t.Object( + { + key: t.String({ + description: 'The key of the document', + examples: ['abc123'] + }), + data: t.String({ + description: 'The document', + examples: ['Hello world'] + }), + url: t.Optional( + t.String({ + description: 'The URL for viewing the document on the web', + examples: ['https://jspaste.eu/abc123'] + }) + ), + expirationTimestamp: t.Optional( + t.Number({ + description: + 'UNIX timestamp with the expiration date in milliseconds. Undefined if the document is permanent.', + examples: [60, 0] + }) + ) + }, + { + description: + 'The document object, including the key, the data, the display URL and an expiration timestamp for the document' + } + ), + 400: ErrorSender.errorType(), + 404: ErrorSender.errorType() + }, + detail: { summary: 'Get document', tags: ['v2'] } + }; + + this.server.get( + path.concat('/:key'), + async ({ errorSender, request, query: { p }, params: { key } }) => + DocumentHandler.handleAccess( + { + errorSender, + key, + password: request.headers.get('password') || p || '' + }, + ServerVersion.v2 + ), + hook + ); + } +} diff --git a/src/routes/v2/edit.route.ts b/src/routes/EditV2.ts similarity index 51% rename from src/routes/v2/edit.route.ts rename to src/routes/EditV2.ts index 4314f70..ff22bad 100644 --- a/src/routes/v2/edit.route.ts +++ b/src/routes/EditV2.ts @@ -1,22 +1,15 @@ -import { Elysia, t } from 'elysia'; -import { ErrorSender } from '../../classes/ErrorSender'; -import { DocumentHandler } from '../../classes/DocumentHandler.ts'; -import { errorSenderPlugin } from '../../plugins/errorSender.ts'; +import { AbstractRoute } from '../classes/AbstractRoute.ts'; +import { type Elysia, t } from 'elysia'; +import { DocumentHandler } from '../classes/DocumentHandler.ts'; +import { ErrorSender } from '../classes/ErrorSender.ts'; -export default new Elysia({ - name: 'routes:v2:documents:edit' -}) - .use(errorSenderPlugin) - .patch( - ':key', - async ({ errorSender, request, body, params: { key } }) => - DocumentHandler.handleEdit({ - errorSender, - key, - newBody: body, - secret: request.headers.get('secret') || '' - }), - { +export class EditV2 extends AbstractRoute { + public constructor(server: Elysia) { + super(server); + } + + public override register(path: string): void { + const hook = { type: 'arrayBuffer', body: t.Any({ description: 'The new file' }), params: t.Object({ @@ -45,5 +38,18 @@ export default new Elysia({ 404: ErrorSender.errorType() }, detail: { summary: 'Edit document', tags: ['v2'] } - } - ); + }; + + this.server.patch( + path.concat('/:key'), + async ({ errorSender, request, body, params: { key } }) => + DocumentHandler.handleEdit({ + errorSender, + key, + newBody: body, + secret: request.headers.get('secret') || '' + }), + hook + ); + } +} diff --git a/src/routes/ExistsV2.ts b/src/routes/ExistsV2.ts new file mode 100644 index 0000000..e4d47ec --- /dev/null +++ b/src/routes/ExistsV2.ts @@ -0,0 +1,32 @@ +import { AbstractRoute } from '../classes/AbstractRoute.ts'; +import { type Elysia, t } from 'elysia'; +import { ErrorSender } from '../classes/ErrorSender.ts'; +import { DocumentHandler } from '../classes/DocumentHandler.ts'; + +export class ExistsV2 extends AbstractRoute { + public constructor(server: Elysia) { + super(server); + } + + public override register(path: string): void { + const hook = { + params: t.Object({ + key: t.String({ + description: 'The document key', + examples: ['abc123'] + }) + }), + response: { + 200: t.Boolean({ description: 'A boolean indicating if the document exists' }), + 400: ErrorSender.errorType() + }, + detail: { summary: 'Check document', tags: ['v2'] } + }; + + this.server.get( + path.concat('/:key/exists'), + async ({ errorSender, params: { key } }) => DocumentHandler.handleExists({ errorSender, key: key }), + hook + ); + } +} diff --git a/src/routes/IndexV1.ts b/src/routes/IndexV1.ts new file mode 100644 index 0000000..34a0c2d --- /dev/null +++ b/src/routes/IndexV1.ts @@ -0,0 +1,20 @@ +import { AbstractRoute } from '../classes/AbstractRoute.ts'; +import { type Elysia, t } from 'elysia'; + +export class IndexV1 extends AbstractRoute { + public constructor(server: Elysia) { + super(server); + } + + public override register(path: string): void { + const hook = { + response: t.String({ + description: 'A small welcome message with the current API version', + examples: ['Welcome to JSPaste API v1'] + }), + detail: { summary: 'Index', tags: ['v1'] } + }; + + this.server.get(path, () => 'Welcome to JSPaste API v1', hook); + } +} diff --git a/src/routes/IndexV2.ts b/src/routes/IndexV2.ts new file mode 100644 index 0000000..7b1796f --- /dev/null +++ b/src/routes/IndexV2.ts @@ -0,0 +1,20 @@ +import { AbstractRoute } from '../classes/AbstractRoute.ts'; +import { type Elysia, t } from 'elysia'; + +export class IndexV2 extends AbstractRoute { + public constructor(server: Elysia) { + super(server); + } + + public override register(path: string): void { + const hook = { + response: t.String({ + description: 'A small welcome message with the current API version', + examples: ['Welcome to JSPaste API v2'] + }), + detail: { summary: 'Index', tags: ['v2'] } + }; + + this.server.get(path, () => 'Welcome to JSPaste API v2', hook); + } +} diff --git a/src/routes/PublishV1.ts b/src/routes/PublishV1.ts new file mode 100644 index 0000000..675607a --- /dev/null +++ b/src/routes/PublishV1.ts @@ -0,0 +1,39 @@ +import { AbstractRoute } from '../classes/AbstractRoute.ts'; +import { type Elysia, t } from 'elysia'; +import { DocumentHandler } from '../classes/DocumentHandler.ts'; +import { ErrorSender } from '../classes/ErrorSender.ts'; +import { ServerVersion } from '../utils/constants.ts'; + +export class PublishV1 extends AbstractRoute { + public constructor(server: Elysia) { + super(server); + } + + public override register(path: string): void { + const hook = { + type: 'arrayBuffer', + body: t.Any({ description: 'The file to be uploaded' }), + response: { + 200: t.Object( + { + key: t.String({ + description: 'The generated key to access the document' + }), + secret: t.String({ + description: 'The generated secret to delete the document' + }) + }, + { description: 'An object with a key and a secret for the document' } + ), + 400: ErrorSender.errorType() + }, + detail: { summary: 'Publish document', tags: ['v1'] } + }; + + this.server.post( + path, + async ({ errorSender, body }) => DocumentHandler.handlePublish({ errorSender, body }, ServerVersion.v1), + hook + ); + } +} diff --git a/src/routes/v2/publish.route.ts b/src/routes/PublishV2.ts similarity index 67% rename from src/routes/v2/publish.route.ts rename to src/routes/PublishV2.ts index 0d52acf..6795af2 100644 --- a/src/routes/v2/publish.route.ts +++ b/src/routes/PublishV2.ts @@ -1,29 +1,16 @@ -import { Elysia, t } from 'elysia'; -import { ErrorSender } from '../../classes/ErrorSender'; -import { DocumentHandler } from '../../classes/DocumentHandler.ts'; -import { defaultDocumentLifetime, ServerVersion } from '../../utils/constants.ts'; -import { errorSenderPlugin } from '../../plugins/errorSender.ts'; +import { AbstractRoute } from '../classes/AbstractRoute.ts'; +import { type Elysia, t } from 'elysia'; +import { DocumentHandler } from '../classes/DocumentHandler.ts'; +import { ErrorSender } from '../classes/ErrorSender.ts'; +import { defaultDocumentLifetime, ServerVersion } from '../utils/constants.ts'; -export default new Elysia({ - name: 'routes:v2:documents:publish' -}) - .use(errorSenderPlugin) - .post( - '', - async ({ errorSender, request, query, body }) => - DocumentHandler.handlePublish( - { - errorSender, - body, - selectedKey: request.headers.get('key') || '', - selectedKeyLength: parseInt(request.headers.get('key-length') ?? '') || undefined, - selectedSecret: request.headers.get('secret') || '', - lifetime: parseInt(request.headers.get('lifetime') || defaultDocumentLifetime.toString()), - password: request.headers.get('password') || query['password'] || '' - }, - ServerVersion.v2 - ), - { +export class PublishV2 extends AbstractRoute { + public constructor(server: Elysia) { + super(server); + } + + public override register(path: string): void { + const hook = { type: 'arrayBuffer', body: t.Any({ description: 'The file to be uploaded' @@ -97,5 +84,24 @@ export default new Elysia({ 400: ErrorSender.errorType() }, detail: { summary: 'Publish document', tags: ['v2'] } - } - ); + }; + + this.server.post( + path, + async ({ errorSender, request, query, body }) => + DocumentHandler.handlePublish( + { + errorSender, + body, + selectedKey: request.headers.get('key') || '', + selectedKeyLength: parseInt(request.headers.get('key-length') ?? '') || undefined, + selectedSecret: request.headers.get('secret') || '', + lifetime: parseInt(request.headers.get('lifetime') || defaultDocumentLifetime.toString()), + password: request.headers.get('password') || query['password'] || '' + }, + ServerVersion.v2 + ), + hook + ); + } +} diff --git a/src/routes/v1/remove.route.ts b/src/routes/RemoveV1.ts similarity index 50% rename from src/routes/v1/remove.route.ts rename to src/routes/RemoveV1.ts index 5f75de5..e39255a 100644 --- a/src/routes/v1/remove.route.ts +++ b/src/routes/RemoveV1.ts @@ -1,21 +1,15 @@ -import { Elysia, t } from 'elysia'; -import { ErrorSender } from '../../classes/ErrorSender'; -import { DocumentHandler } from '../../classes/DocumentHandler.ts'; -import { errorSenderPlugin } from '../../plugins/errorSender.ts'; +import { AbstractRoute } from '../classes/AbstractRoute.ts'; +import { type Elysia, t } from 'elysia'; +import { ErrorSender } from '../classes/ErrorSender.ts'; +import { DocumentHandler } from '../classes/DocumentHandler.ts'; -export default new Elysia({ - name: 'routes:v1:documents:remove' -}) - .use(errorSenderPlugin) - .delete( - ':key', - async ({ errorSender, request, params: { key } }) => - DocumentHandler.handleRemove({ - errorSender, - key, - secret: request.headers.get('secret') || '' - }), - { +export class RemoveV1 extends AbstractRoute { + public constructor(server: Elysia) { + super(server); + } + + public override register(path: string): void { + const hook = { params: t.Object({ key: t.String({ description: 'The document key', @@ -42,5 +36,17 @@ export default new Elysia({ 404: ErrorSender.errorType() }, detail: { summary: 'Remove document', tags: ['v1'] } - } - ); + }; + + this.server.delete( + path.concat('/:key'), + async ({ errorSender, request, params: { key } }) => + DocumentHandler.handleRemove({ + errorSender, + key, + secret: request.headers.get('secret') || '' + }), + hook + ); + } +} diff --git a/src/routes/v2/remove.route.ts b/src/routes/RemoveV2.ts similarity index 50% rename from src/routes/v2/remove.route.ts rename to src/routes/RemoveV2.ts index f8b2fd6..0c736ca 100644 --- a/src/routes/v2/remove.route.ts +++ b/src/routes/RemoveV2.ts @@ -1,21 +1,15 @@ -import { Elysia, t } from 'elysia'; -import { ErrorSender } from '../../classes/ErrorSender'; -import { DocumentHandler } from '../../classes/DocumentHandler.ts'; -import { errorSenderPlugin } from '../../plugins/errorSender.ts'; +import { AbstractRoute } from '../classes/AbstractRoute.ts'; +import { type Elysia, t } from 'elysia'; +import { ErrorSender } from '../classes/ErrorSender.ts'; +import { DocumentHandler } from '../classes/DocumentHandler.ts'; -export default new Elysia({ - name: 'routes:v2:documents:remove' -}) - .use(errorSenderPlugin) - .delete( - ':key', - async ({ errorSender, request, params: { key } }) => - DocumentHandler.handleRemove({ - errorSender, - key, - secret: request.headers.get('secret') || '' - }), - { +export class RemoveV2 extends AbstractRoute { + public constructor(server: Elysia) { + super(server); + } + + public override register(path: string): void { + const hook = { params: t.Object({ key: t.String({ description: 'The document key', @@ -42,5 +36,17 @@ export default new Elysia({ 404: ErrorSender.errorType() }, detail: { summary: 'Remove document', tags: ['v2'] } - } - ); + }; + + this.server.delete( + path.concat('/:key'), + async ({ errorSender, request, params: { key } }) => + DocumentHandler.handleRemove({ + errorSender, + key, + secret: request.headers.get('secret') || '' + }), + hook + ); + } +} diff --git a/src/routes/v1/access.route.ts b/src/routes/v1/access.route.ts deleted file mode 100644 index 0ec94ef..0000000 --- a/src/routes/v1/access.route.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { Elysia, t } from 'elysia'; -import { ErrorSender } from '../../classes/ErrorSender'; -import { DocumentHandler } from '../../classes/DocumentHandler.ts'; -import { errorSenderPlugin } from '../../plugins/errorSender.ts'; -import { ServerVersion } from '../../utils/constants.ts'; - -export default new Elysia({ - name: 'routes:v1:documents:access' -}) - .use(errorSenderPlugin) - .get( - ':key', - async ({ errorSender, params: { key } }) => - DocumentHandler.handleAccess({ errorSender, key: key }, ServerVersion.v1), - { - params: t.Object({ - key: t.String({ - description: 'The document key', - examples: ['abc123'] - }) - }), - response: { - 200: t.Object( - { - key: t.String({ - description: 'The key of the document', - examples: ['abc123'] - }), - data: t.String({ - description: 'The document', - examples: ['Hello world'] - }) - }, - { description: 'The document object' } - ), - 400: ErrorSender.errorType(), - 404: ErrorSender.errorType() - }, - detail: { summary: 'Get document', tags: ['v1'] } - } - ) - .get( - ':key/raw', - async ({ errorSender, set, params: { key } }) => { - set.headers['Content-Type'] = 'text/plain'; - - return DocumentHandler.handleAccess({ errorSender, key: key, raw: true }, ServerVersion.v1); - }, - { - params: t.Object( - { - key: t.String({ - description: 'The document key', - examples: ['abc123'] - }) - }, - { - description: 'The request parameters', - examples: [{ key: 'abc123' }] - } - ), - response: { - 200: t.Any({ - description: 'The raw document', - examples: ['Hello world'] - }), - 400: ErrorSender.errorType(), - 404: ErrorSender.errorType() - }, - detail: { - summary: 'Get raw document', - tags: ['v1'] - } - } - ); diff --git a/src/routes/v1/index.route.ts b/src/routes/v1/index.route.ts deleted file mode 100644 index dc9e617..0000000 --- a/src/routes/v1/index.route.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Elysia, t } from 'elysia'; - -export default new Elysia({ - name: 'routes:v1:documents' -}).get( - '', - () => { - return 'Welcome to JSPaste API v1'; - }, - { - response: t.String({ - description: 'A small welcome message with the current API version', - examples: ['Welcome to JSPaste API v1'] - }), - detail: { summary: 'Index', tags: ['v1'] } - } -); diff --git a/src/routes/v1/publish.route.ts b/src/routes/v1/publish.route.ts deleted file mode 100644 index 67589bb..0000000 --- a/src/routes/v1/publish.route.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Elysia, t } from 'elysia'; -import { ErrorSender } from '../../classes/ErrorSender'; -import { DocumentHandler } from '../../classes/DocumentHandler.ts'; -import { errorSenderPlugin } from '../../plugins/errorSender.ts'; -import { ServerVersion } from '../../utils/constants.ts'; - -export default new Elysia({ - name: 'routes:v1:documents:publish' -}) - .use(errorSenderPlugin) - .post('', async ({ errorSender, body }) => DocumentHandler.handlePublish({ errorSender, body }, ServerVersion.v1), { - type: 'arrayBuffer', - body: t.Any({ description: 'The file to be uploaded' }), - response: { - 200: t.Object( - { - key: t.String({ - description: 'The generated key to access the document' - }), - secret: t.String({ - description: 'The generated secret to delete the document' - }) - }, - { description: 'An object with a key and a secret for the document' } - ), - 400: ErrorSender.errorType() - }, - detail: { summary: 'Publish document', tags: ['v1'] } - }); diff --git a/src/routes/v2/access.route.ts b/src/routes/v2/access.route.ts deleted file mode 100644 index e887d07..0000000 --- a/src/routes/v2/access.route.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { Elysia, t } from 'elysia'; -import { ErrorSender } from '../../classes/ErrorSender'; -import { errorSenderPlugin } from '../../plugins/errorSender.ts'; -import { DocumentHandler } from '../../classes/DocumentHandler.ts'; -import { ServerVersion } from '../../utils/constants.ts'; - -export default new Elysia({ - name: 'routes:v2:documents:access' -}) - .use(errorSenderPlugin) - .get( - ':key', - async ({ errorSender, request, query: { p }, params: { key } }) => - DocumentHandler.handleAccess( - { - errorSender, - key, - password: request.headers.get('password') || p || '' - }, - ServerVersion.v2 - ), - { - params: t.Object({ - key: t.String({ - description: 'The document key', - examples: ['abc123'] - }) - }), - headers: t.Optional( - t.Object({ - password: t.Optional( - t.String({ - description: 'The document password if aplicable', - examples: ['abc123'] - }) - ) - }) - ), - query: t.Optional( - t.Object({ - p: t.Optional( - t.String({ - description: - 'The document password if aplicable, It is preferred to pass the password through headers, only use this method for support of web browsers.', - examples: ['aaaaa-bbbbb-ccccc-ddddd'] - }) - ) - }) - ), - response: { - 200: t.Object( - { - key: t.String({ - description: 'The key of the document', - examples: ['abc123'] - }), - data: t.String({ - description: 'The document', - examples: ['Hello world'] - }), - url: t.Optional( - t.String({ - description: 'The URL for viewing the document on the web', - examples: ['https://jspaste.eu/abc123'] - }) - ), - expirationTimestamp: t.Optional( - t.Number({ - description: - 'UNIX timestamp with the expiration date in milliseconds. Undefined if the document is permanent.', - examples: [60, 0] - }) - ) - }, - { - description: - 'The document object, including the key, the data, the display URL and an expiration timestamp for the document' - } - ), - 400: ErrorSender.errorType(), - 404: ErrorSender.errorType() - }, - detail: { summary: 'Get document', tags: ['v2'] } - } - ) - .get( - ':key/raw', - async ({ errorSender, set, request, query: { p }, params: { key } }) => { - set.headers['Content-Type'] = 'text/plain'; - - return DocumentHandler.handleAccess( - { - errorSender, - key, - password: request.headers.get('password') || p || '', - raw: true - }, - ServerVersion.v2 - ); - }, - { - params: t.Object( - { - key: t.String({ - description: 'The document key', - examples: ['abc123'] - }) - }, - { - description: 'The request parameters', - examples: [{ key: 'abc123' }] - } - ), - headers: t.Optional( - t.Object({ - password: t.Optional( - t.String({ - description: 'The document password if aplicable', - examples: ['aaaaa-bbbbb-ccccc-ddddd'] - }) - ) - }) - ), - query: t.Optional( - t.Object({ - p: t.Optional( - t.String({ - description: - 'The document password if aplicable, It is preferred to pass the password through headers, only use this method for support of web browsers.', - examples: ['aaaaa-bbbbb-ccccc-ddddd'] - }) - ) - }) - ), - response: { - 200: t.Any({ - description: 'The raw document', - examples: ['Hello world'] - }), - 400: ErrorSender.errorType(), - 404: ErrorSender.errorType() - }, - detail: { - summary: 'Get raw document', - tags: ['v2'] - } - } - ); diff --git a/src/routes/v2/exists.route.ts b/src/routes/v2/exists.route.ts deleted file mode 100644 index 52c6049..0000000 --- a/src/routes/v2/exists.route.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Elysia, t } from 'elysia'; -import { ErrorSender } from '../../classes/ErrorSender.ts'; -import { DocumentHandler } from '../../classes/DocumentHandler.ts'; -import { errorSenderPlugin } from '../../plugins/errorSender.ts'; - -export default new Elysia({ - name: 'routes:v2:documents:exists' -}) - .use(errorSenderPlugin) - .get( - ':key/exists', - async ({ errorSender, params: { key } }) => DocumentHandler.handleExists({ errorSender, key: key }), - { - params: t.Object({ - key: t.String({ - description: 'The document key', - examples: ['abc123'] - }) - }), - response: { - 200: t.Boolean({ description: 'A boolean indicating if the document exists' }), - 400: ErrorSender.errorType() - }, - - detail: { summary: 'Check document', tags: ['v2'] } - } - ); diff --git a/src/routes/v2/index.route.ts b/src/routes/v2/index.route.ts deleted file mode 100644 index 5fc6f49..0000000 --- a/src/routes/v2/index.route.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Elysia, t } from 'elysia'; - -export default new Elysia({ - name: 'routes:v2:documents' -}).get( - '', - () => { - return 'Welcome to JSPaste API v2'; - }, - { - response: t.String({ - description: 'A small welcome message with the current API version', - examples: ['Welcome to JSPaste API v2'] - }), - detail: { summary: 'Index', tags: ['v2'] } - } -); diff --git a/src/structures/documentStruct.proto b/src/structures/documentStruct.proto index a8a3ab2..f6793ea 100644 --- a/src/structures/documentStruct.proto +++ b/src/structures/documentStruct.proto @@ -1,5 +1,3 @@ -//npx protoc --proto_path=".\src\structures" --ts_out=".\src\structures" "documentStruct.proto" - syntax = "proto3"; option optimize_for = SPEED; diff --git a/tsconfig.json b/tsconfig.json index 11214e7..ab75125 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -24,8 +24,10 @@ "noImplicitReturns": true, "noPropertyAccessFromIndexSignature": true, "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, "verbatimModuleSyntax": true }, - "include": ["src/**/*.ts"], + "include": ["src/**/*"], "exclude": ["dist/", "documents/", "node_modules/"] } \ No newline at end of file