after biome unsafe, restart frontend, add superadmin

This commit is contained in:
creations 2025-04-13 10:22:47 -04:00
parent 25fcd99acf
commit c02b519eee
Signed by: creations
GPG key ID: 8F553AA4320FC711
41 changed files with 189 additions and 910 deletions

View file

@ -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),

View file

@ -5,14 +5,14 @@ export const order: number = 6;
export async function createTable(reservation?: ReservedSQL): Promise<void> {
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<void> {
throw error;
} finally {
if (selfReservation) {
reservation.release();
activeReservation.release();
}
}
}

View file

@ -5,14 +5,14 @@ export const order: number = 5;
export async function createTable(reservation?: ReservedSQL): Promise<void> {
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<void> {
);
`;
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<void> {
`;
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<void> {
`;
}
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<void> {
`;
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<void> {
throw error;
} finally {
if (selfReservation) {
reservation.release();
activeReservation.release();
}
}
}

View file

@ -5,14 +5,14 @@ export const order: number = 4;
export async function createTable(reservation?: ReservedSQL): Promise<void> {
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<void> {
);
`;
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<void> {
`;
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<void> {
`;
}
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<void> {
`;
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<void> {
throw error;
} finally {
if (selfReservation) {
reservation.release();
activeReservation.release();
}
}
}

View file

@ -5,14 +5,14 @@ export const order: number = 3;
export async function createTable(reservation?: ReservedSQL): Promise<void> {
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<void> {
throw error;
} finally {
if (selfReservation) {
reservation.release();
activeReservation.release();
}
}
}

View file

@ -21,14 +21,14 @@ const defaultSettings: Setting[] = [
export async function createTable(reservation?: ReservedSQL): Promise<void> {
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<void> {
);
`;
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<void> {
`;
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<void> {
`;
}
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<void> {
`;
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<void> {
}
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<void> {
throw error;
} finally {
if (selfReservation) {
reservation.release();
activeReservation.release();
}
}
}
@ -100,15 +100,15 @@ export async function getSetting(
reservation?: ReservedSQL,
): Promise<string | null> {
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<void> {
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<void> {
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();
}
}
}

View file

@ -5,14 +5,14 @@ export const order: number = 1;
export async function createTable(reservation?: ReservedSQL): Promise<void> {
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<void> {
throw error;
} finally {
if (selfReservation) {
reservation.release();
activeReservation.release();
}
}
}

View file

@ -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",

View file

@ -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;
}
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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) => `<p>${err}</p>`)
.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 =
'<path d="M12 4.5c-5 0-9.27 3.11-11 7.5 1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zm0 13c-3.03 0-5.5-2.47-5.5-5.5s2.47-5.5 5.5-5.5 5.5 2.47 5.5 5.5-2.47 5.5-5.5 5.5z"/>';
} else {
passwordInput.type = "password";
togglePassword.innerHTML =
'<path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zm0 13c-3.03 0-5.5-2.47-5.5-5.5s2.47-5.5 5.5-5.5 5.5 2.47 5.5 5.5-2.47 5.5-5.5 5.5zm0-9a3.5 3.5 0 100 7 3.5 3.5 0 000-7z"/>';
}
});

View file

@ -7,6 +7,7 @@ export async function authByToken(
reservation?: ReservedSQL,
): Promise<ApiUserSession | null> {
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();
}
}
}

View file

@ -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));
}

View file

@ -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);

View file

@ -1,4 +1,4 @@
import { resolve } from "path";
import { resolve } from "node:path";
import { renderFile } from "ejs";
export async function renderEjsTemplate(

View file

@ -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";

View file

@ -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);

View file

@ -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<UserSession> {
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]);

View file

@ -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<ArrayBuffer> {
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<ArrayBuffer> =>
Bun.write(thumbnailPath, thumbnailBuffer.buffer).then(
(): Promise<ArrayBuffer> => Bun.file(thumbnailPath).arrayBuffer(),
),
)
.then((arrayBuffer: ArrayBuffer) => {
resolve(arrayBuffer);
return Promise.all([
Bun.file(filePath).unlink(),
Bun.file(thumbnailPath).unlink(),
]);
})
.catch(reject);
},
);
}

View file

@ -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<void> {
async function main(): Promise<void> {
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) => {

View file

@ -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.");

View file

@ -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();

View file

@ -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";

View file

@ -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);

View file

@ -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<Response> {
},
},
);
} 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]);

View file

@ -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]);

View file

@ -110,9 +110,9 @@ async function handler(request: ExtendedRequest): Promise<Response> {
);
}
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(",") : [];

View file

@ -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<Response> {
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 };

View file

@ -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<Response> {
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 };

View file

@ -1,22 +0,0 @@
import { renderEjsTemplate } from "@helpers/ejs";
const routeDef: RouteDef = {
method: "GET",
accepts: "*/*",
returns: "text/html",
};
async function handler(request: ExtendedRequest): Promise<Response> {
// 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 };

View file

@ -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<Response> {
}
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())
: [];

View file

@ -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";

View file

@ -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<Response> {
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",

View file

@ -1,25 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<%- include("../global", { styles: ["auth"], scripts: ["auth"] }) %>
</head>
<body>
<div class="auth-container">
<div class="auth-logo">
<h1><%= instance_name %></h1>
<p>Sign in to your account</p>
</div>
<div class="auth-card">
<div class="auth-header">
<h2>Welcome Back</h2>
</div>
<%- include("../partials/authForm", { pageType: "login" }) %>
</div>
</div>
</body>
</html>

View file

@ -1,25 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<%- include("../global", { styles: ["auth"], scripts: ["auth"] }) %>
</head>
<body>
<div class="auth-container">
<div class="auth-logo">
<h1><%= instance_name %></h1>
<p>Create your account</p>
</div>
<div class="auth-card">
<div class="auth-header">
<h2>Join Us</h2>
</div>
<%- include("../partials/authForm", { pageType: "register" }) %>
</div>
</div>
</body>
</html>

View file

@ -1,13 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<%- include("../global", { styles: ["dashboard/index"], scripts: [] }) %>
</head>
<body>
<%- include("../partials/sidebar") %>
<div class="content">
<h1>Dashboard</h1>
<p>Welcome to the dashboard!</p>
</div>
</body>
</html>

View file

@ -1,60 +0,0 @@
<div class="auth-form">
<div class="error-message" id="error-message">
<%= pageType==="register" ? "Registration failed. Please try again."
: "Invalid email or password. Please try again." %>
</div>
<form id="<%= pageType === "register" ? "register-form" : "login-form" %>" class="form">
<% if (pageType==="register") { %>
<div class="form-group">
<label for="username">Username</label>
<input type="text" name="username" id="username" required placeholder="Enter your username">
</div>
<% if (requires_invite === true) { %>
<div class="form-group">
<label for="invite">Invite Code</label>
<input type="text" name="invite" id="invite-code" required placeholder="Enter your invite code">
</div>
<% } %>
<% } %>
<div class="form-group">
<label for="email">Email</label>
<input type="email" name="email" id="email" required placeholder="Enter your email">
</div>
<div class="form-group password-group">
<label for="password">Password</label>
<div class="password-wrapper">
<input type="password" name="password" id="password" required placeholder="Enter your password">
<svg id="toggle-password" class="toggle-password" viewBox="0 0 24 24">
<path
d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zm0 13c-3.03 0-5.5-2.47-5.5-5.5s2.47-5.5 5.5-5.5 5.5 2.47 5.5 5.5-2.47 5.5-5.5 5.5zm0-9a3.5 3.5 0 100 7 3.5 3.5 0 000-7z" />
</svg>
</div>
</div>
<% if (pageType !=="register" ) { %>
<div class="form-footer">
<label>
<input type="checkbox" name="remember" id="remember-me">Remember me
</label>
<a href="/auth/forgot-password">Forgot password?</a>
</div>
<% } %>
<button type="submit">
<%= pageType==="register" ? "Register" : "Login" %>
</button>
</form>
<div class="auth-toggle">
<p>
<%= pageType==="register" ? "Already have an account?" : "Don't have an account?" %>
<a href="<%= pageType === 'register' ? '/auth/login' : '/auth/register' %>" class="auth-link">
<%= pageType==="register" ? "Login" : "Register" %>
</a>
</p>
</div>
</div>

View file

@ -1,12 +0,0 @@
<div class="sidebar">
<div class="actions">
<a href="/dashboard" class="action <%- active === 'dashboard' ? 'active' : '' %>">
<svg fill="" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" stroke=""><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M31.772 16.043l-15.012-15.724c-0.189-0.197-0.449-0.307-0.721-0.307s-0.533 0.111-0.722 0.307l-15.089 15.724c-0.383 0.398-0.369 1.031 0.029 1.414 0.399 0.382 1.031 0.371 1.414-0.029l1.344-1.401v14.963c0 0.552 0.448 1 1 1h6.986c0.551 0 0.998-0.445 1-0.997l0.031-9.989h7.969v9.986c0 0.552 0.448 1 1 1h6.983c0.552 0 1-0.448 1-1v-14.968l1.343 1.407c0.197 0.204 0.459 0.308 0.722 0.308 0.249 0 0.499-0.092 0.692-0.279 0.398-0.382 0.411-1.015 0.029-1.413zM26.985 14.213v15.776h-4.983v-9.986c0-0.552-0.448-1-1-1h-9.965c-0.551 0-0.998 0.445-1 0.997l-0.031 9.989h-4.989v-15.777c0-0.082-0.013-0.162-0.032-0.239l11.055-11.52 10.982 11.507c-0.021 0.081-0.036 0.165-0.036 0.252z"></path> </g></svg>
<span>Dashboard</span>
</a>
</div>
<div class="user-area">
<img src="https://cdn5.vectorstock.com/i/1000x1000/08/19/gray-photo-placeholder-icon-design-ui-vector-35850819.jpg" alt="User avatar">
<div class="username">John Doe</div>
</div>
</div>