diff --git a/config/environment.ts b/config/environment.ts index 91ada96..c6d357b 100644 --- a/config/environment.ts +++ b/config/environment.ts @@ -1,17 +1,17 @@ import { logger } from "@creations.works/logger"; -const environment: Environment = { +export 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"), }; -const redisTtl: number = process.env.REDIS_TTL +export const redisTtl: number = process.env.REDIS_TTL ? Number.parseInt(process.env.REDIS_TTL, 10) : 60 * 60 * 1; // 1 hour -const cassandra: CassandraConfig = { +export 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 @@ const cassandra: CassandraConfig = { authEnabled: process.env.CASSANDRA_AUTH_ENABLED === "false", }; -function verifyRequiredVariables(): void { +export function verifyRequiredVariables(): void { const requiredVariables = [ "HOST", "PORT", @@ -51,5 +51,3 @@ function verifyRequiredVariables(): void { process.exit(1); } } - -export { environment, cassandra, redisTtl, verifyRequiredVariables }; diff --git a/config/setup/index.ts b/config/setup/index.ts index ea13288..fc9f954 100644 --- a/config/setup/index.ts +++ b/config/setup/index.ts @@ -6,13 +6,13 @@ import { verifyRequiredVariables, } from "@config/environment"; import { logger } from "@creations.works/logger"; -import { cassandra } from "@lib/cassandra"; +import { CassandraService } from "@lib/cassandra"; async function setup(): Promise { verifyRequiredVariables(); - await cassandra.connect({ withKeyspace: false }); + await CassandraService.connect({ withKeyspace: false }); - const client = cassandra.getClient(); + const client = CassandraService.getClient(); const keyspace = cassandraConfig.keyspace; if (!keyspace) { @@ -60,7 +60,7 @@ setup() process.exit(1); }) .finally(() => { - cassandra.shutdown().catch((error: Error) => { + CassandraService.shutdown().catch((error: Error) => { logger.error(["Error shutting down Cassandra client:", error as Error]); }); diff --git a/config/setup/tables/users.ts b/config/setup/tables/users.ts index e7dbb95..00f62c1 100644 --- a/config/setup/tables/users.ts +++ b/config/setup/tables/users.ts @@ -1,10 +1,10 @@ -import { cassandra } from "@lib/cassandra"; +import { CassandraService } from "@lib/cassandra"; async function createTable() { - await cassandra.getClient().execute(` + await CassandraService.getClient().execute(` CREATE TABLE IF NOT EXISTS users ( id TEXT PRIMARY KEY, - username TEXT, + name TEXT, display_name TEXT, email TEXT, password TEXT, @@ -14,14 +14,6 @@ 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 }; diff --git a/package.json b/package.json index 62d5f6b..a3f3ce1 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "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": { @@ -21,7 +22,6 @@ }, "dependencies": { "@creations.works/logger": "^1.0.3", - "cassandra-driver": "^4.8.0", - "pika-id": "^1.1.3" + "cassandra-driver": "^4.8.0" } } diff --git a/src/index.ts b/src/index.ts index 432e6a8..3d62bbc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ import { serverHandler } from "@/server"; import { verifyRequiredVariables } from "@config/environment"; import { logger } from "@creations.works/logger"; -import { cassandra } from "@lib/cassandra"; +import { CassandraService } from "@lib/cassandra"; import { redis } from "bun"; async function main(): Promise { @@ -16,7 +16,7 @@ async function main(): Promise { process.exit(1); } - await cassandra.connect(); + await CassandraService.connect(); serverHandler.initialize(); } diff --git a/src/lib/cassandra.ts b/src/lib/cassandra.ts index eddec13..02a5acb 100644 --- a/src/lib/cassandra.ts +++ b/src/lib/cassandra.ts @@ -49,4 +49,4 @@ class CassandraService { } } -export { CassandraService as cassandra }; +export { CassandraService }; diff --git a/src/lib/http.ts b/src/lib/http.ts index 1401874..3ba934b 100644 --- a/src/lib/http.ts +++ b/src/lib/http.ts @@ -9,10 +9,10 @@ const statusMessages: Record = { 500: "Internal Server Error", }; -function jsonResponse( +export async function returnGenericJsonResponse( statusCode: number, options: GenericJsonResponseOptions = {}, -): Response { +): Promise { const { headers, message, error, ...extra } = options; if (statusCode === 204) { @@ -53,9 +53,8 @@ function jsonResponse( return Response.json(orderedResponse, { status: statusCode, headers: { + "Content-Type": "application/json", ...headers, }, }); } - -export { jsonResponse }; diff --git a/src/lib/pika.ts b/src/lib/pika.ts deleted file mode 100644 index be8c627..0000000 --- a/src/lib/pika.ts +++ /dev/null @@ -1,61 +0,0 @@ -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 }; diff --git a/src/lib/validators/email.ts b/src/lib/validators/email.ts deleted file mode 100644 index 9fe4e2e..0000000 --- a/src/lib/validators/email.ts +++ /dev/null @@ -1,19 +0,0 @@ -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 }; diff --git a/src/lib/validators/index.ts b/src/lib/validators/index.ts deleted file mode 100644 index 555cc17..0000000 --- a/src/lib/validators/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "@lib/validators/name"; -export * from "@lib/validators/password"; -export * from "@lib/validators/email"; diff --git a/src/lib/validators/name.ts b/src/lib/validators/name.ts deleted file mode 100644 index 6025f95..0000000 --- a/src/lib/validators/name.ts +++ /dev/null @@ -1,30 +0,0 @@ -// ? 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 }; diff --git a/src/lib/validators/password.ts b/src/lib/validators/password.ts deleted file mode 100644 index 26b2ee4..0000000 --- a/src/lib/validators/password.ts +++ /dev/null @@ -1,36 +0,0 @@ -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 }; diff --git a/src/routes/user/register.ts b/src/routes/user/register.ts index 815ecd5..29a9ddf 100644 --- a/src/routes/user/register.ts +++ b/src/routes/user/register.ts @@ -1,94 +1,33 @@ -import { cassandra } from "@lib/cassandra"; -import { jsonResponse } from "@lib/http"; -import { pika } from "@lib/pika"; -import { - isValidEmail, - isValidPassword, - isValidUsername, -} from "@lib/validators"; +import { returnGenericJsonResponse } from "@/lib/http"; const routeDef: RouteDef = { method: "POST", accepts: "application/json", - returns: "application/json;charset=utf-8", + returns: "application/json", needsBody: "json", }; async function handler( - _request: ExtendedRequest, + request: ExtendedRequest, requestBody: unknown, ): Promise { const { username, password, email } = requestBody as { - username?: string; - password?: string; - email?: string; + username: string; + password: string; + email: string; }; - 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("; "), + if (!username || !password || !email) { + return returnGenericJsonResponse(400, { + message: "Missing required fields: username, password, email", }); } - 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, { + return returnGenericJsonResponse(200, { message: "User registered successfully", - data: { - username, - email, - id: userId, - }, + username: username, + password, + email, }); } diff --git a/src/server.ts b/src/server.ts index d4b6ba1..5646636 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,7 +1,6 @@ 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, @@ -139,13 +138,18 @@ class ServerHandler { (!Array.isArray(routeModule.routeDef.method) && routeModule.routeDef.method !== request.method) ) { - response = jsonResponse(405, { - error: `Method ${request.method} Not Allowed, expected ${ - Array.isArray(routeModule.routeDef.method) - ? routeModule.routeDef.method.join(", ") - : routeModule.routeDef.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 }, + ); } else { const expectedContentType: string | string[] | null = routeModule.routeDef.accepts; @@ -163,13 +167,18 @@ class ServerHandler { } if (!matchesAccepts) { - response = jsonResponse(406, { - error: `Content-Type ${actualContentType} Not Acceptable, expected ${ - Array.isArray(expectedContentType) - ? expectedContentType.join(", ") - : expectedContentType - }`, - }); + response = Response.json( + { + success: false, + code: 406, + error: `Content-Type ${actualContentType} Not Acceptable, expected ${ + Array.isArray(expectedContentType) + ? expectedContentType.join(", ") + : expectedContentType + }`, + }, + { status: 406 }, + ); } else { extendedRequest.params = params; extendedRequest.query = query; @@ -191,10 +200,24 @@ class ServerHandler { } catch (error: unknown) { logger.error([`Error handling route ${request.url}:`, error as Error]); - response = jsonResponse(500); + response = Response.json( + { + success: false, + code: 500, + error: "Internal Server Error", + }, + { status: 500 }, + ); } } else { - response = jsonResponse(404); + response = Response.json( + { + success: false, + code: 404, + error: "Not Found", + }, + { status: 404 }, + ); } const headers = request.headers; diff --git a/types/validation.d.ts b/types/validation.d.ts deleted file mode 100644 index 93ca84b..0000000 --- a/types/validation.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -type genericValidation = { - length: { min: number; max: number }; - regex: RegExp; -}; - -type validationResult = { - valid: boolean; - error?: string; - username?: string; -};