rename some functions, change name to username ( username table), fix exports, add validations folder
All checks were successful
Code quality checks / biome (push) Successful in 9s

This commit is contained in:
creations 2025-05-02 17:40:37 -04:00
parent 61db491848
commit f93cef442a
Signed by: creations
GPG key ID: 8F553AA4320FC711
15 changed files with 280 additions and 72 deletions

View file

@ -1,17 +1,17 @@
import { logger } from "@creations.works/logger";
export const environment: Environment = {
const environment: Environment = {
port: Number.parseInt(process.env.PORT || "", 10),
host: process.env.HOST || "0.0.0.0",
development:
process.env.NODE_ENV === "development" || process.argv.includes("--dev"),
};
export const redisTtl: number = process.env.REDIS_TTL
const redisTtl: number = process.env.REDIS_TTL
? Number.parseInt(process.env.REDIS_TTL, 10)
: 60 * 60 * 1; // 1 hour
export const cassandra: CassandraConfig = {
const cassandra: CassandraConfig = {
host: process.env.CASSANDRA_HOST || "localhost",
port: Number.parseInt(process.env.CASSANDRA_PORT || "9042", 10),
keyspace: process.env.CASSANDRA_KEYSPACE || "void_db",
@ -24,7 +24,7 @@ export const cassandra: CassandraConfig = {
authEnabled: process.env.CASSANDRA_AUTH_ENABLED === "false",
};
export function verifyRequiredVariables(): void {
function verifyRequiredVariables(): void {
const requiredVariables = [
"HOST",
"PORT",
@ -51,3 +51,5 @@ export function verifyRequiredVariables(): void {
process.exit(1);
}
}
export { environment, cassandra, redisTtl, verifyRequiredVariables };

View file

@ -6,13 +6,13 @@ import {
verifyRequiredVariables,
} from "@config/environment";
import { logger } from "@creations.works/logger";
import { CassandraService } from "@lib/cassandra";
import { cassandra } from "@lib/cassandra";
async function setup(): Promise<void> {
verifyRequiredVariables();
await CassandraService.connect({ withKeyspace: false });
await cassandra.connect({ withKeyspace: false });
const client = CassandraService.getClient();
const client = cassandra.getClient();
const keyspace = cassandraConfig.keyspace;
if (!keyspace) {
@ -60,7 +60,7 @@ setup()
process.exit(1);
})
.finally(() => {
CassandraService.shutdown().catch((error: Error) => {
cassandra.shutdown().catch((error: Error) => {
logger.error(["Error shutting down Cassandra client:", error as Error]);
});

View file

@ -1,10 +1,10 @@
import { CassandraService } from "@lib/cassandra";
import { cassandra } from "@lib/cassandra";
async function createTable() {
await CassandraService.getClient().execute(`
await cassandra.getClient().execute(`
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
name TEXT,
username TEXT,
display_name TEXT,
email TEXT,
password TEXT,
@ -14,6 +14,14 @@ async function createTable() {
updated_at TIMESTAMP
);
`);
await cassandra.getClient().execute(`
CREATE INDEX IF NOT EXISTS users_username_idx ON users (username);
`);
await cassandra.getClient().execute(`
CREATE INDEX IF NOT EXISTS users_email_idx ON users (email);
`);
}
export { createTable };

View file

@ -8,7 +8,6 @@
"lint": "bunx biome check",
"lint:fix": "bunx biome check --fix",
"cleanup": "rm -rf logs node_modules bun.lockdb",
"setup": "bun run config/setup/index.ts"
},
"devDependencies": {
@ -22,6 +21,7 @@
},
"dependencies": {
"@creations.works/logger": "^1.0.3",
"cassandra-driver": "^4.8.0"
"cassandra-driver": "^4.8.0",
"pika-id": "^1.1.3"
}
}

View file

@ -1,7 +1,7 @@
import { serverHandler } from "@/server";
import { verifyRequiredVariables } from "@config/environment";
import { logger } from "@creations.works/logger";
import { CassandraService } from "@lib/cassandra";
import { cassandra } from "@lib/cassandra";
import { redis } from "bun";
async function main(): Promise<void> {
@ -16,7 +16,7 @@ async function main(): Promise<void> {
process.exit(1);
}
await CassandraService.connect();
await cassandra.connect();
serverHandler.initialize();
}

View file

@ -49,4 +49,4 @@ class CassandraService {
}
}
export { CassandraService };
export { CassandraService as cassandra };

View file

@ -9,10 +9,10 @@ const statusMessages: Record<number, string> = {
500: "Internal Server Error",
};
export async function returnGenericJsonResponse(
function jsonResponse(
statusCode: number,
options: GenericJsonResponseOptions = {},
): Promise<Response> {
): Response {
const { headers, message, error, ...extra } = options;
if (statusCode === 204) {
@ -53,8 +53,9 @@ export async function returnGenericJsonResponse(
return Response.json(orderedResponse, {
status: statusCode,
headers: {
"Content-Type": "application/json",
...headers,
},
});
}
export { jsonResponse };

61
src/lib/pika.ts Normal file
View file

@ -0,0 +1,61 @@
import Pika from "pika-id";
const pika = new Pika([
"user",
{
prefix: "user",
description: "User ID",
},
"guild",
{
prefix: "guild",
description: "Guild ID",
},
"message",
{
prefix: "msg",
description: "Message ID",
},
"channel",
{
prefix: "ch",
description: "Channel ID",
},
"role",
{
prefix: "role",
description: "Role ID",
},
"emoji",
{
prefix: "emoji",
description: "Emoji ID",
},
"webhook",
{
prefix: "wh",
description: "Webhook ID",
},
"application",
{
prefix: "app",
description: "Application ID",
},
"invite",
{
prefix: "inv",
description: "Invite ID",
},
"sticker",
{
prefix: "sticker",
description: "Sticker ID",
},
"session",
{
prefix: "sess",
description: "Session ID",
},
]);
export { pika };

View file

@ -0,0 +1,19 @@
const emailRestrictions: { regex: RegExp } = {
regex: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
};
function isValidEmail(rawEmail: string): validationResult {
const email = rawEmail.trim();
if (!email) {
return { valid: false, error: "Email is required" };
}
if (!emailRestrictions.regex.test(email)) {
return { valid: false, error: "Invalid email address" };
}
return { valid: true };
}
export { emailRestrictions, isValidEmail };

View file

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

View file

@ -0,0 +1,30 @@
// ? should support non english characters but won't mess up the url
const nameRestrictions: genericValidation = {
length: { min: 3, max: 20 },
regex: /^[\p{L}\p{N}._-]+$/u,
};
function isValidUsername(rawUsername: string): validationResult {
const username = rawUsername.trim().normalize("NFC");
if (!username) return { valid: false, error: "Username is required" };
if (username.length < nameRestrictions.length.min)
return { valid: false, error: "Username is too short" };
if (username.length > nameRestrictions.length.max)
return { valid: false, error: "Username is too long" };
if (!nameRestrictions.regex.test(username))
return { valid: false, error: "Username contains invalid characters" };
if (/^[._-]|[._-]$/.test(username))
return {
valid: false,
error: "Username can't start or end with special characters",
};
return { valid: true, username };
}
export { nameRestrictions, isValidUsername };

View file

@ -0,0 +1,36 @@
const passwordRestrictions: genericValidation = {
length: { min: 12, max: 64 },
regex: /^(?=.*\p{Ll})(?=.*\p{Lu})(?=.*\d)(?=.*[^\w\s]).{12,64}$/u,
};
function isValidPassword(rawPassword: string): validationResult {
if (!rawPassword) {
return { valid: false, error: "Password is required" };
}
if (rawPassword.length < passwordRestrictions.length.min) {
return {
valid: false,
error: `Password must be at least ${passwordRestrictions.length.min} characters`,
};
}
if (rawPassword.length > passwordRestrictions.length.max) {
return {
valid: false,
error: `Password must be at most ${passwordRestrictions.length.max} characters`,
};
}
if (!passwordRestrictions.regex.test(rawPassword)) {
return {
valid: false,
error:
"Password must contain at least one uppercase, one lowercase, one digit, and one special character",
};
}
return { valid: true };
}
export { passwordRestrictions, isValidPassword };

View file

@ -1,33 +1,94 @@
import { returnGenericJsonResponse } from "@lib/http";
import { cassandra } from "@lib/cassandra";
import { jsonResponse } from "@lib/http";
import { pika } from "@lib/pika";
import {
isValidEmail,
isValidPassword,
isValidUsername,
} from "@lib/validators";
const routeDef: RouteDef = {
method: "POST",
accepts: "application/json",
returns: "application/json",
returns: "application/json;charset=utf-8",
needsBody: "json",
};
async function handler(
request: ExtendedRequest,
_request: ExtendedRequest,
requestBody: unknown,
): Promise<Response> {
const { username, password, email } = requestBody as {
username: string;
password: string;
email: string;
username?: string;
password?: string;
email?: string;
};
if (!username || !password || !email) {
return returnGenericJsonResponse(400, {
message: "Missing required fields: username, password, email",
const fields = { username, password, email };
const validators = {
username: isValidUsername,
password: isValidPassword,
email: isValidEmail,
};
const errors: string[] = [];
for (const [key, validate] of Object.entries(validators)) {
const value = fields[key as keyof typeof fields];
if (typeof value !== "string" || value.trim() === "") {
errors.push(`${key} is required`);
continue;
}
const result = validate(value);
if (!result.valid) errors.push(`${key}: ${result.error}`);
}
if (errors.length > 0) {
return jsonResponse(400, {
message: "Validation failed",
error: errors.join("; "),
});
}
return returnGenericJsonResponse(200, {
const usernameResult = await cassandra
.getClient()
.execute("SELECT id FROM users WHERE username = ?", [username], {
prepare: true,
});
const emailResult = await cassandra
.getClient()
.execute("SELECT id FROM users WHERE email = ?", [email], {
prepare: true,
});
if (usernameResult.rowLength > 0 || emailResult.rowLength > 0) {
const errorMessages: string[] = [];
if (usernameResult.rowLength > 0) {
errorMessages.push("Username has already been taken");
}
if (emailResult.rowLength > 0) {
errorMessages.push("Email has already been taken");
}
return jsonResponse(400, {
message: "Validation failed",
error: errorMessages.join("; "),
});
}
const userId = pika.gen("user");
return jsonResponse(200, {
message: "User registered successfully",
username: username,
password,
email,
data: {
username,
email,
id: userId,
},
});
}

View file

@ -1,6 +1,7 @@
import { resolve } from "node:path";
import { environment } from "@config/environment";
import { logger } from "@creations.works/logger";
import { jsonResponse } from "@lib/http";
import {
type BunFile,
FileSystemRouter,
@ -138,18 +139,13 @@ class ServerHandler {
(!Array.isArray(routeModule.routeDef.method) &&
routeModule.routeDef.method !== request.method)
) {
response = Response.json(
{
success: false,
code: 405,
error: `Method ${request.method} Not Allowed, expected ${
Array.isArray(routeModule.routeDef.method)
? routeModule.routeDef.method.join(", ")
: routeModule.routeDef.method
}`,
},
{ status: 405 },
);
response = jsonResponse(405, {
error: `Method ${request.method} Not Allowed, expected ${
Array.isArray(routeModule.routeDef.method)
? routeModule.routeDef.method.join(", ")
: routeModule.routeDef.method
}`,
});
} else {
const expectedContentType: string | string[] | null =
routeModule.routeDef.accepts;
@ -167,18 +163,13 @@ class ServerHandler {
}
if (!matchesAccepts) {
response = Response.json(
{
success: false,
code: 406,
error: `Content-Type ${actualContentType} Not Acceptable, expected ${
Array.isArray(expectedContentType)
? expectedContentType.join(", ")
: expectedContentType
}`,
},
{ status: 406 },
);
response = jsonResponse(406, {
error: `Content-Type ${actualContentType} Not Acceptable, expected ${
Array.isArray(expectedContentType)
? expectedContentType.join(", ")
: expectedContentType
}`,
});
} else {
extendedRequest.params = params;
extendedRequest.query = query;
@ -200,24 +191,10 @@ class ServerHandler {
} catch (error: unknown) {
logger.error([`Error handling route ${request.url}:`, error as Error]);
response = Response.json(
{
success: false,
code: 500,
error: "Internal Server Error",
},
{ status: 500 },
);
response = jsonResponse(500);
}
} else {
response = Response.json(
{
success: false,
code: 404,
error: "Not Found",
},
{ status: 404 },
);
response = jsonResponse(404);
}
const headers = request.headers;

10
types/validation.d.ts vendored Normal file
View file

@ -0,0 +1,10 @@
type genericValidation = {
length: { min: number; max: number };
regex: RegExp;
};
type validationResult = {
valid: boolean;
error?: string;
username?: string;
};