This commit is contained in:
commit
421043c9b5
67 changed files with 3455 additions and 0 deletions
69
environment/config.ts
Normal file
69
environment/config.ts
Normal file
|
@ -0,0 +1,69 @@
|
|||
import { echo } from "@atums/echo";
|
||||
import { validateJWTConfig } from "#lib/validation";
|
||||
import { cassandraConfig, validateCassandraConfig } from "./database/cassandra";
|
||||
import { jwt } from "./jwt";
|
||||
|
||||
import type { Environment } from "#types/config";
|
||||
|
||||
const environment: Environment = {
|
||||
port: Number.parseInt(process.env.PORT || "8080", 10),
|
||||
host: process.env.HOST || "0.0.0.0",
|
||||
development:
|
||||
process.env.NODE_ENV === "development" || process.argv.includes("--dev"),
|
||||
};
|
||||
|
||||
function verifyRequiredVariables(): void {
|
||||
const requiredVariables = [
|
||||
"HOST",
|
||||
"PORT",
|
||||
|
||||
"REDIS_URL",
|
||||
"REDIS_TTL",
|
||||
|
||||
"CASSANDRA_HOST",
|
||||
"CASSANDRA_PORT",
|
||||
"CASSANDRA_CONTACT_POINTS",
|
||||
"CASSANDRA_AUTH_ENABLED",
|
||||
"CASSANDRA_DATACENTER",
|
||||
|
||||
"JWT_SECRET",
|
||||
"JWT_EXPIRATION",
|
||||
"JWT_ISSUER",
|
||||
|
||||
"FRONTEND_FQDN",
|
||||
];
|
||||
|
||||
let hasError = false;
|
||||
|
||||
for (const key of requiredVariables) {
|
||||
const value = process.env[key];
|
||||
if (value === undefined || value.trim() === "") {
|
||||
echo.error(`Missing or empty environment variable: ${key}`);
|
||||
hasError = true;
|
||||
}
|
||||
}
|
||||
|
||||
const validateCassandra = validateCassandraConfig(cassandraConfig);
|
||||
|
||||
if (!validateCassandra.isValid) {
|
||||
echo.error("Cassandra configuration validation failed:");
|
||||
for (const error of validateCassandra.errors) {
|
||||
echo.error(`- ${error}`);
|
||||
}
|
||||
hasError = true;
|
||||
}
|
||||
|
||||
const validateJWT = validateJWTConfig(jwt);
|
||||
|
||||
if (!validateJWT.valid) {
|
||||
echo.error("JWT configuration validation failed:");
|
||||
echo.error(`- ${validateJWT.error}`);
|
||||
hasError = true;
|
||||
}
|
||||
|
||||
if (hasError) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
export { environment, verifyRequiredVariables };
|
5
environment/constants/database/index.ts
Normal file
5
environment/constants/database/index.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { resolve } from "node:path";
|
||||
|
||||
const migrationsPath = resolve("environment", "database", "migrations");
|
||||
|
||||
export { migrationsPath };
|
3
environment/constants/index.ts
Normal file
3
environment/constants/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export * from "./server";
|
||||
export * from "./validation";
|
||||
export * from "./database";
|
6
environment/constants/server.ts
Normal file
6
environment/constants/server.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
const reqLoggerIgnores = {
|
||||
ignoredStartsWith: ["/public"],
|
||||
ignoredPaths: [""],
|
||||
};
|
||||
|
||||
export { reqLoggerIgnores };
|
37
environment/constants/validation.ts
Normal file
37
environment/constants/validation.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
import type { genericValidation } from "#types/lib";
|
||||
|
||||
const nameRestrictions: genericValidation = {
|
||||
length: { min: 3, max: 20 },
|
||||
regex: /^[\p{L}\p{N}._-]+$/u,
|
||||
};
|
||||
|
||||
const displayNameRestrictions: genericValidation = {
|
||||
length: { min: 1, max: 32 },
|
||||
regex: /^[\p{L}\p{N}\p{M}\p{S}\p{P}\s]+$/u,
|
||||
};
|
||||
|
||||
const forbiddenDisplayNamePatterns = [
|
||||
/[\r\n\t]/,
|
||||
/\s{3,}/,
|
||||
/^\s|\s$/,
|
||||
/#everyone|#here/i,
|
||||
/\p{Cf}/u,
|
||||
/\p{Cc}/u,
|
||||
];
|
||||
|
||||
const passwordRestrictions: genericValidation = {
|
||||
length: { min: 12, max: 64 },
|
||||
regex: /^(?=.*\p{Ll})(?=.*\p{Lu})(?=.*\d)(?=.*[^\w\s]).{12,64}$/u,
|
||||
};
|
||||
|
||||
const emailRestrictions: { regex: RegExp } = {
|
||||
regex: /^[^\s#]+#[^\s#]+\.[^\s#]+$/,
|
||||
};
|
||||
|
||||
export {
|
||||
nameRestrictions,
|
||||
displayNameRestrictions,
|
||||
forbiddenDisplayNamePatterns,
|
||||
passwordRestrictions,
|
||||
emailRestrictions,
|
||||
};
|
114
environment/database/cassandra.ts
Normal file
114
environment/database/cassandra.ts
Normal file
|
@ -0,0 +1,114 @@
|
|||
import type { CassandraConfig } from "#types/config";
|
||||
|
||||
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): {
|
||||
isValid: boolean;
|
||||
errors: string[];
|
||||
} {
|
||||
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
environment/database/index.ts
Normal file
1
environment/database/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from "./cassandra";
|
14
environment/database/migrations/up/001_create_users.sql
Normal file
14
environment/database/migrations/up/001_create_users.sql
Normal file
|
@ -0,0 +1,14 @@
|
|||
CREATE TABLE IF NOT EXISTS users (
|
||||
id TEXT PRIMARY KEY,
|
||||
username TEXT,
|
||||
display_name TEXT,
|
||||
email TEXT,
|
||||
password TEXT,
|
||||
avatar_url TEXT,
|
||||
is_verified BOOLEAN,
|
||||
created_at TIMESTAMP,
|
||||
updated_at TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS users_username_idx ON users (username);
|
||||
CREATE INDEX IF NOT EXISTS users_email_idx ON users (email);
|
28
environment/jwt.ts
Normal file
28
environment/jwt.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { getExpirationInSeconds } from "#lib/utils";
|
||||
import { validateJWTConfig } from "#lib/validation";
|
||||
|
||||
import type { JWTConfig } from "#types/config";
|
||||
|
||||
function createJWTConfig(): JWTConfig {
|
||||
const jwtSecret = process.env.JWT_SECRET || "";
|
||||
const jwtExpiration = process.env.JWT_EXPIRATION || "1h";
|
||||
const jwtIssuer = process.env.JWT_ISSUER || "";
|
||||
const jwtAlgorithm = process.env.JWT_ALGORITHM || "HS256";
|
||||
|
||||
const configForValidation: JWTConfig = {
|
||||
secret: jwtSecret,
|
||||
expiration: getExpirationInSeconds(jwtExpiration),
|
||||
issuer: jwtIssuer,
|
||||
algorithm: jwtAlgorithm,
|
||||
};
|
||||
|
||||
const validation = validateJWTConfig(configForValidation);
|
||||
|
||||
if (!validation.valid) {
|
||||
throw new Error(`JWT Configuration Error: ${validation.error}`);
|
||||
}
|
||||
|
||||
return configForValidation;
|
||||
}
|
||||
|
||||
export const jwt = createJWTConfig();
|
Loading…
Add table
Add a link
Reference in a new issue