add guild sql, move things around for req body
All checks were successful
Code quality checks / biome (push) Successful in 9s
All checks were successful
Code quality checks / biome (push) Successful in 9s
This commit is contained in:
parent
33a602cdd0
commit
ca0410f7fb
30 changed files with 332 additions and 183 deletions
|
@ -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";
|
||||
|
||||
|
|
12
src/environment/constants/guild/defaults.ts
Normal file
12
src/environment/constants/guild/defaults.ts
Normal 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 };
|
7
src/environment/constants/guild/features.ts
Normal file
7
src/environment/constants/guild/features.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
const GUILD_FEATURES = {
|
||||
COMMUNITY: "COMMUNITY",
|
||||
VERIFIED: "VERIFIED",
|
||||
// idrk know more rn
|
||||
};
|
||||
|
||||
export { GUILD_FEATURES };
|
1
src/environment/constants/guild/index.ts
Normal file
1
src/environment/constants/guild/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from "./defaults";
|
|
@ -34,3 +34,4 @@ export * from "./mailer";
|
|||
export * from "./user";
|
||||
export * from "./cache";
|
||||
export * from "./http";
|
||||
export * from "./guild";
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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 };
|
|
@ -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 };
|
||||
|
|
27
src/environment/database/migrations/up/002_create_guilds.sql
Normal file
27
src/environment/database/migrations/up/002_create_guilds.sql
Normal 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);
|
|
@ -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);
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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";
|
||||
|
|
4
src/lib/validation/user/index.ts
Normal file
4
src/lib/validation/user/index.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export * from "./name";
|
||||
export * from "./password";
|
||||
export * from "./email";
|
||||
export * from "./mailer";
|
|
@ -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";
|
|
@ -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 };
|
||||
}
|
0
src/routes/guild/create.ts
Normal file
0
src/routes/guild/create.ts
Normal 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;
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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") {
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue