diff --git a/src/environment/config.ts b/src/environment/config.ts index 81c7bf0..7d389d2 100644 --- a/src/environment/config.ts +++ b/src/environment/config.ts @@ -2,7 +2,7 @@ import { echo } from "@atums/echo"; import { validateJWTConfig, validateMailerConfig } from "#lib/validation"; import { isValidUrl } from "#lib/validation/url"; import { requiredVariables } from "./constants"; -import { cassandraConfig, validateCassandraConfig } from "./database/cassandra"; +import { cassandraConfig, validateCassandraConfig } from "./database"; import { jwt } from "./jwt"; import { mailerConfig } from "./mailer"; diff --git a/src/environment/constants/guild/defaults.ts b/src/environment/constants/guild/defaults.ts new file mode 100644 index 0000000..f0d23bf --- /dev/null +++ b/src/environment/constants/guild/defaults.ts @@ -0,0 +1,12 @@ +function createDefaultGuildData() { + return { + verification_level: 0, + default_message_notifications: 0, + preferred_locale: "en-US", + features: new Set(), + created_at: new Date(), + updated_at: new Date(), + }; +} + +export { createDefaultGuildData }; diff --git a/src/environment/constants/guild/features.ts b/src/environment/constants/guild/features.ts new file mode 100644 index 0000000..52ee904 --- /dev/null +++ b/src/environment/constants/guild/features.ts @@ -0,0 +1,7 @@ +const GUILD_FEATURES = { + COMMUNITY: "COMMUNITY", + VERIFIED: "VERIFIED", + // idrk know more rn +}; + +export { GUILD_FEATURES }; diff --git a/src/environment/constants/guild/index.ts b/src/environment/constants/guild/index.ts new file mode 100644 index 0000000..8156f2d --- /dev/null +++ b/src/environment/constants/guild/index.ts @@ -0,0 +1 @@ +export * from "./defaults"; diff --git a/src/environment/constants/index.ts b/src/environment/constants/index.ts index 062632b..b454586 100644 --- a/src/environment/constants/index.ts +++ b/src/environment/constants/index.ts @@ -34,3 +34,4 @@ export * from "./mailer"; export * from "./user"; export * from "./cache"; export * from "./http"; +export * from "./guild"; diff --git a/src/environment/constants/validation.ts b/src/environment/constants/validation.ts index 8232cc3..840b79c 100644 --- a/src/environment/constants/validation.ts +++ b/src/environment/constants/validation.ts @@ -5,6 +5,62 @@ const nameRestrictions: genericValidation = { regex: /^[\p{L}\p{N}._-]+$/u, }; +const reservedNames = [ + "admin", + "root", + "system", + "administrator", + "mod", + "moderator", + "owner", + "superuser", + "sudo", + "staff", + "support", + "help", + + "server", + "guild", + "channel", + + "null", + "undefined", + "void", + "nil", + "none", + "empty", + "blank", + "true", + "false", + "yes", + "no", + "on", + "off", + + "official", + "verified", + "team", + "company", + "corp", + "inc", + "llc", + "trademark", + "copyright", + "dmca", + + "online", + "offline", + "away", + "busy", + "dnd", + "invisible", + "active", + "inactive", + "banned", + "suspended", + "deleted", +]; + const displayNameRestrictions: genericValidation = { length: { min: 1, max: 32 }, regex: /^[\p{L}\p{N}\p{M}\p{S}\p{P}\s]+$/u, @@ -14,7 +70,6 @@ const forbiddenDisplayNamePatterns = [ /[\r\n\t]/, /\s{3,}/, /^\s|\s$/, - /@everyone|@here/i, /\p{Cf}/u, /\p{Cc}/u, ]; @@ -28,10 +83,17 @@ const emailRestrictions: { regex: RegExp } = { regex: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, }; +const avatarRestrictions: genericValidation = { + length: { min: 1, max: 5 * 1024 * 1024 }, // 5 MB + regex: /^(data:image\/(jpeg|png|gif|webp);base64,)/, +}; + export { nameRestrictions, displayNameRestrictions, forbiddenDisplayNamePatterns, passwordRestrictions, emailRestrictions, + reservedNames, + avatarRestrictions, }; diff --git a/src/environment/database/cassandra.ts b/src/environment/database/cassandra.ts deleted file mode 100644 index 25a988d..0000000 --- a/src/environment/database/cassandra.ts +++ /dev/null @@ -1,114 +0,0 @@ -import type { CassandraConfig } from "#types/config"; -import type { simpleConfigValidation } from "#types/lib"; - -function isValidHost(host: string): boolean { - if (!host || host.trim().length === 0) return false; - - if (host === "localhost") return true; - - const ipv4Regex = - /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; - if (ipv4Regex.test(host)) return true; - - const hostnameRegex = - /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; - return hostnameRegex.test(host); -} - -function isValidPort(port: number): boolean { - return Number.isInteger(port) && port > 0 && port <= 65535; -} - -function isValidKeyspace(keyspace: string): boolean { - if (!keyspace || keyspace.trim().length === 0) return false; - - const keyspaceRegex = /^[a-zA-Z][a-zA-Z0-9_]{0,47}$/; - return keyspaceRegex.test(keyspace); -} - -function isValidContactPoints(contactPoints: string[]): boolean { - if (!Array.isArray(contactPoints) || contactPoints.length === 0) return false; - - return contactPoints.every((point) => { - const trimmed = point.trim(); - return trimmed.length > 0 && isValidHost(trimmed); - }); -} - -function isValidCredentials( - username: string, - password: string, - authEnabled: boolean, -): boolean { - if (!authEnabled) return true; - - return username.trim().length > 0 && password.trim().length > 0; -} - -function isValidDatacenter(datacenter: string, authEnabled: boolean): boolean { - if (!authEnabled) return true; - - return datacenter.trim().length > 0; -} - -function validateCassandraConfig( - config: CassandraConfig, -): simpleConfigValidation { - const errors: string[] = []; - - if (!isValidHost(config.host)) { - errors.push(`Invalid host: ${config.host}`); - } - - if (!isValidPort(config.port)) { - errors.push( - `Invalid port: ${config.port}. Port must be between 1 and 65535`, - ); - } - - if (!isValidKeyspace(config.keyspace)) { - errors.push( - `Invalid keyspace: ${config.keyspace}. Must start with letter, contain only alphanumeric and underscores, max 48 chars`, - ); - } - - if (!isValidContactPoints(config.contactPoints)) { - errors.push( - `Invalid contact points: ${config.contactPoints.join(", ")}. All contact points must be valid hosts`, - ); - } - - if ( - !isValidCredentials(config.username, config.password, config.authEnabled) - ) { - errors.push( - "Invalid credentials: Username and password are required when authentication is enabled", - ); - } - - if (!isValidDatacenter(config.datacenter, config.authEnabled)) { - errors.push( - "Invalid datacenter: Datacenter is required when authentication is enabled", - ); - } - - return { - isValid: errors.length === 0, - errors, - }; -} - -const rawConfig: CassandraConfig = { - host: process.env.CASSANDRA_HOST || "localhost", - port: Number.parseInt(process.env.CASSANDRA_PORT || "9042", 10), - keyspace: process.env.CASSANDRA_KEYSPACE || "void_db", - username: process.env.CASSANDRA_USERNAME || "", - password: process.env.CASSANDRA_PASSWORD || "", - datacenter: process.env.CASSANDRA_DATACENTER || "", - contactPoints: (process.env.CASSANDRA_CONTACT_POINTS || "localhost") - .split(",") - .map((point) => point.trim()), - authEnabled: process.env.CASSANDRA_AUTH_ENABLED !== "false", -}; - -export { rawConfig as cassandraConfig, validateCassandraConfig }; diff --git a/src/environment/database/index.ts b/src/environment/database/index.ts index a0382f4..25a988d 100644 --- a/src/environment/database/index.ts +++ b/src/environment/database/index.ts @@ -1 +1,114 @@ -export * from "./cassandra"; +import type { CassandraConfig } from "#types/config"; +import type { simpleConfigValidation } from "#types/lib"; + +function isValidHost(host: string): boolean { + if (!host || host.trim().length === 0) return false; + + if (host === "localhost") return true; + + const ipv4Regex = + /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; + if (ipv4Regex.test(host)) return true; + + const hostnameRegex = + /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; + return hostnameRegex.test(host); +} + +function isValidPort(port: number): boolean { + return Number.isInteger(port) && port > 0 && port <= 65535; +} + +function isValidKeyspace(keyspace: string): boolean { + if (!keyspace || keyspace.trim().length === 0) return false; + + const keyspaceRegex = /^[a-zA-Z][a-zA-Z0-9_]{0,47}$/; + return keyspaceRegex.test(keyspace); +} + +function isValidContactPoints(contactPoints: string[]): boolean { + if (!Array.isArray(contactPoints) || contactPoints.length === 0) return false; + + return contactPoints.every((point) => { + const trimmed = point.trim(); + return trimmed.length > 0 && isValidHost(trimmed); + }); +} + +function isValidCredentials( + username: string, + password: string, + authEnabled: boolean, +): boolean { + if (!authEnabled) return true; + + return username.trim().length > 0 && password.trim().length > 0; +} + +function isValidDatacenter(datacenter: string, authEnabled: boolean): boolean { + if (!authEnabled) return true; + + return datacenter.trim().length > 0; +} + +function validateCassandraConfig( + config: CassandraConfig, +): simpleConfigValidation { + const errors: string[] = []; + + if (!isValidHost(config.host)) { + errors.push(`Invalid host: ${config.host}`); + } + + if (!isValidPort(config.port)) { + errors.push( + `Invalid port: ${config.port}. Port must be between 1 and 65535`, + ); + } + + if (!isValidKeyspace(config.keyspace)) { + errors.push( + `Invalid keyspace: ${config.keyspace}. Must start with letter, contain only alphanumeric and underscores, max 48 chars`, + ); + } + + if (!isValidContactPoints(config.contactPoints)) { + errors.push( + `Invalid contact points: ${config.contactPoints.join(", ")}. All contact points must be valid hosts`, + ); + } + + if ( + !isValidCredentials(config.username, config.password, config.authEnabled) + ) { + errors.push( + "Invalid credentials: Username and password are required when authentication is enabled", + ); + } + + if (!isValidDatacenter(config.datacenter, config.authEnabled)) { + errors.push( + "Invalid datacenter: Datacenter is required when authentication is enabled", + ); + } + + return { + isValid: errors.length === 0, + errors, + }; +} + +const rawConfig: CassandraConfig = { + host: process.env.CASSANDRA_HOST || "localhost", + port: Number.parseInt(process.env.CASSANDRA_PORT || "9042", 10), + keyspace: process.env.CASSANDRA_KEYSPACE || "void_db", + username: process.env.CASSANDRA_USERNAME || "", + password: process.env.CASSANDRA_PASSWORD || "", + datacenter: process.env.CASSANDRA_DATACENTER || "", + contactPoints: (process.env.CASSANDRA_CONTACT_POINTS || "localhost") + .split(",") + .map((point) => point.trim()), + authEnabled: process.env.CASSANDRA_AUTH_ENABLED !== "false", +}; + +export { rawConfig as cassandraConfig, validateCassandraConfig }; diff --git a/src/environment/database/migrations/up/002_create_guilds.sql b/src/environment/database/migrations/up/002_create_guilds.sql new file mode 100644 index 0000000..37a74d0 --- /dev/null +++ b/src/environment/database/migrations/up/002_create_guilds.sql @@ -0,0 +1,27 @@ +CREATE TABLE IF NOT EXISTS guilds ( + id TEXT PRIMARY KEY, + name TEXT, + description TEXT, + icon_url TEXT, + banner_url TEXT, + splash_url TEXT, + + owner_id TEXT, + + verification_level INT, + default_message_notifications INT, + + system_channel_id TEXT, + rules_channel_id TEXT, + public_updates_channel_id TEXT, + + features SET, + + preferred_locale TEXT, + + created_at TIMESTAMP, + updated_at TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS guilds_owner_idx ON guilds (owner_id); +CREATE INDEX IF NOT EXISTS guilds_created_idx ON guilds (created_at); diff --git a/src/environment/database/migrations/up/003_guilds_by_owner.sql b/src/environment/database/migrations/up/003_guilds_by_owner.sql new file mode 100644 index 0000000..9a4162b --- /dev/null +++ b/src/environment/database/migrations/up/003_guilds_by_owner.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS guilds_by_owner ( + owner_id TEXT, + guild_id TEXT, + name TEXT, + created_at TIMESTAMP, + PRIMARY KEY (owner_id, created_at, guild_id) +) WITH CLUSTERING ORDER BY (created_at DESC); diff --git a/src/lib/auth/cookies.ts b/src/lib/auth/cookies.ts index d1bd3a0..15b6782 100644 --- a/src/lib/auth/cookies.ts +++ b/src/lib/auth/cookies.ts @@ -2,9 +2,10 @@ import { environment } from "#environment/config"; import { jwt } from "#environment/jwt"; import type { CookieOptions } from "#types/config"; +import type { ExtendedRequest } from "#types/server"; class CookieService { - extractToken(request: Request): string | null { + extractToken(request: Request | ExtendedRequest): string | null { return request.headers.get("Cookie")?.match(/session=([^;]+)/)?.[1] || null; } diff --git a/src/lib/auth/session.ts b/src/lib/auth/session.ts index ebbe05c..b776ff6 100644 --- a/src/lib/auth/session.ts +++ b/src/lib/auth/session.ts @@ -1,10 +1,9 @@ -import { jwt } from "#environment/jwt"; -import { cookieService } from "#lib/auth/cookies"; -import { jwtService } from "#lib/auth/jwt"; - import { redis } from "bun"; +import { jwt } from "#environment/jwt"; +import { cookieService, jwtService } from "#lib/auth"; import type { CookieOptions, SessionData, UserSession } from "#types/config"; +import type { ExtendedRequest } from "#types/server"; class SessionManager { async createSession( @@ -26,7 +25,9 @@ class SessionManager { ); } - async getSession(request: Request): Promise { + async getSession( + request: Request | ExtendedRequest, + ): Promise { const token = cookieService.extractToken(request); if (!token) return null; @@ -53,7 +54,7 @@ class SessionManager { } async updateSession( - request: Request, + request: Request | ExtendedRequest, payload: UserSession, userAgent: string, cookieOptions?: CookieOptions, @@ -79,7 +80,7 @@ class SessionManager { } async refreshSession( - request: Request, + request: Request | ExtendedRequest, cookieOptions?: CookieOptions, ): Promise { const token = cookieService.extractToken(request); @@ -110,7 +111,7 @@ class SessionManager { return jwtService.decode(token); } - async invalidateSession(request: Request): Promise { + async invalidateSession(request: Request | ExtendedRequest): Promise { const token = cookieService.extractToken(request); if (!token) return; diff --git a/src/lib/validation/index.ts b/src/lib/validation/index.ts index 53c2574..c237180 100644 --- a/src/lib/validation/index.ts +++ b/src/lib/validation/index.ts @@ -1,7 +1,5 @@ -export * from "./name"; -export * from "./password"; -export * from "./email"; export * from "./jwt"; export * from "./url"; export * from "./general"; -export * from "./mailer"; + +export * from "./user"; diff --git a/src/lib/validation/email.ts b/src/lib/validation/user/email.ts similarity index 100% rename from src/lib/validation/email.ts rename to src/lib/validation/user/email.ts diff --git a/src/lib/validation/user/index.ts b/src/lib/validation/user/index.ts new file mode 100644 index 0000000..7a479e1 --- /dev/null +++ b/src/lib/validation/user/index.ts @@ -0,0 +1,4 @@ +export * from "./name"; +export * from "./password"; +export * from "./email"; +export * from "./mailer"; diff --git a/src/lib/validation/mailer.ts b/src/lib/validation/user/mailer.ts similarity index 95% rename from src/lib/validation/mailer.ts rename to src/lib/validation/user/mailer.ts index b3042f5..ddc46e3 100644 --- a/src/lib/validation/mailer.ts +++ b/src/lib/validation/user/mailer.ts @@ -1,5 +1,5 @@ +import { isValidHostname, isValidPort } from "../general"; import { isValidEmail } from "./email"; -import { isValidHostname, isValidPort } from "./general"; import type { MailerConfig } from "#types/config"; import type { simpleConfigValidation } from "#types/lib"; diff --git a/src/lib/validation/name.ts b/src/lib/validation/user/name.ts similarity index 90% rename from src/lib/validation/name.ts rename to src/lib/validation/user/name.ts index 91d9233..926c8ef 100644 --- a/src/lib/validation/name.ts +++ b/src/lib/validation/user/name.ts @@ -1,7 +1,8 @@ import { displayNameRestrictions, forbiddenDisplayNamePatterns, - nameRestrictions + nameRestrictions, + reservedNames, } from "#environment/constants"; import type { validationResult } from "#types/lib"; @@ -24,11 +25,19 @@ function isValidUsername(rawUsername: string): validationResult { if (!nameRestrictions.regex.test(username)) return { valid: false, error: "Username contains invalid characters" }; - if (/^[._-]|[._-]$/.test(username)) + if (/^[._-]|[._-]$/.test(username)) { return { valid: false, error: "Username can't start or end with special characters", }; + } + + if (reservedNames.includes(username.toLowerCase())) { + return { + valid: false, + error: "Username is reserved and cannot be used", + }; + } return { valid: true, username }; } diff --git a/src/lib/validation/password.ts b/src/lib/validation/user/password.ts similarity index 100% rename from src/lib/validation/password.ts rename to src/lib/validation/user/password.ts diff --git a/src/routes/guild/create.ts b/src/routes/guild/create.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/routes/user/[id].ts b/src/routes/user/[id].ts index 8c86741..ee7bdf3 100644 --- a/src/routes/user/[id].ts +++ b/src/routes/user/[id].ts @@ -20,6 +20,7 @@ async function handler(request: ExtendedRequest): Promise { try { const { id: identifier } = request.params; const { session } = request; + let userQuery: string; let queryParams: string[]; let targetUser: UserRow | null = null; diff --git a/src/routes/user/forgot/password.ts b/src/routes/user/forgot/password.ts index fe00ff1..00a6bca 100644 --- a/src/routes/user/forgot/password.ts +++ b/src/routes/user/forgot/password.ts @@ -28,12 +28,9 @@ const routeDef: RouteDef = { needsBody: "json", }; -async function handler( - _request: ExtendedRequest, - requestBody: unknown, -): Promise { +async function handler(request: ExtendedRequest): Promise { try { - const { email } = requestBody as ForgotPasswordRequest; + const { email } = request.requestBody as ForgotPasswordRequest; if (!email) { const response: BaseResponse = { diff --git a/src/routes/user/login.ts b/src/routes/user/login.ts index a3957ac..efe5cfc 100644 --- a/src/routes/user/login.ts +++ b/src/routes/user/login.ts @@ -23,12 +23,9 @@ const routeDef: RouteDef = { needsBody: "json", }; -async function handler( - request: ExtendedRequest, - requestBody: unknown, -): Promise { +async function handler(request: ExtendedRequest): Promise { try { - const { identifier, password } = requestBody as LoginRequest; + const { identifier, password } = request.requestBody as LoginRequest; const { force } = request.query; if (force !== "true" && force !== "1") { diff --git a/src/routes/user/register.ts b/src/routes/user/register.ts index b4c4edb..349fadf 100644 --- a/src/routes/user/register.ts +++ b/src/routes/user/register.ts @@ -31,13 +31,10 @@ const routeDef: RouteDef = { needsBody: "json", }; -async function handler( - _request: ExtendedRequest, - requestBody: unknown, -): Promise { +async function handler(request: ExtendedRequest): Promise { try { const { username, displayName, email, password } = - requestBody as RegisterRequest; + request.requestBody as RegisterRequest; if (!username || !email || !password) { const response: RegisterResponse = { diff --git a/src/routes/user/reset/password.ts b/src/routes/user/reset/password.ts index 962853f..5cc1408 100644 --- a/src/routes/user/reset/password.ts +++ b/src/routes/user/reset/password.ts @@ -26,16 +26,13 @@ const routeDef: RouteDef = { needsBody: "json", }; -async function handler( - _request: ExtendedRequest, - requestBody: unknown, -): Promise { +async function handler(request: ExtendedRequest): Promise { try { const { token, newPassword, logoutAllSessions = true, - } = requestBody as ResetPasswordRequest; + } = request.requestBody as ResetPasswordRequest; if (!token || !newPassword) { const response: ResetPasswordResponse = { diff --git a/src/routes/user/update/email.ts b/src/routes/user/update/email.ts index 94664b2..2d7d13d 100644 --- a/src/routes/user/update/email.ts +++ b/src/routes/user/update/email.ts @@ -32,10 +32,7 @@ const routeDef: RouteDef = { needsBody: "json", }; -async function handler( - request: ExtendedRequest, - requestBody: unknown, -): Promise { +async function handler(request: ExtendedRequest): Promise { try { const { session } = request; @@ -52,7 +49,7 @@ async function handler( return await handleEmailVerification(request, session); } - return await handleEmailChangeRequest(request, requestBody, session); + return await handleEmailChangeRequest(request, session); } catch (error) { echo.error({ message: "Email change operation failed", @@ -72,10 +69,9 @@ async function handler( async function handleEmailChangeRequest( request: ExtendedRequest, - requestBody: unknown, session: UserSession, ): Promise { - const { newEmail } = requestBody as EmailChangeRequest; + const { newEmail } = request.requestBody as EmailChangeRequest; if (!newEmail) { const response: EmailChangeResponse = { diff --git a/src/routes/user/update/info.ts b/src/routes/user/update/info.ts index 82d6bdb..a9d86e5 100644 --- a/src/routes/user/update/info.ts +++ b/src/routes/user/update/info.ts @@ -24,10 +24,7 @@ const routeDef: RouteDef = { needsBody: "json", }; -async function handler( - request: ExtendedRequest, - requestBody: unknown, -): Promise { +async function handler(request: ExtendedRequest): Promise { try { const { session } = request; @@ -40,7 +37,7 @@ async function handler( return Response.json(response, { status: httpStatus.UNAUTHORIZED }); } - const { username, displayName } = requestBody as UpdateInfoRequest; + const { username, displayName } = request.requestBody as UpdateInfoRequest; if (username === undefined && displayName === undefined) { const response: UpdateInfoResponse = { diff --git a/src/routes/user/update/password.ts b/src/routes/user/update/password.ts index a29b93c..0ff7298 100644 --- a/src/routes/user/update/password.ts +++ b/src/routes/user/update/password.ts @@ -24,10 +24,7 @@ const routeDef: RouteDef = { needsBody: "json", }; -async function handler( - request: ExtendedRequest, - requestBody: unknown, -): Promise { +async function handler(request: ExtendedRequest): Promise { try { const { session } = request; @@ -41,7 +38,7 @@ async function handler( } const { currentPassword, newPassword, logoutAllSessions } = - requestBody as UpdatePasswordRequest; + request.requestBody as UpdatePasswordRequest; if (!currentPassword || !newPassword) { const response: UpdatePasswordResponse = { diff --git a/src/server.ts b/src/server.ts index 6e3980a..084bdb0 100644 --- a/src/server.ts +++ b/src/server.ts @@ -167,7 +167,7 @@ class ServerHandler { } const match: MatchedRoute | null = this.router.match(request); - let requestBody: unknown = {}; + let requestBody: unknown = null; if (match) { const { filePath, params, query } = match; @@ -184,7 +184,7 @@ class ServerHandler { actualContentType === "application/json" ) { try { - requestBody = await request.json(); + requestBody = (await request.json()) as Record; } catch { requestBody = {}; } @@ -193,10 +193,49 @@ class ServerHandler { actualContentType === "multipart/form-data" ) { try { - requestBody = await request.formData(); + requestBody = (await request.formData()) as FormData; + } catch { + requestBody = new FormData(); + } + } else if ( + routeModule.routeDef.needsBody === "urlencoded" && + actualContentType === "application/x-www-form-urlencoded" + ) { + try { + const formData = await request.formData(); + requestBody = Object.fromEntries(formData.entries()) as Record< + string, + string + >; } catch { requestBody = {}; } + } else if ( + routeModule.routeDef.needsBody === "text" && + actualContentType?.startsWith("text/") + ) { + try { + requestBody = (await request.text()) as string; + } catch { + requestBody = ""; + } + } else if ( + routeModule.routeDef.needsBody === "raw" || + routeModule.routeDef.needsBody === "buffer" + ) { + try { + requestBody = (await request.arrayBuffer()) as ArrayBuffer; + } catch { + requestBody = new ArrayBuffer(0); + } + } else if (routeModule.routeDef.needsBody === "blob") { + try { + requestBody = (await request.blob()) as Blob; + } catch { + requestBody = new Blob(); + } + } else if (routeModule.routeDef.needsBody) { + requestBody = null; } if ( @@ -249,15 +288,10 @@ class ServerHandler { } else { extendedRequest.params = params; extendedRequest.query = query; - + extendedRequest.requestBody = requestBody; extendedRequest.session = await sessionManager.getSession(request); - response = await routeModule.handler( - extendedRequest, - requestBody, - server, - ); - + response = await routeModule.handler(extendedRequest, server); if (routeModule.routeDef.returns !== "*/*") { response.headers.set( "Content-Type", diff --git a/types/server/routes.ts b/types/server/routes.ts index d325488..714b3db 100644 --- a/types/server/routes.ts +++ b/types/server/routes.ts @@ -5,13 +5,19 @@ type RouteDef = { method: string | string[]; accepts: string | null | string[]; returns: string; - needsBody?: "multipart" | "json"; + needsBody?: + | "multipart" + | "json" + | "urlencoded" + | "text" + | "raw" + | "buffer" + | "blob"; }; type RouteModule = { handler: ( request: Request | ExtendedRequest, - requestBody: unknown, server: Server, ) => Promise | Response; routeDef: RouteDef; diff --git a/types/server/server.ts b/types/server/server.ts index 9845ed8..f4bb997 100644 --- a/types/server/server.ts +++ b/types/server/server.ts @@ -1,4 +1,4 @@ -import type { UserSession } from "#types/config"; +import type { UserSession } from "../config/auth"; type Query = Record; type Params = Record; @@ -7,6 +7,7 @@ interface ExtendedRequest extends Request { startPerf: number; query: Query; params: Params; + requestBody: unknown; session?: UserSession | null; }