- better reading of avalable routes
All checks were successful
Code quality checks / biome (push) Successful in 10s

- add name validation for channel names and catagories
- add create, delete, move for channels
This commit is contained in:
creations 2025-05-11 10:51:48 -04:00
parent 0cb7ebb245
commit 93ed37b3e9
Signed by: creations
GPG key ID: 8F553AA4320FC711
10 changed files with 559 additions and 6 deletions

View file

@ -0,0 +1,29 @@
import { cassandra } from "@lib/cassandra";
async function createTable() {
const client = cassandra.getClient();
await client.execute(`
CREATE TABLE IF NOT EXISTS categories (
id TEXT PRIMARY KEY,
guild_id TEXT,
name TEXT,
position INT,
created_at TIMESTAMP,
updated_at TIMESTAMP
);
`);
await client.execute(`
CREATE INDEX IF NOT EXISTS categories_guild_id_idx ON categories (guild_id);
`);
}
async function dropTable() {
const client = cassandra.getClient();
await client.execute("DROP TABLE IF EXISTS categories;");
await client.execute("DROP INDEX IF EXISTS categories_guild_id_idx;");
}
export { createTable, dropTable };

View file

@ -0,0 +1,37 @@
import { cassandra } from "@lib/cassandra";
async function createTable() {
const client = cassandra.getClient();
await client.execute(`
CREATE TABLE IF NOT EXISTS channels (
id TEXT PRIMARY KEY,
guild_id TEXT,
name TEXT,
type TEXT,
topic TEXT,
position INT,
category_id TEXT,
created_at TIMESTAMP,
updated_at TIMESTAMP
);
`);
await client.execute(`
CREATE INDEX IF NOT EXISTS channels_guild_id_idx ON channels (guild_id);
`);
await client.execute(`
CREATE INDEX IF NOT EXISTS channels_category_id_idx ON channels (category_id);
`);
}
async function dropTable() {
const client = cassandra.getClient();
await client.execute("DROP TABLE IF EXISTS channels;");
await client.execute("DROP INDEX IF EXISTS channels_guild_id_idx;");
await client.execute("DROP INDEX IF EXISTS channels_category_id_idx;");
}
export { createTable, dropTable };

View file

@ -17,7 +17,6 @@ async function main(): Promise<void> {
}
await cassandra.connect();
serverHandler.initialize();
}

View file

@ -21,6 +21,11 @@ const pika = new Pika([
prefix: "ch",
description: "Channel ID",
},
"category",
{
prefix: "cat",
description: "Category ID",
},
"role",
{
prefix: "role",

View file

@ -26,4 +26,69 @@ function isValidGuildName(rawName: string): validationResult {
return { valid: true, name };
}
export { guildNameRestrictions, isValidGuildName };
const channelNameRestrictions: genericValidation = {
length: { min: 1, max: 100 },
regex: /^[\p{L}\p{N}\p{Emoji}_\-.]+$/u, // emojis + underscore, dot, dash ?
};
function isValidChannelName(rawName: string): validationResult {
const name = rawName.trim().normalize("NFC");
if (!name) return { valid: false, error: "Channel name is required" };
if (name.length < channelNameRestrictions.length.min)
return { valid: false, error: "Channel name is too short" };
if (name.length > channelNameRestrictions.length.max)
return { valid: false, error: "Channel name is too long" };
if (/\s/.test(name))
return { valid: false, error: "Channel name cannot contain spaces" };
if (!channelNameRestrictions.regex.test(name))
return {
valid: false,
error: "Channel name contains invalid characters",
};
if (/^[._-]|[._-]$/.test(name))
return {
valid: false,
error: "Channel name can't start or end with special characters",
};
return { valid: true, name };
}
const categoryNameRestrictions: genericValidation = {
length: { min: 1, max: 100 },
regex: /^[\p{L}\p{N} ._-]+$/u,
};
function isValidCategoryName(rawName: string): validationResult {
const name = rawName.trim().normalize("NFC");
if (!name) return { valid: false, error: "Category name is required" };
if (name.length < categoryNameRestrictions.length.min)
return { valid: false, error: "Category name is too short" };
if (name.length > categoryNameRestrictions.length.max)
return { valid: false, error: "Category name is too long" };
if (!categoryNameRestrictions.regex.test(name))
return {
valid: false,
error: "Category name contains invalid characters",
};
if (/^[._ -]|[._ -]$/.test(name))
return {
valid: false,
error: "Category name can't start or end with special characters",
};
return { valid: true, name };
}
export { isValidGuildName, isValidChannelName, isValidCategoryName };

View file

@ -1,3 +1,4 @@
export * from "@lib/validators/name";
export * from "@lib/validators/password";
export * from "@lib/validators/email";
export * from "@lib/validators/guild";

View file

@ -0,0 +1,187 @@
import { logger } from "@creations.works/logger";
import { cassandra } from "@lib/cassandra";
import { jsonResponse } from "@lib/http";
import { pika } from "@lib/pika";
import { isValidChannelName } from "@lib/validators";
import type { Client } from "cassandra-driver";
const routeDef: RouteDef = {
method: "POST",
accepts: "application/json",
returns: "application/json;charset=utf-8",
needsBody: "json",
};
async function handler(
request: ExtendedRequest,
requestBody: unknown,
): Promise<Response> {
const user: UserSession | null = request.session;
if (!user) {
return jsonResponse(401, {
message: "Unauthorized",
error: "You must be logged in to create a channel",
});
}
const { name, type, topic, position, category_id } = requestBody as Record<
string,
unknown
>;
const { id: guildId } = request.params;
if (!guildId || typeof guildId !== "string") {
return jsonResponse(400, {
message: "Missing or invalid guild ID",
});
}
const errors: string[] = [];
if (typeof name !== "string") {
errors.push("Channel name is required");
} else {
const result = isValidChannelName(name);
if (!result.valid) errors.push(result.error ?? "Invalid channel name");
}
if (type !== "text" && type !== "voice") {
errors.push("Invalid or missing type (must be 'text' or 'voice')");
}
if (
topic !== undefined &&
(typeof topic !== "string" || topic.length > 1024)
) {
errors.push("Invalid topic");
}
if (
position !== undefined &&
(typeof position !== "number" || position < 0)
) {
errors.push("Invalid position");
}
if (category_id !== undefined) {
if (typeof category_id !== "string") {
errors.push("Invalid category_id");
} else {
const result = pika.validate(category_id, "category");
if (!result) errors.push("Invalid category name format");
}
}
if (errors.length > 0) {
return jsonResponse(400, {
message: "Validation failed",
error: errors.join("; "),
});
}
const now = new Date();
const channelId = pika.gen("channel");
const client: Client = cassandra.getClient();
let finalPosition = typeof position === "number" ? position : 0;
if (position === undefined) {
try {
const result = await client.execute(
"SELECT position FROM channels WHERE guild_id = ?",
[guildId],
{ prepare: true },
);
let maxPos = -1;
for (const row of result.rows) {
if (typeof row.position === "number" && row.position > maxPos) {
maxPos = row.position;
}
}
finalPosition = maxPos + 1;
} catch (err) {
logger.error(["Failed to fetch existing channels:", err as Error]);
return jsonResponse(500, {
message: "Internal server error",
error: "Could not determine channel position",
});
}
}
if (category_id !== undefined) {
try {
const categoryCheck = await client.execute(
"SELECT id FROM categories WHERE id = ? AND guild_id = ?",
[category_id, guildId],
{ prepare: true },
);
if (categoryCheck.rowLength === 0) {
return jsonResponse(400, {
message: "Validation failed",
error: "Provided category does not exist in this guild",
});
}
} catch (err) {
logger.error(["Failed to verify category:", err as Error]);
return jsonResponse(500, {
message: "Internal server error",
error: "Failed to verify category existence",
});
}
}
// TODO: Check if the user has permission to create a channel in this guild
try {
await client.execute(
`INSERT INTO channels (
id, guild_id, name, type, topic, position, category_id, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
channelId,
guildId,
name,
type,
topic ?? null,
finalPosition,
category_id ?? null,
now,
now,
],
{ prepare: true },
);
logger.custom(
"[CHANNEL CREATE]",
`(${channelId})`,
`${name} in guild ${guildId} by ${user.id}`,
"36",
);
return jsonResponse(201, {
message: "Channel created successfully",
data: {
id: channelId,
guildId,
name,
type,
topic: topic ?? null,
position: finalPosition,
category_id: category_id ?? null,
created_at: now,
updated_at: now,
},
});
} catch (error) {
logger.error(["Failed to create channel:", error as Error]);
return jsonResponse(500, {
message: "Internal server error",
error: "Failed to create channel",
});
}
}
export { handler, routeDef };

View file

@ -0,0 +1,90 @@
import { logger } from "@creations.works/logger";
import { cassandra } from "@lib/cassandra";
import { jsonResponse } from "@lib/http";
import { pika } from "@lib/pika";
import type { Client } from "cassandra-driver";
const routeDef: RouteDef = {
method: "DELETE",
accepts: "application/json",
returns: "application/json;charset=utf-8",
needsBody: "json",
};
async function handler(
request: ExtendedRequest,
requestBody: unknown,
): Promise<Response> {
const user: UserSession | null = request.session;
if (!user) {
return jsonResponse(401, {
message: "Unauthorized",
error: "You must be logged in to delete a channel",
});
}
const { id: guildId } = request.params;
if (!guildId || typeof guildId !== "string") {
return jsonResponse(400, {
message: "Missing or invalid guild ID",
});
}
const { id: channel_id } = requestBody as Record<string, unknown>;
if (typeof channel_id !== "string" || !pika.validate(channel_id, "channel")) {
return jsonResponse(400, {
message: "Validation failed",
error: "Invalid or missing channel_id",
});
}
const client: Client = cassandra.getClient();
try {
const result = await client.execute(
"SELECT id, guild_id FROM channels WHERE id = ?",
[channel_id],
{ prepare: true },
);
if (result.rowLength === 0) {
return jsonResponse(404, {
message: "Channel not found",
error: "The specified channel does not exist",
});
}
const channel = result.first();
if (channel.guild_id !== guildId) {
return jsonResponse(403, {
message: "Forbidden",
error: "Channel does not belong to this guild",
});
}
await client.execute("DELETE FROM channels WHERE id = ?", [channel_id], {
prepare: true,
});
logger.custom(
"[CHANNEL DELETE]",
`(${channel_id})`,
`from guild ${guildId} by ${user.id}`,
"31",
);
return jsonResponse(200, {
message: "Channel deleted successfully",
});
} catch (error) {
logger.error(["Failed to delete channel:", error as Error]);
return jsonResponse(500, {
message: "Internal server error",
error: "Failed to delete channel",
});
}
}
export { handler, routeDef };

View file

@ -0,0 +1,131 @@
import { logger } from "@creations.works/logger";
import { cassandra } from "@lib/cassandra";
import { jsonResponse } from "@lib/http";
import { pika } from "@lib/pika";
import type { Client } from "cassandra-driver";
const routeDef: RouteDef = {
method: "POST",
accepts: "application/json",
returns: "application/json;charset=utf-8",
needsBody: "json",
};
async function handler(
request: ExtendedRequest,
requestBody: unknown,
): Promise<Response> {
const user: UserSession | null = request.session;
if (!user) {
return jsonResponse(401, {
message: "Unauthorized",
error: "You must be logged in to move a channel",
});
}
const { id: guildId } = request.params;
if (!guildId || typeof guildId !== "string") {
return jsonResponse(400, {
message: "Missing or invalid guild ID",
});
}
const { channel_id, position, category_id } = requestBody as Record<
string,
unknown
>;
const errors: string[] = [];
if (typeof channel_id !== "string" || !pika.validate(channel_id, "channel")) {
errors.push("Invalid or missing channel_id");
}
if (typeof position !== "number" || position < 0) {
errors.push("Invalid or missing position");
}
if (category_id !== undefined) {
if (
typeof category_id !== "string" ||
!pika.validate(category_id, "category")
) {
errors.push("Invalid category_id");
}
}
if (errors.length > 0) {
return jsonResponse(400, {
message: "Validation failed",
error: errors.join("; "),
});
}
const client: Client = cassandra.getClient();
// TODO: check if user has permission to move the channel
try {
const existing = await client.execute(
"SELECT id FROM channels WHERE id = ? AND guild_id = ?",
[channel_id, guildId],
{ prepare: true },
);
if (existing.rowLength === 0) {
return jsonResponse(404, {
message: "Channel not found",
error: "The specified channel does not exist in this guild",
});
}
if (category_id !== undefined) {
const catCheck = await client.execute(
"SELECT id FROM categories WHERE id = ? AND guild_id = ?",
[category_id, guildId],
{ prepare: true },
);
if (catCheck.rowLength === 0) {
return jsonResponse(400, {
message: "Validation failed",
error: "Provided category does not exist in this guild",
});
}
}
const now = new Date();
await client.execute(
"UPDATE channels SET position = ?, category_id = ?, updated_at = ? WHERE id = ? AND guild_id = ?",
[position, category_id ?? null, now, channel_id, guildId],
{ prepare: true },
);
logger.custom(
"[CHANNEL MOVE]",
`(${channel_id})`,
`moved to pos ${position}, cat ${category_id ?? "null"} in guild ${guildId} by ${user.id}`,
"33",
);
return jsonResponse(200, {
message: "Channel moved successfully",
data: {
id: channel_id,
guildId,
position,
category_id: category_id ?? null,
updated_at: now,
},
});
} catch (error) {
logger.error(["Failed to move channel:", error as Error]);
return jsonResponse(500, {
message: "Internal server error",
error: "Failed to move channel",
});
}
}
export { handler, routeDef };

View file

@ -51,11 +51,20 @@ class ServerHandler {
const sortedRoutes: [string, string][] = Object.entries(
this.router.routes,
).sort(([pathA]: [string, string], [pathB]: [string, string]) =>
pathA.localeCompare(pathB),
);
).sort(([pathA], [pathB]) => pathA.localeCompare(pathB));
let lastCategory: string | null = null;
for (const [path, filePath] of sortedRoutes) {
const parts = path.split("/").filter(Boolean);
const category = parts.length === 0 ? "" : parts[0];
if (category !== lastCategory) {
if (lastCategory !== null) logger.space();
logger.info(` ${category}/`);
lastCategory = category;
}
logger.info(` Route: ${path}, File: ${filePath}`);
}
}