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 { 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";
|
||||||
|
|
||||||
|
|
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 "./user";
|
||||||
export * from "./cache";
|
export * from "./cache";
|
||||||
export * from "./http";
|
export * from "./http";
|
||||||
|
export * from "./guild";
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 { 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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";
|
||||||
|
|
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 { 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";
|
|
@ -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 };
|
||||||
}
|
}
|
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 {
|
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;
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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") {
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue