diff --git a/config/environment.ts b/config/environment.ts index 50cfe9e..1130339 100644 --- a/config/environment.ts +++ b/config/environment.ts @@ -1,4 +1,4 @@ -import { resolve } from "path"; +import { resolve } from "node:path"; export const environment: Environment = { port: Number.parseInt(process.env.PORT || "8080", 10), diff --git a/config/sql/avatars.ts b/config/sql/avatars.ts index 78d33d2..cbaafb6 100644 --- a/config/sql/avatars.ts +++ b/config/sql/avatars.ts @@ -5,14 +5,14 @@ export const order: number = 6; export async function createTable(reservation?: ReservedSQL): Promise { let selfReservation = false; + const activeReservation: ReservedSQL = reservation ?? (await sql.reserve()); if (!reservation) { - reservation = await sql.reserve(); selfReservation = true; } try { - await reservation` + await activeReservation` CREATE TABLE IF NOT EXISTS avatars ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), owner UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, @@ -28,7 +28,7 @@ export async function createTable(reservation?: ReservedSQL): Promise { throw error; } finally { if (selfReservation) { - reservation.release(); + activeReservation.release(); } } } diff --git a/config/sql/files.ts b/config/sql/files.ts index 5eafcfd..6a35576 100644 --- a/config/sql/files.ts +++ b/config/sql/files.ts @@ -5,14 +5,14 @@ export const order: number = 5; export async function createTable(reservation?: ReservedSQL): Promise { let selfReservation = false; + const activeReservation: ReservedSQL = reservation ?? (await sql.reserve()); if (!reservation) { - reservation = await sql.reserve(); selfReservation = true; } try { - await reservation` + await activeReservation` CREATE TABLE IF NOT EXISTS files ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), owner UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, @@ -37,7 +37,7 @@ export async function createTable(reservation?: ReservedSQL): Promise { ); `; - const functionExists: { exists: boolean }[] = await reservation` + const functionExists: { exists: boolean }[] = await activeReservation` SELECT EXISTS ( SELECT 1 FROM pg_proc JOIN pg_namespace ON pg_proc.pronamespace = pg_namespace.oid @@ -46,7 +46,7 @@ export async function createTable(reservation?: ReservedSQL): Promise { `; if (!functionExists[0].exists) { - await reservation` + await activeReservation` CREATE FUNCTION update_files_updated_at() RETURNS TRIGGER AS $$ BEGIN @@ -57,7 +57,7 @@ export async function createTable(reservation?: ReservedSQL): Promise { `; } - const triggerExists: { exists: boolean }[] = await reservation` + const triggerExists: { exists: boolean }[] = await activeReservation` SELECT EXISTS ( SELECT 1 FROM pg_trigger WHERE tgname = 'trigger_update_files_updated_at' @@ -65,7 +65,7 @@ export async function createTable(reservation?: ReservedSQL): Promise { `; if (!triggerExists[0].exists) { - await reservation` + await activeReservation` CREATE TRIGGER trigger_update_files_updated_at BEFORE UPDATE ON files FOR EACH ROW @@ -80,7 +80,7 @@ export async function createTable(reservation?: ReservedSQL): Promise { throw error; } finally { if (selfReservation) { - reservation.release(); + activeReservation.release(); } } } diff --git a/config/sql/folders.ts b/config/sql/folders.ts index d2358d8..bc7beae 100644 --- a/config/sql/folders.ts +++ b/config/sql/folders.ts @@ -5,14 +5,14 @@ export const order: number = 4; export async function createTable(reservation?: ReservedSQL): Promise { let selfReservation = false; + const activeReservation: ReservedSQL = reservation ?? (await sql.reserve()); if (!reservation) { - reservation = await sql.reserve(); selfReservation = true; } try { - await reservation` + await activeReservation` CREATE TABLE IF NOT EXISTS folders ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), owner UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, @@ -26,7 +26,7 @@ export async function createTable(reservation?: ReservedSQL): Promise { ); `; - const functionExists: { exists: boolean }[] = await reservation` + const functionExists: { exists: boolean }[] = await activeReservation` SELECT EXISTS ( SELECT 1 FROM pg_proc JOIN pg_namespace ON pg_proc.pronamespace = pg_namespace.oid @@ -35,7 +35,7 @@ export async function createTable(reservation?: ReservedSQL): Promise { `; if (!functionExists[0].exists) { - await reservation` + await activeReservation` CREATE FUNCTION update_folders_updated_at() RETURNS TRIGGER AS $$ BEGIN @@ -46,7 +46,7 @@ export async function createTable(reservation?: ReservedSQL): Promise { `; } - const triggerExists: { exists: boolean }[] = await reservation` + const triggerExists: { exists: boolean }[] = await activeReservation` SELECT EXISTS ( SELECT 1 FROM pg_trigger WHERE tgname = 'trigger_update_folders_updated_at' @@ -54,7 +54,7 @@ export async function createTable(reservation?: ReservedSQL): Promise { `; if (!triggerExists[0].exists) { - await reservation` + await activeReservation` CREATE TRIGGER trigger_update_folders_updated_at BEFORE UPDATE ON folders FOR EACH ROW @@ -69,7 +69,7 @@ export async function createTable(reservation?: ReservedSQL): Promise { throw error; } finally { if (selfReservation) { - reservation.release(); + activeReservation.release(); } } } diff --git a/config/sql/invites.ts b/config/sql/invites.ts index 7f27e11..23a25d3 100644 --- a/config/sql/invites.ts +++ b/config/sql/invites.ts @@ -5,14 +5,14 @@ export const order: number = 3; export async function createTable(reservation?: ReservedSQL): Promise { let selfReservation = false; + const activeReservation: ReservedSQL = reservation ?? (await sql.reserve()); if (!reservation) { - reservation = await sql.reserve(); selfReservation = true; } try { - await reservation` + await activeReservation` CREATE TABLE IF NOT EXISTS invites ( id TEXT PRIMARY KEY NOT NULL UNIQUE, created_by UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, @@ -27,7 +27,7 @@ export async function createTable(reservation?: ReservedSQL): Promise { throw error; } finally { if (selfReservation) { - reservation.release(); + activeReservation.release(); } } } diff --git a/config/sql/settings.ts b/config/sql/settings.ts index 8a2d339..938b5da 100644 --- a/config/sql/settings.ts +++ b/config/sql/settings.ts @@ -21,14 +21,14 @@ const defaultSettings: Setting[] = [ export async function createTable(reservation?: ReservedSQL): Promise { let selfReservation = false; + const activeReservation: ReservedSQL = reservation ?? (await sql.reserve()); if (!reservation) { - reservation = await sql.reserve(); selfReservation = true; } try { - await reservation` + await activeReservation` CREATE TABLE IF NOT EXISTS settings ( "key" VARCHAR(64) PRIMARY KEY NOT NULL UNIQUE, "value" TEXT NOT NULL, @@ -37,7 +37,7 @@ export async function createTable(reservation?: ReservedSQL): Promise { ); `; - const functionExists: { exists: boolean }[] = await reservation` + const functionExists: { exists: boolean }[] = await activeReservation` SELECT EXISTS ( SELECT 1 FROM pg_proc JOIN pg_namespace ON pg_proc.pronamespace = pg_namespace.oid @@ -46,7 +46,7 @@ export async function createTable(reservation?: ReservedSQL): Promise { `; if (!functionExists[0].exists) { - await reservation` + await activeReservation` CREATE FUNCTION update_settings_updated_at() RETURNS TRIGGER AS $$ BEGIN @@ -57,7 +57,7 @@ export async function createTable(reservation?: ReservedSQL): Promise { `; } - const triggerExists: { exists: boolean }[] = await reservation` + const triggerExists: { exists: boolean }[] = await activeReservation` SELECT EXISTS ( SELECT 1 FROM pg_trigger WHERE tgname = 'trigger_update_settings_updated_at' @@ -65,7 +65,7 @@ export async function createTable(reservation?: ReservedSQL): Promise { `; if (!triggerExists[0].exists) { - await reservation` + await activeReservation` CREATE TRIGGER trigger_update_settings_updated_at BEFORE UPDATE ON settings FOR EACH ROW @@ -74,7 +74,7 @@ export async function createTable(reservation?: ReservedSQL): Promise { } for (const setting of defaultSettings) { - await reservation` + await activeReservation` INSERT INTO settings ("key", "value") VALUES (${setting.key}, ${setting.value}) ON CONFLICT ("key") DO NOTHING; @@ -88,7 +88,7 @@ export async function createTable(reservation?: ReservedSQL): Promise { throw error; } finally { if (selfReservation) { - reservation.release(); + activeReservation.release(); } } } @@ -100,15 +100,15 @@ export async function getSetting( reservation?: ReservedSQL, ): Promise { let selfReservation = false; + const activeReservation: ReservedSQL = reservation ?? (await sql.reserve()); if (!reservation) { - reservation = await sql.reserve(); selfReservation = true; } try { const result: { value: string }[] = - await reservation`SELECT value FROM settings WHERE "key" = ${key};`; + await activeReservation`SELECT value FROM settings WHERE "key" = ${key};`; if (result.length === 0) { return null; @@ -120,7 +120,7 @@ export async function getSetting( throw error; } finally { if (selfReservation) { - reservation.release(); + activeReservation.release(); } } } @@ -131,14 +131,14 @@ export async function setSetting( reservation?: ReservedSQL, ): Promise { let selfReservation = false; + const activeReservation: ReservedSQL = reservation ?? (await sql.reserve()); if (!reservation) { - reservation = await sql.reserve(); selfReservation = true; } try { - await reservation` + await activeReservation` INSERT INTO settings ("key", "value", updated_at) VALUES (${key}, ${value}, NOW()) ON CONFLICT ("key") @@ -148,7 +148,7 @@ export async function setSetting( throw error; } finally { if (selfReservation) { - reservation.release(); + activeReservation.release(); } } } @@ -158,20 +158,20 @@ export async function deleteSetting( reservation?: ReservedSQL, ): Promise { let selfReservation = false; + const activeReservation: ReservedSQL = reservation ?? (await sql.reserve()); if (!reservation) { - reservation = await sql.reserve(); selfReservation = true; } try { - await reservation`DELETE FROM settings WHERE "key" = ${key};`; + await activeReservation`DELETE FROM settings WHERE "key" = ${key};`; } catch (error) { logger.error(["Could not delete the setting:", error as Error]); throw error; } finally { if (selfReservation) { - reservation.release(); + activeReservation.release(); } } } @@ -180,15 +180,15 @@ export async function getAllSettings( reservation?: ReservedSQL, ): Promise<{ key: string; value: string }[]> { let selfReservation = false; + const activeReservation: ReservedSQL = reservation ?? (await sql.reserve()); if (!reservation) { - reservation = await sql.reserve(); selfReservation = true; } try { const result: { key: string; value: string }[] = - await reservation`SELECT "key", "value" FROM settings;`; + await activeReservation`SELECT "key", "value" FROM settings;`; return result; } catch (error) { @@ -196,7 +196,7 @@ export async function getAllSettings( throw error; } finally { if (selfReservation) { - reservation.release(); + activeReservation.release(); } } } diff --git a/config/sql/users.ts b/config/sql/users.ts index c6da657..b346b30 100644 --- a/config/sql/users.ts +++ b/config/sql/users.ts @@ -5,14 +5,14 @@ export const order: number = 1; export async function createTable(reservation?: ReservedSQL): Promise { let selfReservation = false; + const activeReservation: ReservedSQL = reservation ?? (await sql.reserve()); if (!reservation) { - reservation = await sql.reserve(); selfReservation = true; } try { - await reservation` + await activeReservation` CREATE TABLE IF NOT EXISTS users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), authorization_token UUID NOT NULL UNIQUE DEFAULT gen_random_uuid(), @@ -32,7 +32,7 @@ export async function createTable(reservation?: ReservedSQL): Promise { throw error; } finally { if (selfReservation) { - reservation.release(); + activeReservation.release(); } } } diff --git a/package.json b/package.json index 813bf9e..cea47bb 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ }, "dependencies": { "ejs": "^3.1.10", + "eta": "^3.5.0", "exiftool-vendored": "^29.3.0", "fast-jwt": "6.0.1", "fluent-ffmpeg": "^2.1.3", diff --git a/public/css/auth.css b/public/css/auth.css deleted file mode 100644 index dfeb8c6..0000000 --- a/public/css/auth.css +++ /dev/null @@ -1,198 +0,0 @@ -.container { - display: flex; - justify-content: center; - align-items: center; - flex-direction: column; - height: 100vh; -} - -.content { - border: 1px solid var(--border); - background-color: var(--background-secondary); - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 2rem; - width: clamp(200px, 50%, 300px); -} - -.content form { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; -} - -.auth-container { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - min-height: 100vh; - background: linear-gradient( - 135deg, - rgba(31 30 30 / 90%) 0%, - rgba(45 45 45 / 90%) 100% - ); -} - -.auth-logo { - text-align: center; - margin-bottom: 2rem; -} - -.auth-logo h1 { - font-size: 2.5rem; - margin-bottom: 0.5rem; - color: var(--accent); -} - -.auth-logo p { - color: var(--text-secondary); - margin-top: 0.5rem; -} - -.auth-card { - background-color: var(--background-secondary); - border-radius: 8px; - box-shadow: var(--card-shadow); - width: 100%; - max-width: 400px; - overflow: hidden; - animation: fade-in 0.5s ease; -} - -.auth-header { - padding: 1.5rem; - text-align: center; - border-bottom: 1px solid var(--border); - background-color: rgba(0 0 0 / 10%); -} - -.auth-header h2 { - margin: 0; - font-size: 1.5rem; - color: var(--text); -} - -.auth-form { - padding: 1.5rem; -} - -.auth-form form { - display: flex; - flex-direction: column; - width: 100%; -} - -.auth-toggle { - text-align: center; - margin-top: 1.5rem; - font-size: 0.9rem; - color: var(--text-secondary); -} - -.form-footer { - display: flex; - justify-content: space-between; - align-items: center; - font-size: 0.9rem; - margin-top: 1rem; -} - -.form-footer a { - color: var(--accent); - text-decoration: none; -} - -.form-footer a:hover { - text-decoration: underline; -} - -.form-footer label { - display: flex; - align-items: center; - gap: 0.5rem; - cursor: pointer; - white-space: nowrap; -} - -.auth-form button { - margin-top: 0.5rem; - width: 100%; -} - -.password-group { - position: relative; - width: 100%; -} - -.password-wrapper { - position: relative; - width: 100%; -} - -.password-wrapper input { - width: 100%; - padding-right: 2rem; -} - -.toggle-password { - position: absolute; - right: 12px; - top: 50%; - transform: translateY(-50%); - cursor: pointer; - width: 20px; - height: 20px; - fill: var(--text-secondary); - transition: fill 0.2s ease; -} - -.toggle-password:hover { - fill: var(--text); -} - -.error-message { - color: var(--error); - background-color: rgb(237 66 69 / 10%); - padding: 0.75rem; - margin-bottom: 1.5rem; - border-radius: 4px; - display: none; - font-size: 0.9rem; - text-align: center; -} - -@keyframes fade-in { - from { - opacity: 0; - transform: translateY(-20px); - } - - to { - opacity: 1; - transform: translateY(0); - } -} - -.auth-link { - color: var(--accent); - text-decoration: none; - font-weight: bold; -} - -.auth-link:hover { - text-decoration: underline; -} - -@media (width <= 480px) { - .auth-card { - max-width: 100%; - } - - .auth-logo h1 { - font-size: 2rem; - } -} diff --git a/public/css/dashboard/index.css b/public/css/dashboard/index.css deleted file mode 100644 index c2feb7f..0000000 --- a/public/css/dashboard/index.css +++ /dev/null @@ -1,79 +0,0 @@ -body { - display: flex; - flex-direction: row; -} - -/* sidebar */ - -.sidebar { - background-color: var(--background-secondary); - width: 220px; - height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: space-between; - border-right: 1px solid var(--border); - box-sizing: border-box; -} - -.sidebar .actions { - display: flex; - flex-direction: column; - align-items: center; - width: 100%; - box-sizing: border-box; -} - -.sidebar .actions .action { - display: flex; - justify-content: flex-start; - gap: .5rem; - align-items: center; - padding: 1rem; - height: 3rem; - width: 100%; - transition: background-color 0.2s ease; - text-decoration: none; - color: var(--text); - box-sizing: border-box; -} - -.sidebar .actions .action svg { - width: 15px; - height: 15px; -} - -.sidebar .actions .action:hover { - background-color: var(--background); -} - -.sidebar .actions .action.active { - background-color: var(--background); -} - -.sidebar .actions .action.active:hover { - background-color: var(--background-secondary); -} - -.sidebar .user-area { - display: flex; - flex-direction: column; - align-items: center; - width: 100%; - border-top: 1px solid var(--border); - background-color: rgba(0 0 0 / 10%); -} - -.sidebar .user-area img { - width: 100px; - height: 100px; - border-radius: 50%; - object-fit: cover; -} - -.sidebar .user-area .username { - margin-top: 1rem; - font-weight: bold; - color: var(--text); -} diff --git a/public/css/global.css b/public/css/global.css index b51f16a..fe28f4b 100644 --- a/public/css/global.css +++ b/public/css/global.css @@ -46,82 +46,3 @@ body { background-color: var(--background); color: var(--text); } - -.container { - max-width: 1200px; - margin: 0 auto; - padding: 0 1rem; -} - -input, -button, -textarea, -select { - font-family: inherit; - font-size: 1rem; - border-radius: 4px; - transition: all 0.2s ease; -} - -button, -.button { - cursor: pointer; - padding: 0.75rem 1.5rem; - border: none; - background-color: var(--accent); - color: white; - font-weight: bold; - border-radius: 4px; - transition: background-color 0.2s ease; -} - -button:hover, -.button:hover { - background-color: var(--accent-hover); -} - -input, -textarea, -select { - padding: 0.75rem; - border: 1px solid var(--border); - background-color: var(--input-background); - color: var(--text); - width: 100%; - box-sizing: border-box; -} - -input:focus, -textarea:focus, -select:focus { - outline: none; - border-color: var(--accent); - box-shadow: 0 0 0 2px rgb(88 101 242 / 30%); -} - -.form-group { - margin-bottom: 1.5rem; - width: 100%; -} - -.form-group label { - display: block; - margin-bottom: 0.5rem; - font-weight: bold; - color: var(--text-secondary); -} - -svg { - fill: var(--svg-fill); - transition: fill 0.2s ease; -} - -svg.stroke-only { - fill: none; - stroke: var(--svg-fill); - stroke-width: 2; -} - -svg:hover { - fill: var(--accent); -} diff --git a/public/js/auth.js b/public/js/auth.js deleted file mode 100644 index 6b9c884..0000000 --- a/public/js/auth.js +++ /dev/null @@ -1,130 +0,0 @@ -const loginForm = document.getElementById("login-form"); -const registerForm = document.getElementById("register-form"); -const errorMessage = document.getElementById("error-message"); -const rememberMe = document.getElementById("remember-me"); -const emailInput = document.getElementById("email"); - -if (emailInput && localStorage.getItem("email")) { - emailInput.value = localStorage.getItem("email"); -} - -if (loginForm) { - loginForm.addEventListener("submit", async (e) => { - e.preventDefault(); - - const email = emailInput?.value.trim(); - const password = document.getElementById("password")?.value.trim(); - - if (!email || !password) { - if (errorMessage) { - errorMessage.style.display = "block"; - errorMessage.textContent = "Please enter both email and password."; - } - return; - } - - if (rememberMe?.checked) { - localStorage.setItem("email", email); - } else { - sessionStorage.setItem("email", email); - } - - try { - const response = await fetch("/api/auth/login", { - method: "POST", - headers: { "Content-Type": "application/json" }, - credentials: "same-origin", - body: JSON.stringify({ email, password }), - }); - - const data = await response.json(); - - if (data.success) { - window.location.href = "/dashboard"; - } else { - if (errorMessage) { - errorMessage.style.display = "block"; - errorMessage.textContent = - data.error || "Invalid email or password. Please try again."; - } - } - } catch (error) { - console.error("Login error:", error); - if (errorMessage) { - errorMessage.style.display = "block"; - errorMessage.textContent = "An error occurred. Please try again."; - } - } - }); -} else if (registerForm) { - registerForm.addEventListener("submit", async (e) => { - e.preventDefault(); - - const email = emailInput?.value.trim(); - const username = document.getElementById("username")?.value.trim(); - const password = document.getElementById("password")?.value.trim(); - const inviteCode = document.getElementById("invite-code")?.value.trim(); - - if (!email || !password) { - if (errorMessage) { - errorMessage.style.display = "block"; - errorMessage.textContent = "Please enter email, password."; - } - return; - } - - try { - const response = await fetch("/api/auth/register", { - method: "POST", - headers: { "Content-Type": "application/json" }, - credentials: "same-origin", - body: JSON.stringify({ - username, - email, - password, - invite: inviteCode, - }), - }); - - const data = await response.json(); - - if (data.success) { - window.location.href = "/dashboard"; - } else { - if (errorMessage) { - errorMessage.style.display = "block"; - - if (Array.isArray(data.errors)) { - errorMessage.innerHTML = data.errors - .map((err) => `

${err}

`) - .join(""); - } else { - errorMessage.textContent = - data.error || "An error occurred. Please try again."; - } - } - } - } catch (error) { - console.error("Register error:", error); - if (errorMessage) { - errorMessage.style.display = "block"; - errorMessage.textContent = "An error occurred. Please try again."; - } - } - }); -} - -const passwordInput = document.getElementById("password"); -const togglePassword = document.getElementById("toggle-password"); - -togglePassword.addEventListener("click", () => { - if (passwordInput.type === "password") { - passwordInput.type = "text"; - togglePassword.innerHTML = - ''; - } else { - passwordInput.type = "password"; - togglePassword.innerHTML = - ''; - } -}); diff --git a/public/js/dashboard/index.js b/public/js/dashboard/index.js deleted file mode 100644 index e69de29..0000000 diff --git a/src/helpers/auth.ts b/src/helpers/auth.ts index 8426b25..5df1a3f 100644 --- a/src/helpers/auth.ts +++ b/src/helpers/auth.ts @@ -7,6 +7,7 @@ export async function authByToken( reservation?: ReservedSQL, ): Promise { let selfReservation = false; + let activeReservation: ReservedSQL | undefined = reservation; const authorizationHeader: string | null = request.headers.get("Authorization"); @@ -17,14 +18,14 @@ export async function authByToken( const authorizationToken: string = authorizationHeader.slice(7).trim(); if (!authorizationToken || !isUUID(authorizationToken)) return null; - if (!reservation) { - reservation = await sql.reserve(); + if (!activeReservation) { + activeReservation = await sql.reserve(); selfReservation = true; } try { const result: User[] = - await reservation`SELECT * FROM users WHERE authorization_token = ${authorizationToken};`; + await activeReservation`SELECT * FROM users WHERE authorization_token = ${authorizationToken};`; if (result.length === 0) return null; @@ -44,7 +45,7 @@ export async function authByToken( return null; } finally { if (selfReservation) { - reservation.release(); + activeReservation.release(); } } } diff --git a/src/helpers/char.ts b/src/helpers/char.ts index 45837a4..2ef3e03 100644 --- a/src/helpers/char.ts +++ b/src/helpers/char.ts @@ -2,8 +2,8 @@ import { DateTime } from "luxon"; export function timestampToReadable(timestamp?: number): string { const date: Date = - timestamp && !isNaN(timestamp) ? new Date(timestamp) : new Date(); - if (isNaN(date.getTime())) return "Invalid Date"; + timestamp && !Number.isNaN(timestamp) ? new Date(timestamp) : new Date(); + if (Number.isNaN(date.getTime())) return "Invalid Date"; return date.toISOString().replace("T", " ").replace("Z", ""); } @@ -84,15 +84,13 @@ export function isValidTimezone(timezone: string): boolean { } export function generateRandomString(length?: number): string { - if (!length) { - length = length || Math.floor(Math.random() * 10) + 5; - } + const finalLength: number = length ?? Math.floor(Math.random() * 10) + 5; - const characters: string = + const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; let result = ""; - for (let i = 0; i < length; i++) { + for (let i = 0; i < finalLength; i++) { result += characters.charAt(Math.floor(Math.random() * characters.length)); } diff --git a/src/helpers/commands/clearTable.ts b/src/helpers/commands/clearTable.ts index cdb11a1..0cd5269 100644 --- a/src/helpers/commands/clearTable.ts +++ b/src/helpers/commands/clearTable.ts @@ -24,8 +24,7 @@ import { type ReservedSQL, sql } from "bun"; error.message.includes("foreign key constraint") ) { console.error( - `Could not clear table "${table}" due to foreign key constraints.\n` + - "Try using --cascade if you want to remove dependent records.", + `Could not clear table "${table}" due to foreign key constraints.\nTry using --cascade if you want to remove dependent records.`, ); } else { console.error("Could not clear table:", error); diff --git a/src/helpers/ejs.ts b/src/helpers/ejs.ts index 7ff667c..6544009 100644 --- a/src/helpers/ejs.ts +++ b/src/helpers/ejs.ts @@ -1,4 +1,4 @@ -import { resolve } from "path"; +import { resolve } from "node:path"; import { renderFile } from "ejs"; export async function renderEjsTemplate( diff --git a/src/helpers/logger.ts b/src/helpers/logger.ts index 7ebcf40..345fd75 100644 --- a/src/helpers/logger.ts +++ b/src/helpers/logger.ts @@ -1,13 +1,13 @@ -import type { Stats } from "fs"; +import type { Stats } from "node:fs"; import { type WriteStream, createWriteStream, existsSync, mkdirSync, statSync, -} from "fs"; -import { EOL } from "os"; -import { basename, join } from "path"; +} from "node:fs"; +import { EOL } from "node:os"; +import { basename, join } from "node:path"; import { environment } from "@config/environment"; import { timestampToReadable } from "@helpers/char"; diff --git a/src/helpers/redis.ts b/src/helpers/redis.ts index 3251320..a633580 100644 --- a/src/helpers/redis.ts +++ b/src/helpers/redis.ts @@ -86,12 +86,12 @@ class RedisJson { } return value; - } else if (type === "STRING") { + } + if (type === "STRING") { const value: string | null = await this.client.get(key); return value; - } else { - throw new Error(`Invalid type: ${type}`); } + throw new Error(`Invalid type: ${type}`); } catch (error) { logger.error(`Error getting value from Redis for key: ${key}`); logger.error(error as Error); diff --git a/src/helpers/sessions.ts b/src/helpers/sessions.ts index 20fa5c7..67d7d27 100644 --- a/src/helpers/sessions.ts +++ b/src/helpers/sessions.ts @@ -50,7 +50,7 @@ class SessionManager { const userSessions: string[] = await redis .getInstance() - .keys("session:*:" + token); + .keys(`session:*:${token}`); if (!userSessions.length) return null; const sessionData: unknown = await redis @@ -76,7 +76,7 @@ class SessionManager { const userSessions: string[] = await redis .getInstance() - .keys("session:*:" + token); + .keys(`session:*:${token}`); if (!userSessions.length) throw new Error("Session not found or expired"); const sessionKey: string = userSessions[0]; @@ -96,7 +96,7 @@ class SessionManager { public async verifySession(token: string): Promise { const userSessions: string[] = await redis .getInstance() - .keys("session:*:" + token); + .keys(`session:*:${token}`); if (!userSessions.length) throw new Error("Session not found or expired"); const sessionData: unknown = await redis @@ -122,7 +122,7 @@ class SessionManager { const userSessions: string[] = await redis .getInstance() - .keys("session:*:" + token); + .keys(`session:*:${token}`); if (!userSessions.length) return; await redis.getInstance().delete("JSON", userSessions[0]); diff --git a/src/helpers/workers/thumbnails.ts b/src/helpers/workers/thumbnails.ts index da2c845..d3f0d7d 100644 --- a/src/helpers/workers/thumbnails.ts +++ b/src/helpers/workers/thumbnails.ts @@ -1,11 +1,11 @@ -import { join, resolve } from "path"; +import { join, resolve } from "node:path"; import { dataType } from "@config/environment.ts"; import { logger } from "@helpers/logger.ts"; import { type BunFile, s3, sql } from "bun"; import ffmpeg from "fluent-ffmpeg"; import imageThumbnail from "image-thumbnail"; -declare var self: Worker; +declare let self: Worker; async function generateVideoThumbnail( filePath: string, @@ -47,37 +47,34 @@ async function generateImageThumbnail( thumbnailPath: string, ): Promise { return new Promise( - async ( + ( resolve: (value: ArrayBuffer) => void, reject: (reason: Error) => void, - ) => { - try { - const options: { - responseType: "buffer"; - height: number; - jpegOptions: { - force: boolean; - quality: number; - }; - } = { - height: 320, - responseType: "buffer", - jpegOptions: { - force: true, - quality: 60, - }, - }; + ): void => { + const options = { + height: 320, + responseType: "buffer" as const, + jpegOptions: { + force: true, + quality: 60, + }, + }; - const thumbnailBuffer: Buffer = await imageThumbnail(filePath, options); - - await Bun.write(thumbnailPath, thumbnailBuffer.buffer); - resolve(await Bun.file(thumbnailPath).arrayBuffer()); - - await Bun.file(filePath).unlink(); - await Bun.file(thumbnailPath).unlink(); - } catch (error) { - reject(error as Error); - } + imageThumbnail(filePath, options) + .then( + (thumbnailBuffer: Buffer): Promise => + Bun.write(thumbnailPath, thumbnailBuffer.buffer).then( + (): Promise => Bun.file(thumbnailPath).arrayBuffer(), + ), + ) + .then((arrayBuffer: ArrayBuffer) => { + resolve(arrayBuffer); + return Promise.all([ + Bun.file(filePath).unlink(), + Bun.file(thumbnailPath).unlink(), + ]); + }) + .catch(reject); }, ); } diff --git a/src/index.ts b/src/index.ts index 495815c..923f860 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,9 @@ -import { existsSync, mkdirSync } from "fs"; -import { resolve } from "path"; +import { existsSync, mkdirSync } from "node:fs"; +import { readdir } from "node:fs/promises"; +import { resolve } from "node:path"; import { dataType } from "@config/environment"; import { logger } from "@helpers/logger"; import { type ReservedSQL, s3, sql } from "bun"; -import { readdir } from "fs/promises"; import { serverHandler } from "@/server"; @@ -39,59 +39,52 @@ async function initializeDatabase(): Promise { async function main(): Promise { try { - try { - await sql`SELECT 1;`; + await sql`SELECT 1;`; - logger.info([ - "Connected to PostgreSQL on", - `${process.env.PGHOST}:${process.env.PGPORT}`, - ]); - } catch (error) { - logger.error([ - "Could not establish a connection to PostgreSQL:", - error as Error, - ]); - process.exit(1); - } + logger.info([ + "Connected to PostgreSQL on", + `${process.env.PGHOST}:${process.env.PGPORT}`, + ]); + } catch (error) { + logger.error([ + "Could not establish a connection to PostgreSQL:", + error as Error, + ]); + process.exit(1); + } - if (dataType.type === "local" && dataType.path) { - if (!existsSync(dataType.path)) { - try { - mkdirSync(dataType.path); - } catch (error) { - logger.error([ - "Could not create datasource local directory", - error as Error, - ]); - process.exit(1); - } - } - - logger.info(["Using local datasource directory", `${dataType.path}`]); - } else { + if (dataType.type === "local" && dataType.path) { + if (!existsSync(dataType.path)) { try { - await s3.write("test", "test"); - await s3.delete("test"); - - logger.info([ - "Connected to S3 with bucket", - `${process.env.S3_BUCKET}`, - ]); + mkdirSync(dataType.path); } catch (error) { logger.error([ - "Could not establish a connection to S3 bucket:", + "Could not create datasource local directory", error as Error, ]); process.exit(1); } } - await redis.initialize(); - serverHandler.initialize(); - await initializeDatabase(); - } catch (error) { - throw error; + logger.info(["Using local datasource directory", `${dataType.path}`]); + } else { + try { + await s3.write("test", "test"); + await s3.delete("test"); + + logger.info(["Connected to S3 with bucket", `${process.env.S3_BUCKET}`]); + } catch (error) { + logger.error([ + "Could not establish a connection to S3 bucket:", + error as Error, + ]); + process.exit(1); + } } + + await redis.initialize(); + serverHandler.initialize(); + await initializeDatabase(); } main().catch((error: Error) => { diff --git a/src/routes/api/auth/login.ts b/src/routes/api/auth/login.ts index 90d7f32..6ede556 100644 --- a/src/routes/api/auth/login.ts +++ b/src/routes/api/auth/login.ts @@ -66,11 +66,11 @@ async function handler( password ? { check: isValidPassword(password), field: "Password" } : null, ].filter(Boolean) as UserValidation[]; - validations.forEach(({ check }: UserValidation): void => { + for (const { check } of validations) { if (!check.valid && check.error) { errors.push(check.error); } - }); + } if (!username && !email) { errors.push("Either a username or an email is required."); diff --git a/src/routes/api/auth/register.ts b/src/routes/api/auth/register.ts index 065858c..22b6307 100644 --- a/src/routes/api/auth/register.ts +++ b/src/routes/api/auth/register.ts @@ -49,11 +49,11 @@ async function handler( { check: isValidPassword(password), field: "Password" }, ]; - validations.forEach(({ check }: UserValidation): void => { + for (const { check } of validations) { if (!check.valid && check.error) { errors.push(check.error); } - }); + } const normalizedUsername: string = username.normalize("NFC"); const reservation: ReservedSQL = await sql.reserve(); diff --git a/src/routes/api/files/delete[query].ts b/src/routes/api/files/delete[query].ts index 8a9dac6..6469bc6 100644 --- a/src/routes/api/files/delete[query].ts +++ b/src/routes/api/files/delete[query].ts @@ -1,4 +1,4 @@ -import { resolve } from "path"; +import { resolve } from "node:path"; import { dataType } from "@config/environment"; import { type SQLQuery, s3, sql } from "bun"; diff --git a/src/routes/api/files/upload.ts b/src/routes/api/files/upload.ts index faf4468..d9d9f89 100644 --- a/src/routes/api/files/upload.ts +++ b/src/routes/api/files/upload.ts @@ -1,4 +1,4 @@ -import { resolve } from "path"; +import { resolve } from "node:path"; import { dataType } from "@config/environment"; import { getSetting } from "@config/sql/settings"; import { @@ -174,13 +174,9 @@ async function processFile( let hashedPassword: string | null = null; if (user_provided_password) { - try { - hashedPassword = await bunPassword.hash(user_provided_password, { - algorithm: "argon2id", - }); - } catch (error) { - throw error; - } + hashedPassword = await bunPassword.hash(user_provided_password, { + algorithm: "argon2id", + }); } const randomUUID: string = randomUUIDv7(); @@ -217,7 +213,7 @@ async function processFile( // ? Should work not sure about non-english characters const sanitizedFileName: string = rawName .normalize("NFD") - .replace(/[\u0300-\u036f]/g, "") + .replace(/\p{Mn}/gu, "") .replace(/[^a-zA-Z0-9._-]/g, "_") .toLowerCase(); @@ -276,7 +272,7 @@ async function processFile( return; } } else { - path = "/uploads/" + uuidWithExtension; + path = `/uploads/${uuidWithExtension}`; try { await s3.write(path, fileBuffer); @@ -321,7 +317,7 @@ async function processFile( return; } - if (uploadEntry.password) delete uploadEntry.password; + if (uploadEntry.password) uploadEntry.password = undefined; uploadEntry.url = `${userHeaderOptions.domain}/raw/${uploadEntry.name}`; successfulFiles.push(uploadEntry); diff --git a/src/routes/api/user/avatar/delete.ts b/src/routes/api/user/avatar/delete.ts index 2c20c9a..9ea6295 100644 --- a/src/routes/api/user/avatar/delete.ts +++ b/src/routes/api/user/avatar/delete.ts @@ -1,4 +1,4 @@ -import { resolve } from "path"; +import { resolve } from "node:path"; import { dataType } from "@config/environment"; import { s3, sql } from "bun"; @@ -113,16 +113,15 @@ async function handler(request: ExtendedRequest): Promise { }, }, ); - } else { - return Response.json( - { - success: true, - code: 200, - message: "Avatar deleted", - }, - { status: 200 }, - ); } + return Response.json( + { + success: true, + code: 200, + message: "Avatar deleted", + }, + { status: 200 }, + ); } catch (error) { logger.error(["Error processing delete request:", error as Error]); diff --git a/src/routes/api/user/avatar/set.ts b/src/routes/api/user/avatar/set.ts index dd1e463..0b9c90f 100644 --- a/src/routes/api/user/avatar/set.ts +++ b/src/routes/api/user/avatar/set.ts @@ -1,4 +1,4 @@ -import { resolve } from "path"; +import { resolve } from "node:path"; import { dataType } from "@config/environment"; import { isValidTypeOrExtension } from "@config/sql/avatars"; import { getSetting } from "@config/sql/settings"; @@ -201,17 +201,16 @@ async function handler( }, }, ); - } else { - return Response.json( - { - success: true, - code: 200, - message: "Avatar uploaded", - url: message, - }, - { status: 200 }, - ); } + return Response.json( + { + success: true, + code: 200, + message: "Avatar uploaded", + url: message, + }, + { status: 200 }, + ); } catch (error) { logger.error(["Error processing file:", error as Error]); diff --git a/src/routes/api/user/info[query].ts b/src/routes/api/user/info[query].ts index af35ca7..62ff923 100644 --- a/src/routes/api/user/info[query].ts +++ b/src/routes/api/user/info[query].ts @@ -110,9 +110,9 @@ async function handler(request: ExtendedRequest): Promise { ); } - delete user.password; - delete user.authorization_token; - if (!isSelf) delete user.email; + user.password = undefined; + user.authorization_token = undefined; + if (!isSelf) user.email = undefined; user.roles = user.roles ? user.roles[0].split(",") : []; diff --git a/src/routes/auth/login.ts b/src/routes/auth/login.ts deleted file mode 100644 index 0c24f39..0000000 --- a/src/routes/auth/login.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { getSetting } from "@config/sql/settings"; -import { renderEjsTemplate } from "@helpers/ejs"; - -const routeDef: RouteDef = { - method: "GET", - accepts: "*/*", - returns: "text/html", -}; - -async function handler(request: ExtendedRequest): Promise { - if (request.session) return Response.redirect("/"); - - const instanceName: string = - (await getSetting("instance_name")) || "Unnamed Instance"; - - const ejsTemplateData: EjsTemplateData = { - title: `Login - ${instanceName}`, - instance_name: instanceName, - }; - - return await renderEjsTemplate("auth/login", ejsTemplateData); -} - -export { handler, routeDef }; diff --git a/src/routes/auth/register.ts b/src/routes/auth/register.ts deleted file mode 100644 index d5be754..0000000 --- a/src/routes/auth/register.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { getSetting } from "@config/sql/settings"; -import { renderEjsTemplate } from "@helpers/ejs"; -import { type ReservedSQL, sql } from "bun"; - -import { logger } from "@/helpers/logger"; - -const routeDef: RouteDef = { - method: "GET", - accepts: "*/*", - returns: "text/html", -}; - -async function handler(request: ExtendedRequest): Promise { - if (request.session) return Response.redirect("/"); - - const reservation: ReservedSQL = await sql.reserve(); - try { - const [firstUser] = await sql`SELECT COUNT(*) FROM users`; - - const instanceName: string = - (await getSetting("instance_name", reservation)) || "Unnamed Instance"; - const requiresInvite: boolean = - (await getSetting("enable_invitations", reservation)) === "true" && - firstUser.count !== "0"; - - const ejsTemplateData: EjsTemplateData = { - title: `Register - ${instanceName}`, - instance_name: instanceName, - requires_invite: requiresInvite, - }; - - return await renderEjsTemplate("auth/register", ejsTemplateData); - } catch (error) { - logger.error(["Error rendering register page", error as Error]); - return Response.redirect("/"); - } finally { - reservation.release(); - } -} - -export { handler, routeDef }; diff --git a/src/routes/dashboard/index.ts b/src/routes/dashboard/index.ts deleted file mode 100644 index 30e2ddf..0000000 --- a/src/routes/dashboard/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { renderEjsTemplate } from "@helpers/ejs"; - -const routeDef: RouteDef = { - method: "GET", - accepts: "*/*", - returns: "text/html", -}; - -async function handler(request: ExtendedRequest): Promise { - // if (!request.session) { - // return Response.redirect("/auth/login"); - // } - - const ejsTemplateData: EjsTemplateData = { - title: "Hello, World!", - active: "dashboard", - }; - - return await renderEjsTemplate("dashboard/index.ejs", ejsTemplateData); -} - -export { handler, routeDef }; diff --git a/src/routes/raw/[query].ts b/src/routes/raw/[query].ts index de4dec6..b6ada16 100644 --- a/src/routes/raw/[query].ts +++ b/src/routes/raw/[query].ts @@ -1,4 +1,4 @@ -import { resolve } from "path"; +import { resolve } from "node:path"; import { dataType } from "@config/environment"; import { type BunFile, type ReservedSQL, sql } from "bun"; @@ -115,7 +115,7 @@ async function handler(request: ExtendedRequest): Promise { } if (json === "true" || json === "1") { - delete fileData.password; + fileData.password = undefined; fileData.tags = fileData.tags = fileData.tags[0]?.trim() ? fileData.tags[0].split(",").filter((tag: string) => tag.trim()) : []; diff --git a/src/routes/user/avatar/[user].ts b/src/routes/user/avatar/[user].ts index 919b0fc..d6d06c7 100644 --- a/src/routes/user/avatar/[user].ts +++ b/src/routes/user/avatar/[user].ts @@ -1,4 +1,4 @@ -import { resolve } from "path"; +import { resolve } from "node:path"; import { dataType } from "@config/environment"; import { isValidUsername } from "@config/sql/users"; import { type BunFile, type ReservedSQL, sql } from "bun"; diff --git a/src/server.ts b/src/server.ts index 7f3350a..15471ee 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,4 +1,4 @@ -import { resolve } from "path"; +import { resolve } from "node:path"; import { environment } from "@config/environment"; import { logger } from "@helpers/logger"; import { @@ -41,10 +41,14 @@ class ServerHandler { maxRequestBodySize: 10 * 1024 * 1024 * 1024, // 10GB ? will be changed to env var soon }); - logger.info( - `Server running at http://${server.hostname}:${server.port}`, - true, - ); + const accessUrls: string[] = [ + `http://${server.hostname}:${server.port}`, + `http://localhost:${server.port}`, + `http://127.0.0.1:${server.port}`, + ]; + + logger.info(`Server running at ${accessUrls[0]}`); + logger.info(`Access via: ${accessUrls[1]} or ${accessUrls[2]}`, true); this.logRoutes(); } @@ -82,10 +86,9 @@ class ServerHandler { return new Response(fileContent, { headers: { "Content-Type": contentType }, }); - } else { - logger.warn(`File not found: ${filePath}`); - return new Response("Not Found", { status: 404 }); } + logger.warn(`File not found: ${filePath}`); + return new Response("Not Found", { status: 404 }); } catch (error) { logger.error([`Error serving static file: ${pathname}`, error as Error]); return new Response("Internal Server Error", { status: 500 }); @@ -93,10 +96,11 @@ class ServerHandler { } private async handleRequest( - request: ExtendedRequest, + request: Request, server: BunServer, ): Promise { - request.startPerf = performance.now(); + const extendedRequest: ExtendedRequest = request as ExtendedRequest; + extendedRequest.startPerf = performance.now(); const pathname: string = new URL(request.url).pathname; if (pathname.startsWith("/public") || pathname === "/favicon.ico") { @@ -185,12 +189,12 @@ class ServerHandler { { status: 406 }, ); } else { - request.params = params; - request.query = query; - request.actualContentType = actualContentType; + extendedRequest.params = params; + extendedRequest.query = query; + extendedRequest.actualContentType = actualContentType; - request.session = - (await authByToken(request)) || + extendedRequest.session = + (await authByToken(extendedRequest)) || (await sessionManager.getSession(request)); response = await routeModule.handler(request, requestBody, server); @@ -242,7 +246,7 @@ class ServerHandler { `(${response.status})`, [ request.url, - `${(performance.now() - request.startPerf).toFixed(2)}ms`, + `${(performance.now() - extendedRequest.startPerf).toFixed(2)}ms`, ip || "unknown", ], "90", diff --git a/src/views/auth/login.ejs b/src/views/auth/login.ejs deleted file mode 100644 index 2715879..0000000 --- a/src/views/auth/login.ejs +++ /dev/null @@ -1,25 +0,0 @@ - - - - - <%- include("../global", { styles: ["auth"], scripts: ["auth"] }) %> - - - -
- - -
-
-

Welcome Back

-
- - <%- include("../partials/authForm", { pageType: "login" }) %> -
-
- - - diff --git a/src/views/auth/register.ejs b/src/views/auth/register.ejs deleted file mode 100644 index b1aa386..0000000 --- a/src/views/auth/register.ejs +++ /dev/null @@ -1,25 +0,0 @@ - - - - - <%- include("../global", { styles: ["auth"], scripts: ["auth"] }) %> - - - -
- - -
-
-

Join Us

-
- - <%- include("../partials/authForm", { pageType: "register" }) %> -
-
- - - diff --git a/src/views/dashboard/index.ejs b/src/views/dashboard/index.ejs deleted file mode 100644 index 3f2b203..0000000 --- a/src/views/dashboard/index.ejs +++ /dev/null @@ -1,13 +0,0 @@ - - - - <%- include("../global", { styles: ["dashboard/index"], scripts: [] }) %> - - - <%- include("../partials/sidebar") %> -
-

Dashboard

-

Welcome to the dashboard!

-
- - diff --git a/src/views/partials/authForm.ejs b/src/views/partials/authForm.ejs deleted file mode 100644 index 9f8a477..0000000 --- a/src/views/partials/authForm.ejs +++ /dev/null @@ -1,60 +0,0 @@ -
-
- <%= pageType==="register" ? "Registration failed. Please try again." - : "Invalid email or password. Please try again." %> -
- -
" class="form"> - <% if (pageType==="register") { %> -
- - -
- - <% if (requires_invite === true) { %> -
- - -
- <% } %> - <% } %> - -
- - -
- -
- -
- - - - -
-
- - <% if (pageType !=="register" ) { %> - - <% } %> - - -
- -
-

- <%= pageType==="register" ? "Already have an account?" : "Don't have an account?" %> - - <%= pageType==="register" ? "Login" : "Register" %> - -

-
-
diff --git a/src/views/partials/header.ejs b/src/views/partials/header.ejs deleted file mode 100644 index e69de29..0000000 diff --git a/src/views/partials/sidebar.ejs b/src/views/partials/sidebar.ejs deleted file mode 100644 index 0c8a2fa..0000000 --- a/src/views/partials/sidebar.ejs +++ /dev/null @@ -1,12 +0,0 @@ -