re-order alot, move to bun redis, generalized

This commit is contained in:
creations 2025-05-18 09:53:23 -04:00
parent a646607597
commit 8a9499be85
Signed by: creations
GPG key ID: 8F553AA4320FC711
51 changed files with 559 additions and 916 deletions

View file

@ -1,36 +0,0 @@
import { resolve } from "node:path";
export 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"),
};
export const redisConfig: {
host: string;
port: number;
username?: string | undefined;
password?: string | undefined;
} = {
host: process.env.REDIS_HOST || "localhost",
port: Number.parseInt(process.env.REDIS_PORT || "6379", 10),
username: process.env.REDIS_USERNAME || undefined,
password: process.env.REDIS_PASSWORD || undefined,
};
export const jwt: {
secret: string;
expiresIn: string;
} = {
secret: process.env.JWT_SECRET || "",
expiresIn: process.env.JWT_EXPIRES || "1d",
};
export const dataType: { type: string; path: string | undefined } = {
type: process.env.DATASOURCE_TYPE || "local",
path:
process.env.DATASOURCE_TYPE === "local"
? resolve(process.env.DATASOURCE_LOCAL_DIRECTORY || "./uploads")
: undefined,
};

61
config/index.ts Normal file
View file

@ -0,0 +1,61 @@
import { resolve } from "node:path";
import { logger } from "@creations.works/logger";
import { normalizeFqdn } from "@lib/char";
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"),
fqdn: normalizeFqdn(process.env.FQDN) || "http://localhost:8080",
};
const dataType: { type: string; path: string | undefined } = {
type: process.env.DATASOURCE_TYPE || "local",
path:
process.env.DATASOURCE_TYPE === "local"
? resolve(process.env.DATASOURCE_LOCAL_DIRECTORY || "./uploads")
: undefined,
};
function verifyRequiredVariables(): void {
const requiredVariables = [
"HOST",
"PORT",
"FQDN",
"PGHOST",
"PGPORT",
"PGUSERNAME",
"PGPASSWORD",
"PGDATABASE",
"REDIS_URL",
"REDIS_TTL",
"JWT_SECRET",
"JWT_EXPIRES",
"DATASOURCE_TYPE",
];
let hasError = false;
for (const key of requiredVariables) {
const value = process.env[key];
if (value === undefined || value.trim() === "") {
logger.error(`Missing or empty environment variable: ${key}`);
hasError = true;
}
}
if (hasError) {
process.exit(1);
}
}
export * from "@config/jwt";
export * from "@config/redis";
export { environment, dataType, verifyRequiredVariables };

27
config/jwt.ts Normal file
View file

@ -0,0 +1,27 @@
const allowedAlgorithms = [
"HS256",
"RS256",
"HS384",
"HS512",
"RS384",
"RS512",
] as const;
type AllowedAlgorithm = (typeof allowedAlgorithms)[number];
function getAlgorithm(envVar: string | undefined): AllowedAlgorithm {
if (allowedAlgorithms.includes(envVar as AllowedAlgorithm)) {
return envVar as AllowedAlgorithm;
}
return "HS256";
}
export const jwt: {
secret: string;
expiration: string;
algorithm: AllowedAlgorithm;
} = {
secret: process.env.JWT_SECRET || "",
expiration: process.env.JWT_EXPIRATION || "1h",
algorithm: getAlgorithm(process.env.JWT_ALGORITHM),
};

3
config/redis.ts Normal file
View file

@ -0,0 +1,3 @@
export const redisTtl: number = process.env.REDIS_TTL
? Number.parseInt(process.env.REDIS_TTL, 10)
: 60 * 60 * 1; // 1 hour

View file

@ -1,4 +1,4 @@
import { logger } from "@helpers/logger";
import { logger } from "@creations.works/logger";
import { type ReservedSQL, sql } from "bun";
export const order: number = 6;
@ -32,13 +32,3 @@ export async function createTable(reservation?: ReservedSQL): Promise<void> {
}
}
}
export function isValidTypeOrExtension(
type: string,
extension: string,
): boolean {
return (
["image/jpeg", "image/png", "image/gif", "image/webp"].includes(type) &&
["jpeg", "jpg", "png", "gif", "webp"].includes(extension)
);
}

View file

@ -1,4 +1,4 @@
import { logger } from "@helpers/logger";
import { logger } from "@creations.works/logger";
import { type ReservedSQL, sql } from "bun";
export const order: number = 5;

View file

@ -1,4 +1,4 @@
import { logger } from "@helpers/logger";
import { logger } from "@creations.works/logger";
import { type ReservedSQL, sql } from "bun";
export const order: number = 4;

View file

@ -1,4 +1,4 @@
import { logger } from "@helpers/logger";
import { logger } from "@creations.works/logger";
import { type ReservedSQL, sql } from "bun";
export const order: number = 3;

View file

@ -1,4 +1,4 @@
import { logger } from "@helpers/logger";
import { logger } from "@creations.works/logger";
import { type ReservedSQL, sql } from "bun";
export const order: number = 2;
@ -93,8 +93,6 @@ export async function createTable(reservation?: ReservedSQL): Promise<void> {
}
}
// * Validation functions
export async function getSetting(
key: string,
reservation?: ReservedSQL,

View file

@ -1,4 +1,4 @@
import { logger } from "@helpers/logger";
import { logger } from "@creations.works/logger";
import { type ReservedSQL, sql } from "bun";
export const order: number = 1;
@ -36,135 +36,3 @@ export async function createTable(reservation?: ReservedSQL): Promise<void> {
}
}
}
// * Validation functions
// ? should support non english characters but won't mess up the url
export const userNameRestrictions: {
length: { min: number; max: number };
regex: RegExp;
} = {
length: { min: 3, max: 20 },
regex: /^[\p{L}\p{N}._-]+$/u,
};
export const passwordRestrictions: {
length: { min: number; max: number };
regex: RegExp;
} = {
length: { min: 12, max: 64 },
regex: /^(?=.*\p{Ll})(?=.*\p{Lu})(?=.*\d)(?=.*[^\w\s]).{12,64}$/u,
};
export const emailRestrictions: { regex: RegExp } = {
regex: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
};
export const inviteRestrictions: { min: number; max: number; regex: RegExp } = {
min: 4,
max: 15,
regex: /^[a-zA-Z0-9]+$/,
};
export function isValidUsername(username: string): {
valid: boolean;
error?: string;
} {
if (!username) {
return { valid: false, error: "" };
}
if (username.length < userNameRestrictions.length.min) {
return { valid: false, error: "Username is too short" };
}
if (username.length > userNameRestrictions.length.max) {
return { valid: false, error: "Username is too long" };
}
if (!userNameRestrictions.regex.test(username)) {
return { valid: false, error: "Username contains invalid characters" };
}
return { valid: true };
}
export function isValidPassword(password: string): {
valid: boolean;
error?: string;
} {
if (!password) {
return { valid: false, error: "" };
}
if (password.length < passwordRestrictions.length.min) {
return {
valid: false,
error: `Password must be at least ${passwordRestrictions.length.min} characters long`,
};
}
if (password.length > passwordRestrictions.length.max) {
return {
valid: false,
error: `Password can't be longer than ${passwordRestrictions.length.max} characters`,
};
}
if (!passwordRestrictions.regex.test(password)) {
return {
valid: false,
error:
"Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character",
};
}
return { valid: true };
}
export function isValidEmail(email: string): {
valid: boolean;
error?: string;
} {
if (!email) {
return { valid: false, error: "" };
}
if (!emailRestrictions.regex.test(email)) {
return { valid: false, error: "Invalid email address" };
}
return { valid: true };
}
export function isValidInvite(invite: string): {
valid: boolean;
error?: string;
} {
if (!invite) {
return { valid: false, error: "" };
}
if (invite.length < inviteRestrictions.min) {
return {
valid: false,
error: `Invite code must be at least ${inviteRestrictions.min} characters long`,
};
}
if (invite.length > inviteRestrictions.max) {
return {
valid: false,
error: `Invite code can't be longer than ${inviteRestrictions.max} characters`,
};
}
if (!inviteRestrictions.regex.test(invite)) {
return {
valid: false,
error: "Invite code contains invalid characters",
};
}
return { valid: true };
}