diff --git a/.env.example b/.env.example index 5e1c5db..b4aa814 100644 --- a/.env.example +++ b/.env.example @@ -2,16 +2,19 @@ HOST=0.0.0.0 PORT=9090 +# Replace with your domain name or IP address +# If you are using a reverse proxy, set the FQDN to your domain name +FQDN=localhost:9090 +FRONTEND_URL=http://localhost:8080 + PGHOST=localhost PGPORT=5432 PGUSERNAME=postgres PGPASSWORD=postgres PGDATABASE=postgres -REDIS_HOST=localhost -REDIS_PORT=6379 -# REDIS_USERNAME=redis -# REDIS_PASSWORD=redis +REDIS_URL=redis://localhost:6379 +REDIS_TTL=3600 # For sessions and cookies, can be generated using `openssl rand -base64 32` JWT_SECRET=your_jwt_secret diff --git a/.gitignore b/.gitignore index 1ba5dab..36e4935 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,3 @@ bun.lock .env /uploads .idea -temp -.vscode/settings.json diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 34232ea..4cfd4bc 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -2,7 +2,6 @@ "recommendations": [ "mikestead.dotenv", "EditorConfig.EditorConfig", - "leonzalion.vscode-ejs", "biomejs.biome" ] } diff --git a/biome.json b/biome.json index 921a7a5..46ee8c9 100644 --- a/biome.json +++ b/biome.json @@ -17,10 +17,19 @@ "organizeImports": { "enabled": true }, + "css": { + "formatter": { + "indentStyle": "tab", + "lineEnding": "lf" + } + }, "linter": { "enabled": true, "rules": { - "recommended": true + "recommended": true, + "correctness": { + "noUnusedImports": "error" + } } }, "javascript": { diff --git a/config/environment.ts b/config/environment.ts deleted file mode 100644 index 1130339..0000000 --- a/config/environment.ts +++ /dev/null @@ -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, -}; diff --git a/config/index.ts b/config/index.ts new file mode 100644 index 0000000..ad67386 --- /dev/null +++ b/config/index.ts @@ -0,0 +1,66 @@ +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", + frontendUrl: + normalizeFqdn(process.env.FRONTEND_URL) || "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, +}; + +const frontendUrl: string = process.env.FRONTEND_URL || "http://localhost:8080"; + +function verifyRequiredVariables(): void { + const requiredVariables = [ + "HOST", + "PORT", + + "FQDN", + "FRONTEND_URL", + + "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, frontendUrl }; diff --git a/config/jwt.ts b/config/jwt.ts new file mode 100644 index 0000000..e268a0c --- /dev/null +++ b/config/jwt.ts @@ -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), +}; diff --git a/config/redis.ts b/config/redis.ts new file mode 100644 index 0000000..8721478 --- /dev/null +++ b/config/redis.ts @@ -0,0 +1,3 @@ +export const redisTtl: number = process.env.REDIS_TTL + ? Number.parseInt(process.env.REDIS_TTL, 10) + : 60 * 60 * 1; // 1 hour diff --git a/config/sql/avatars.ts b/config/sql/avatars.ts index cbaafb6..8f40a73 100644 --- a/config/sql/avatars.ts +++ b/config/sql/avatars.ts @@ -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 { } } } - -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) - ); -} diff --git a/config/sql/files.ts b/config/sql/files.ts index 6a35576..844640d 100644 --- a/config/sql/files.ts +++ b/config/sql/files.ts @@ -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; diff --git a/config/sql/folders.ts b/config/sql/folders.ts index bc7beae..6329808 100644 --- a/config/sql/folders.ts +++ b/config/sql/folders.ts @@ -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; diff --git a/config/sql/invites.ts b/config/sql/invites.ts index 23a25d3..fabbbf7 100644 --- a/config/sql/invites.ts +++ b/config/sql/invites.ts @@ -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; diff --git a/config/sql/settings.ts b/config/sql/settings.ts index 938b5da..41d5fe7 100644 --- a/config/sql/settings.ts +++ b/config/sql/settings.ts @@ -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 { } } -// * Validation functions - export async function getSetting( key: string, reservation?: ReservedSQL, diff --git a/config/sql/users.ts b/config/sql/users.ts index b346b30..0cbf8c5 100644 --- a/config/sql/users.ts +++ b/config/sql/users.ts @@ -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 { } } } - -// * 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 }; -} diff --git a/package.json b/package.json index c82425e..6a99842 100644 --- a/package.json +++ b/package.json @@ -8,13 +8,12 @@ "dev": "bun run --hot src/index.ts --dev", "lint": "bunx biome check", "lint:fix": "bunx biome check --fix", - "cleanup": "rm -rf logs node_modules bun.lockdb", + "cleanup": "rm -rf logs node_modules bun.lock", "clearTable": "bun run src/helpers/commands/clearTable.ts" }, "devDependencies": { "@biomejs/biome": "^1.9.4", - "@types/bun": "^1.2.9", - "@types/ejs": "^3.1.5", + "@types/bun": "^1.2.13", "@types/fluent-ffmpeg": "^2.1.27", "@types/image-thumbnail": "^1.0.4", "@types/luxon": "^3.6.2", @@ -22,17 +21,15 @@ "prettier": "^3.5.3" }, "peerDependencies": { - "typescript": "^5.8.2" + "typescript": "^5.8.3" }, "dependencies": { - "ejs": "^3.1.10", + "@creations.works/logger": "^1.0.3", "eta": "^3.5.0", - "exiftool-vendored": "^29.3.0", + "exiftool-vendored": "^30.0.0", "fast-jwt": "6.0.1", "fluent-ffmpeg": "^2.1.3", "image-thumbnail": "^1.0.17", - "luxon": "^3.6.1", - "preact-render-to-string": "^6.5.13", - "redis": "^4.7.0" + "luxon": "^3.6.1" } } diff --git a/src/components/head.tsx b/src/components/head.tsx deleted file mode 100644 index b519547..0000000 --- a/src/components/head.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import type { FunctionalComponent, JSX } from "preact"; - -export const Head: FunctionalComponent = ({ - title, - styles, - scripts, -}): JSX.Element => ( - <> - - - - - {title && {title}} - - - - {styles?.map( - (style): JSX.Element => ( - - ), - )} - - {scripts?.map( - (script: string | [string, boolean]): JSX.Element | undefined => { - if (typeof script === "string") { - return - <% } else if (Array.isArray(script)) { %> - <% if (script[1]) { %> - - <% } else { %> - - <% } %> - <% } %> - <% }) %> -<% } %> - - diff --git a/src/views/index.ejs b/src/views/index.ejs deleted file mode 100644 index dfc2c62..0000000 --- a/src/views/index.ejs +++ /dev/null @@ -1,9 +0,0 @@ - - - - <%- include("global", { styles: [], scripts: [] }) %> - - - - - diff --git a/src/websocket.ts b/src/websocket.ts index 99686e8..7b65476 100644 --- a/src/websocket.ts +++ b/src/websocket.ts @@ -1,4 +1,4 @@ -import { logger } from "@helpers/logger"; +import { logger } from "@creations.works/logger"; import type { ServerWebSocket } from "bun"; class WebSocketHandler { diff --git a/tsconfig.json b/tsconfig.json index 871dc75..391e2c9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,9 +3,10 @@ "baseUrl": "./", "paths": { "@/*": ["src/*"], + "@config": ["config/index.ts"], "@config/*": ["config/*"], "@types/*": ["types/*"], - "@helpers/*": ["src/helpers/*"] + "@lib/*": ["src/lib/*"] }, "typeRoots": ["./src/types", "./node_modules/@types"], // Enable latest features @@ -14,8 +15,7 @@ "module": "ESNext", "moduleDetection": "force", "jsx": "react-jsx", - "jsxImportSource": "preact", - "allowJs": false, + "allowJs": true, // Bundler mode "moduleResolution": "bundler", "allowImportingTsExtensions": true, diff --git a/types/config.d.ts b/types/config.d.ts index 322a951..e65c147 100644 --- a/types/config.d.ts +++ b/types/config.d.ts @@ -2,6 +2,8 @@ type Environment = { port: number; host: string; development: boolean; + fqdn: string; + frontendUrl: string; }; type UserValidation = { diff --git a/types/ejs.d.ts b/types/ejs.d.ts deleted file mode 100644 index 486a4a4..0000000 --- a/types/ejs.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -interface EjsTemplateData { - [key: string]: string | number | boolean | object | undefined | null; -} diff --git a/types/logger.d.ts b/types/logger.d.ts deleted file mode 100644 index ff6a601..0000000 --- a/types/logger.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -type ILogMessagePart = { value: string; color: string }; - -type ILogMessageParts = { - level: ILogMessagePart; - filename: ILogMessagePart; - readableTimestamp: ILogMessagePart; - message: ILogMessagePart; - [key: string]: ILogMessagePart; -}; diff --git a/types/preact.d.ts b/types/preact.d.ts deleted file mode 100644 index a340d43..0000000 --- a/types/preact.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -type Props = { - title?: string; - styles?: string[]; - scripts?: (string | [string, boolean])[]; - children?: preact.ComponentChildren; -};