add guild sql, move things around for req body
All checks were successful
Code quality checks / biome (push) Successful in 9s

This commit is contained in:
creations 2025-06-18 17:45:30 -04:00
parent 33a602cdd0
commit ca0410f7fb
Signed by: creations
GPG key ID: 8F553AA4320FC711
30 changed files with 332 additions and 183 deletions

View file

@ -2,7 +2,7 @@ import { echo } from "@atums/echo";
import { validateJWTConfig, validateMailerConfig } from "#lib/validation"; import { validateJWTConfig, validateMailerConfig } from "#lib/validation";
import { isValidUrl } from "#lib/validation/url"; import { isValidUrl } from "#lib/validation/url";
import { requiredVariables } from "./constants"; import { requiredVariables } from "./constants";
import { cassandraConfig, validateCassandraConfig } from "./database/cassandra"; import { cassandraConfig, validateCassandraConfig } from "./database";
import { jwt } from "./jwt"; import { jwt } from "./jwt";
import { mailerConfig } from "./mailer"; import { mailerConfig } from "./mailer";

View file

@ -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 };

View file

@ -0,0 +1,7 @@
const GUILD_FEATURES = {
COMMUNITY: "COMMUNITY",
VERIFIED: "VERIFIED",
// idrk know more rn
};
export { GUILD_FEATURES };

View file

@ -0,0 +1 @@
export * from "./defaults";

View file

@ -34,3 +34,4 @@ export * from "./mailer";
export * from "./user"; export * from "./user";
export * from "./cache"; export * from "./cache";
export * from "./http"; export * from "./http";
export * from "./guild";

View file

@ -5,6 +5,62 @@ const nameRestrictions: genericValidation = {
regex: /^[\p{L}\p{N}._-]+$/u, 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 = { const displayNameRestrictions: genericValidation = {
length: { min: 1, max: 32 }, length: { min: 1, max: 32 },
regex: /^[\p{L}\p{N}\p{M}\p{S}\p{P}\s]+$/u, regex: /^[\p{L}\p{N}\p{M}\p{S}\p{P}\s]+$/u,
@ -14,7 +70,6 @@ const forbiddenDisplayNamePatterns = [
/[\r\n\t]/, /[\r\n\t]/,
/\s{3,}/, /\s{3,}/,
/^\s|\s$/, /^\s|\s$/,
/@everyone|@here/i,
/\p{Cf}/u, /\p{Cf}/u,
/\p{Cc}/u, /\p{Cc}/u,
]; ];
@ -28,10 +83,17 @@ const emailRestrictions: { regex: RegExp } = {
regex: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, 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 { export {
nameRestrictions, nameRestrictions,
displayNameRestrictions, displayNameRestrictions,
forbiddenDisplayNamePatterns, forbiddenDisplayNamePatterns,
passwordRestrictions, passwordRestrictions,
emailRestrictions, emailRestrictions,
reservedNames,
avatarRestrictions,
}; };

View file

@ -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 };

View file

@ -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 };

View file

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

View file

@ -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);

View file

@ -2,9 +2,10 @@ import { environment } from "#environment/config";
import { jwt } from "#environment/jwt"; import { jwt } from "#environment/jwt";
import type { CookieOptions } from "#types/config"; import type { CookieOptions } from "#types/config";
import type { ExtendedRequest } from "#types/server";
class CookieService { class CookieService {
extractToken(request: Request): string | null { extractToken(request: Request | ExtendedRequest): string | null {
return request.headers.get("Cookie")?.match(/session=([^;]+)/)?.[1] || null; return request.headers.get("Cookie")?.match(/session=([^;]+)/)?.[1] || null;
} }

View file

@ -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 { redis } from "bun";
import { jwt } from "#environment/jwt";
import { cookieService, jwtService } from "#lib/auth";
import type { CookieOptions, SessionData, UserSession } from "#types/config"; import type { CookieOptions, SessionData, UserSession } from "#types/config";
import type { ExtendedRequest } from "#types/server";
class SessionManager { class SessionManager {
async createSession( async createSession(
@ -26,7 +25,9 @@ class SessionManager {
); );
} }
async getSession(request: Request): Promise<UserSession | null> { async getSession(
request: Request | ExtendedRequest,
): Promise<UserSession | null> {
const token = cookieService.extractToken(request); const token = cookieService.extractToken(request);
if (!token) return null; if (!token) return null;
@ -53,7 +54,7 @@ class SessionManager {
} }
async updateSession( async updateSession(
request: Request, request: Request | ExtendedRequest,
payload: UserSession, payload: UserSession,
userAgent: string, userAgent: string,
cookieOptions?: CookieOptions, cookieOptions?: CookieOptions,
@ -79,7 +80,7 @@ class SessionManager {
} }
async refreshSession( async refreshSession(
request: Request, request: Request | ExtendedRequest,
cookieOptions?: CookieOptions, cookieOptions?: CookieOptions,
): Promise<string | null> { ): Promise<string | null> {
const token = cookieService.extractToken(request); const token = cookieService.extractToken(request);
@ -110,7 +111,7 @@ class SessionManager {
return jwtService.decode(token); return jwtService.decode(token);
} }
async invalidateSession(request: Request): Promise<void> { async invalidateSession(request: Request | ExtendedRequest): Promise<void> {
const token = cookieService.extractToken(request); const token = cookieService.extractToken(request);
if (!token) return; if (!token) return;

View file

@ -1,7 +1,5 @@
export * from "./name";
export * from "./password";
export * from "./email";
export * from "./jwt"; export * from "./jwt";
export * from "./url"; export * from "./url";
export * from "./general"; export * from "./general";
export * from "./mailer";
export * from "./user";

View file

@ -0,0 +1,4 @@
export * from "./name";
export * from "./password";
export * from "./email";
export * from "./mailer";

View file

@ -1,5 +1,5 @@
import { isValidHostname, isValidPort } from "../general";
import { isValidEmail } from "./email"; import { isValidEmail } from "./email";
import { isValidHostname, isValidPort } from "./general";
import type { MailerConfig } from "#types/config"; import type { MailerConfig } from "#types/config";
import type { simpleConfigValidation } from "#types/lib"; import type { simpleConfigValidation } from "#types/lib";

View file

@ -1,7 +1,8 @@
import { import {
displayNameRestrictions, displayNameRestrictions,
forbiddenDisplayNamePatterns, forbiddenDisplayNamePatterns,
nameRestrictions nameRestrictions,
reservedNames,
} from "#environment/constants"; } from "#environment/constants";
import type { validationResult } from "#types/lib"; import type { validationResult } from "#types/lib";
@ -24,11 +25,19 @@ function isValidUsername(rawUsername: string): validationResult {
if (!nameRestrictions.regex.test(username)) if (!nameRestrictions.regex.test(username))
return { valid: false, error: "Username contains invalid characters" }; return { valid: false, error: "Username contains invalid characters" };
if (/^[._-]|[._-]$/.test(username)) if (/^[._-]|[._-]$/.test(username)) {
return { return {
valid: false, valid: false,
error: "Username can't start or end with special characters", 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 }; return { valid: true, username };
} }

View file

View file

@ -20,6 +20,7 @@ async function handler(request: ExtendedRequest): Promise<Response> {
try { try {
const { id: identifier } = request.params; const { id: identifier } = request.params;
const { session } = request; const { session } = request;
let userQuery: string; let userQuery: string;
let queryParams: string[]; let queryParams: string[];
let targetUser: UserRow | null = null; let targetUser: UserRow | null = null;

View file

@ -28,12 +28,9 @@ const routeDef: RouteDef = {
needsBody: "json", needsBody: "json",
}; };
async function handler( async function handler(request: ExtendedRequest): Promise<Response> {
_request: ExtendedRequest,
requestBody: unknown,
): Promise<Response> {
try { try {
const { email } = requestBody as ForgotPasswordRequest; const { email } = request.requestBody as ForgotPasswordRequest;
if (!email) { if (!email) {
const response: BaseResponse = { const response: BaseResponse = {

View file

@ -23,12 +23,9 @@ const routeDef: RouteDef = {
needsBody: "json", needsBody: "json",
}; };
async function handler( async function handler(request: ExtendedRequest): Promise<Response> {
request: ExtendedRequest,
requestBody: unknown,
): Promise<Response> {
try { try {
const { identifier, password } = requestBody as LoginRequest; const { identifier, password } = request.requestBody as LoginRequest;
const { force } = request.query; const { force } = request.query;
if (force !== "true" && force !== "1") { if (force !== "true" && force !== "1") {

View file

@ -31,13 +31,10 @@ const routeDef: RouteDef = {
needsBody: "json", needsBody: "json",
}; };
async function handler( async function handler(request: ExtendedRequest): Promise<Response> {
_request: ExtendedRequest,
requestBody: unknown,
): Promise<Response> {
try { try {
const { username, displayName, email, password } = const { username, displayName, email, password } =
requestBody as RegisterRequest; request.requestBody as RegisterRequest;
if (!username || !email || !password) { if (!username || !email || !password) {
const response: RegisterResponse = { const response: RegisterResponse = {

View file

@ -26,16 +26,13 @@ const routeDef: RouteDef = {
needsBody: "json", needsBody: "json",
}; };
async function handler( async function handler(request: ExtendedRequest): Promise<Response> {
_request: ExtendedRequest,
requestBody: unknown,
): Promise<Response> {
try { try {
const { const {
token, token,
newPassword, newPassword,
logoutAllSessions = true, logoutAllSessions = true,
} = requestBody as ResetPasswordRequest; } = request.requestBody as ResetPasswordRequest;
if (!token || !newPassword) { if (!token || !newPassword) {
const response: ResetPasswordResponse = { const response: ResetPasswordResponse = {

View file

@ -32,10 +32,7 @@ const routeDef: RouteDef = {
needsBody: "json", needsBody: "json",
}; };
async function handler( async function handler(request: ExtendedRequest): Promise<Response> {
request: ExtendedRequest,
requestBody: unknown,
): Promise<Response> {
try { try {
const { session } = request; const { session } = request;
@ -52,7 +49,7 @@ async function handler(
return await handleEmailVerification(request, session); return await handleEmailVerification(request, session);
} }
return await handleEmailChangeRequest(request, requestBody, session); return await handleEmailChangeRequest(request, session);
} catch (error) { } catch (error) {
echo.error({ echo.error({
message: "Email change operation failed", message: "Email change operation failed",
@ -72,10 +69,9 @@ async function handler(
async function handleEmailChangeRequest( async function handleEmailChangeRequest(
request: ExtendedRequest, request: ExtendedRequest,
requestBody: unknown,
session: UserSession, session: UserSession,
): Promise<Response> { ): Promise<Response> {
const { newEmail } = requestBody as EmailChangeRequest; const { newEmail } = request.requestBody as EmailChangeRequest;
if (!newEmail) { if (!newEmail) {
const response: EmailChangeResponse = { const response: EmailChangeResponse = {

View file

@ -24,10 +24,7 @@ const routeDef: RouteDef = {
needsBody: "json", needsBody: "json",
}; };
async function handler( async function handler(request: ExtendedRequest): Promise<Response> {
request: ExtendedRequest,
requestBody: unknown,
): Promise<Response> {
try { try {
const { session } = request; const { session } = request;
@ -40,7 +37,7 @@ async function handler(
return Response.json(response, { status: httpStatus.UNAUTHORIZED }); 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) { if (username === undefined && displayName === undefined) {
const response: UpdateInfoResponse = { const response: UpdateInfoResponse = {

View file

@ -24,10 +24,7 @@ const routeDef: RouteDef = {
needsBody: "json", needsBody: "json",
}; };
async function handler( async function handler(request: ExtendedRequest): Promise<Response> {
request: ExtendedRequest,
requestBody: unknown,
): Promise<Response> {
try { try {
const { session } = request; const { session } = request;
@ -41,7 +38,7 @@ async function handler(
} }
const { currentPassword, newPassword, logoutAllSessions } = const { currentPassword, newPassword, logoutAllSessions } =
requestBody as UpdatePasswordRequest; request.requestBody as UpdatePasswordRequest;
if (!currentPassword || !newPassword) { if (!currentPassword || !newPassword) {
const response: UpdatePasswordResponse = { const response: UpdatePasswordResponse = {

View file

@ -167,7 +167,7 @@ class ServerHandler {
} }
const match: MatchedRoute | null = this.router.match(request); const match: MatchedRoute | null = this.router.match(request);
let requestBody: unknown = {}; let requestBody: unknown = null;
if (match) { if (match) {
const { filePath, params, query } = match; const { filePath, params, query } = match;
@ -184,7 +184,7 @@ class ServerHandler {
actualContentType === "application/json" actualContentType === "application/json"
) { ) {
try { try {
requestBody = await request.json(); requestBody = (await request.json()) as Record<string, unknown>;
} catch { } catch {
requestBody = {}; requestBody = {};
} }
@ -193,10 +193,49 @@ class ServerHandler {
actualContentType === "multipart/form-data" actualContentType === "multipart/form-data"
) { ) {
try { 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 { } catch {
requestBody = {}; 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 ( if (
@ -249,15 +288,10 @@ class ServerHandler {
} else { } else {
extendedRequest.params = params; extendedRequest.params = params;
extendedRequest.query = query; extendedRequest.query = query;
extendedRequest.requestBody = requestBody;
extendedRequest.session = await sessionManager.getSession(request); extendedRequest.session = await sessionManager.getSession(request);
response = await routeModule.handler( response = await routeModule.handler(extendedRequest, server);
extendedRequest,
requestBody,
server,
);
if (routeModule.routeDef.returns !== "*/*") { if (routeModule.routeDef.returns !== "*/*") {
response.headers.set( response.headers.set(
"Content-Type", "Content-Type",

View file

@ -5,13 +5,19 @@ type RouteDef = {
method: string | string[]; method: string | string[];
accepts: string | null | string[]; accepts: string | null | string[];
returns: string; returns: string;
needsBody?: "multipart" | "json"; needsBody?:
| "multipart"
| "json"
| "urlencoded"
| "text"
| "raw"
| "buffer"
| "blob";
}; };
type RouteModule = { type RouteModule = {
handler: ( handler: (
request: Request | ExtendedRequest, request: Request | ExtendedRequest,
requestBody: unknown,
server: Server, server: Server,
) => Promise<Response> | Response; ) => Promise<Response> | Response;
routeDef: RouteDef; routeDef: RouteDef;

View file

@ -1,4 +1,4 @@
import type { UserSession } from "#types/config"; import type { UserSession } from "../config/auth";
type Query = Record<string, string>; type Query = Record<string, string>;
type Params = Record<string, string>; type Params = Record<string, string>;
@ -7,6 +7,7 @@ interface ExtendedRequest extends Request {
startPerf: number; startPerf: number;
query: Query; query: Query;
params: Params; params: Params;
requestBody: unknown;
session?: UserSession | null; session?: UserSession | null;
} }