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 { 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";

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 "./cache";
export * from "./http";
export * from "./guild";

View file

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

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

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 { 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<UserSession | null> {
async getSession(
request: Request | ExtendedRequest,
): Promise<UserSession | null> {
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<string | null> {
const token = cookieService.extractToken(request);
@ -110,7 +111,7 @@ class SessionManager {
return jwtService.decode(token);
}
async invalidateSession(request: Request): Promise<void> {
async invalidateSession(request: Request | ExtendedRequest): Promise<void> {
const token = cookieService.extractToken(request);
if (!token) return;

View file

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

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 { isValidHostname, isValidPort } from "./general";
import type { MailerConfig } from "#types/config";
import type { simpleConfigValidation } from "#types/lib";

View file

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

View file

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -32,10 +32,7 @@ const routeDef: RouteDef = {
needsBody: "json",
};
async function handler(
request: ExtendedRequest,
requestBody: unknown,
): Promise<Response> {
async function handler(request: ExtendedRequest): Promise<Response> {
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<Response> {
const { newEmail } = requestBody as EmailChangeRequest;
const { newEmail } = request.requestBody as EmailChangeRequest;
if (!newEmail) {
const response: EmailChangeResponse = {

View file

@ -24,10 +24,7 @@ const routeDef: RouteDef = {
needsBody: "json",
};
async function handler(
request: ExtendedRequest,
requestBody: unknown,
): Promise<Response> {
async function handler(request: ExtendedRequest): Promise<Response> {
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 = {

View file

@ -24,10 +24,7 @@ const routeDef: RouteDef = {
needsBody: "json",
};
async function handler(
request: ExtendedRequest,
requestBody: unknown,
): Promise<Response> {
async function handler(request: ExtendedRequest): Promise<Response> {
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 = {

View file

@ -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<string, unknown>;
} 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",

View file

@ -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> | Response;
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 Params = Record<string, string>;
@ -7,6 +7,7 @@ interface ExtendedRequest extends Request {
startPerf: number;
query: Query;
params: Params;
requestBody: unknown;
session?: UserSession | null;
}