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
All checks were successful
Code quality checks / biome (push) Successful in 9s
This commit is contained in:
parent
61db491848
commit
f93cef442a
15 changed files with 280 additions and 72 deletions
|
@ -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 };
|
||||
|
|
|
@ -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]);
|
||||
});
|
||||
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -49,4 +49,4 @@ class CassandraService {
|
|||
}
|
||||
}
|
||||
|
||||
export { CassandraService };
|
||||
export { CassandraService as cassandra };
|
||||
|
|
|
@ -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
61
src/lib/pika.ts
Normal 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 };
|
19
src/lib/validators/email.ts
Normal file
19
src/lib/validators/email.ts
Normal 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 };
|
3
src/lib/validators/index.ts
Normal file
3
src/lib/validators/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export * from "@lib/validators/name";
|
||||
export * from "@lib/validators/password";
|
||||
export * from "@lib/validators/email";
|
30
src/lib/validators/name.ts
Normal file
30
src/lib/validators/name.ts
Normal 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 };
|
36
src/lib/validators/password.ts
Normal file
36
src/lib/validators/password.ts
Normal 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 };
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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
10
types/validation.d.ts
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
type genericValidation = {
|
||||
length: { min: number; max: number };
|
||||
regex: RegExp;
|
||||
};
|
||||
|
||||
type validationResult = {
|
||||
valid: boolean;
|
||||
error?: string;
|
||||
username?: string;
|
||||
};
|
Loading…
Add table
Reference in a new issue