commit a081f51cc6398ea591399c524ca43db107f22eb4
Author: Maki
Date: Sun Jan 4 18:00:24 2026 +0800
Initial commit
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..fdb021d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,14 @@
+/node_modules
+/dist
+/build
+
+package-lock.json
+
+/pvfiles/**/**.log
+/pvfiles/**/**.log.acck
+
+/output
+
+/uploads
+files.db
+users.db
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..a0bd6ac
--- /dev/null
+++ b/README.md
@@ -0,0 +1,8 @@
+# [jtw-storage-site](#jtw-storage-site)
+express storage site
+
+### Requirements/Prerequisites
+- Node.js 18^
+- Good internet
+
+Use `reg ` to create accounts. This application may not always be safe.
\ No newline at end of file
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..7bbfffc
--- /dev/null
+++ b/package.json
@@ -0,0 +1,67 @@
+{
+ "name": "express-jtw-update-server",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "bin": "build/src/index.js",
+ "dependencies": {
+ "bcrypt": "^6.0.0",
+ "child_process": "^1.0.2",
+ "cli-color": "^2.0.3",
+ "colorts": "^0.1.63",
+ "copyfiles": "^2.4.1",
+ "dotenv": "^16.4.1",
+ "express": "^4.19.2",
+ "express-error-handler": "^1.1.0",
+ "express-session": "^1.18.2",
+ "luxon": "^3.7.2",
+ "moment-timezone": "^0.5.44",
+ "morgan": "^1.10.0",
+ "multer": "^2.0.2",
+ "nodemon": "^3.1.4",
+ "rimraf": "^6.0.1",
+ "serve-index": "^1.9.1",
+ "socket.io": "^4.7.4",
+ "sqlite3": "^5.1.7",
+ "timers-ext": "^0.1.7",
+ "toidentifier": "^1.0.1",
+ "tree-kill": "^1.2.2",
+ "ts-node": "^10.9.2",
+ "type": "^1.2.0",
+ "type-is": "^1.6.18",
+ "typescript": "^5.3.3",
+ "undici-types": "^5.26.5",
+ "unpipe": "^1.0.0",
+ "untildify": "^4.0.0",
+ "util-deprecate": "^1.0.2",
+ "utils-merge": "^1.0.1",
+ "vary": "^1.1.2",
+ "which": "^2.0.2",
+ "wrap-ansi": "^8.1.0",
+ "wrappy": "^1.0.2",
+ "xtend": "^4.0.2",
+ "y18n": "^5.0.8",
+ "yargs": "^16.2.0",
+ "yargs-parser": "^20.2.9"
+ },
+ "scripts": {
+ "clean": "rimraf output/",
+ "d": "ts-node src/index.ts",
+ "build": "npm run clean && tsc && copyfiles .env package.json output/",
+ "quick": "npm run clean && tsc && node ./output/src/index.js"
+ },
+ "author": "",
+ "license": "ISC",
+ "devDependencies": {
+ "@types/bcrypt": "^6.0.0",
+ "@types/cli-color": "^2.0.6",
+ "@types/express": "^4.17.21",
+ "@types/express-session": "^1.18.2",
+ "@types/geoip-lite": "^1.4.4",
+ "@types/ip": "^1.1.3",
+ "@types/luxon": "^3.7.1",
+ "@types/morgan": "^1.9.9",
+ "@types/multer": "^2.0.0",
+ "@types/ping": "^0.4.4"
+ }
+}
diff --git a/src/cmd/clear.ts b/src/cmd/clear.ts
new file mode 100644
index 0000000..e706919
--- /dev/null
+++ b/src/cmd/clear.ts
@@ -0,0 +1,14 @@
+import { ConsoleCommandData } from "../types/Command"
+
+export const metadata: ConsoleCommandData = {
+ name: 'clear',
+ description: "Clears the console.",
+ usage: false,
+ aliases: ["cls"]
+}
+
+export function Main(args: Array): string {
+ console.clear()
+
+ return "Cleared console."
+}
\ No newline at end of file
diff --git a/src/cmd/delete.ts b/src/cmd/delete.ts
new file mode 100644
index 0000000..95d16d7
--- /dev/null
+++ b/src/cmd/delete.ts
@@ -0,0 +1,81 @@
+import { ConsoleCommandData } from "../types/Command"
+import bcrypt from "bcrypt"
+import { db, db_file } from "../util/database"
+import fs from "fs"
+import path from "path"
+
+export const metadata: ConsoleCommandData = {
+ name: "delete",
+ description: "Deletes a user and all their files.",
+ usage: "delete ",
+ aliases: [ "del" ]
+}
+
+export async function Main(args: Array): Promise {
+ if (args.length < 1)
+ return "Missing username. Use /help delete for help with this command."
+
+ const username = args[0]
+
+ return new Promise((resolve) => {
+ db.get(
+ `SELECT * FROM users WHERE username = ?`,
+ [username],
+ (err, user: any) => {
+ if (err) {
+ return resolve("Database error while looking up user.")
+ }
+
+ if (!user) {
+ return resolve("User not found.")
+ }
+
+ const userId = user.id
+
+ db_file.all(
+ `SELECT stored_name FROM files WHERE user_id = ?`,
+ [userId],
+ (err, rows) => {
+ if (err) {
+ return resolve("Database error fetching user files.")
+ }
+
+ // 2a) Remove files from disk
+ rows.forEach((f: any) => {
+ const filePath = path.join("uploads", f.stored_name)
+ if (fs.existsSync(filePath)) {
+ fs.unlink(filePath, (fsErr) => {
+ if (fsErr) {
+ console.error(`Failed to delete file: ${filePath}`, fsErr)
+ }
+ })
+ }
+ })
+
+ db_file.run(
+ `DELETE FROM files WHERE user_id = ?`,
+ [userId],
+ (fileDelErr: any) => {
+ if (fileDelErr) {
+ return resolve("Error deleting file records.")
+ }
+
+ db.run(
+ `DELETE FROM users WHERE username = ?`,
+ [username],
+ (userDelErr: any) => {
+ if (userDelErr) {
+ return resolve("Error deleting user.")
+ }
+
+ resolve(`User '${username}' deleted successfully.`)
+ }
+ )
+ }
+ )
+ }
+ )
+ }
+ )
+ })
+}
diff --git a/src/cmd/eval.ts b/src/cmd/eval.ts
new file mode 100644
index 0000000..ae579f4
--- /dev/null
+++ b/src/cmd/eval.ts
@@ -0,0 +1,30 @@
+import { ConsoleCommandData } from "../types/Command";
+
+export const metadata: ConsoleCommandData = {
+ name: "eval",
+ description: "Evaluates JavaScript code.",
+ usage: `eval ${''['bold']}`,
+ aliases: ["do"]
+}
+
+export async function Main(args: string[]) {
+ let result = ""
+ result = args.join(" ")
+
+ const showRawOutput = result.includes("--raw") || result.includes("-r")
+ result = result.replace('--raw', '').replace('-r', '')
+ let evaled = eval(result)
+
+ if (!showRawOutput) {
+ result = result.replaceAll(process.env.token, "\"[token\"").replaceAll('client.token', '\"[token]\"').replaceAll('process.env.token', '\"[token]\"');
+
+ //Convert evaled into a JSON String if it is an Object
+ if (typeof evaled === "object") evaled = JSON.stringify(evaled);
+
+ //Do this if evaled is anything else other than a number, text, or true/false
+ if (typeof evaled !== "number" && typeof evaled !== "string" && typeof evaled !== "boolean") evaled = `Output could not be converted to text (output was of type: ${typeof evaled}).`;
+ evaled = evaled.toString().replaceAll(process.env.token.toString(), "[token]")
+ result = result.toString().replaceAll(process.env.token, "[token]").replaceAll('client.token', '[token]').replaceAll('process.env.token', '[token]');
+ }
+ return `${showRawOutput ? "Raw e" : "E"}valuation result:\n${evaled}`
+}
\ No newline at end of file
diff --git a/src/cmd/exit.ts b/src/cmd/exit.ts
new file mode 100644
index 0000000..f4bf803
--- /dev/null
+++ b/src/cmd/exit.ts
@@ -0,0 +1,39 @@
+import { ConsoleCommandData } from "../types/Command"
+import treekill from 'tree-kill'
+import Logger from '../util/Logger'
+const c = new Logger("CLIENT/exit.js")
+
+export const metadata: ConsoleCommandData = {
+ name: 'exit',
+ description: "Stops the application.",
+ usage: false,
+ aliases: ["stop", "quit"]
+}
+
+var exitedAlready: boolean = false;
+
+export function Main(args: Array): string | number {
+ var code: number = Number.parseInt(args[0]) || 1
+
+ HandleExit();
+
+ return 0;
+}
+
+export function HandleExit() {
+ ['exit', 'SIGINT', 'SIGUSR1', 'SIGUSR2', 'uncaughtException', 'SIGTERM'].forEach((eventType) => {
+ process.on(eventType, ()=>{})
+ })
+ try {
+ if (!exitedAlready) {
+ let RootProcPid = process.ppid
+ exitedAlready = true;
+ c.log("Have a nice day!")
+ treekill(RootProcPid)
+ }
+ } catch {
+ treekill(process.ppid)
+ }
+
+ process.exit()
+}
\ No newline at end of file
diff --git a/src/cmd/help.ts b/src/cmd/help.ts
new file mode 100644
index 0000000..1c1624d
--- /dev/null
+++ b/src/cmd/help.ts
@@ -0,0 +1,62 @@
+import Logger from "../util/Logger";
+const cm = new Logger("CLIENT/help.js")
+import fs from 'fs'
+import 'colorts/lib/string'
+import { ConsoleCommandData } from "../types/Command";
+
+export const metadata = {
+ name: "help",
+ description: "Returns a list of usable commands / usage of a command.",
+ usage: `help ${"[command name]"["italic"]}`,
+ aliases: false
+}
+
+export function Main(args: Array) {
+ const cmdname = args[0]
+ const cwd = process.cwd();
+
+ try {
+ if (cmdname) {
+ const metadata: ConsoleCommandData = require(cwd + `/output/src/cmd/${cmdname.replace('ts', 'js')}`).metadata
+
+ cm.log('Command name: ' + cmdname)
+ cm.log(`Description: ${metadata.description}`)
+ cm.log(`Usage: ${metadata.usage ? metadata.usage : 'No particular arguments.'}`)
+ if (Array.isArray(metadata.aliases) && metadata.aliases.length)
+ cm.log(`Aliases: ${metadata.aliases.join(', ')}`)
+ return 0
+ }
+ } catch {
+ cm.log(`The command ${cmdname} was not found.`)
+ return 0
+ }
+
+ if (!cmdname)
+ try {
+ //var commands = "\n"
+ const commandList = fs.readdirSync(cwd + '/output/src/cmd')
+ //var processed_commands: number = 0
+ cm.log(`${"Italic text"["italic"]} represent optional arguments, while ${"bold"["bold"]} text represent mandatory arguments.`)
+ cm.log(`Command usage: ${this.metadata.usage}\n`)
+ cm.log("The following commands are available:")
+
+ for (let i = 0; i < commandList.length; i++) {
+ const commandFile = commandList[i];
+ if (commandFile.includes('.map'))
+ ""
+ else {
+ //processed_commands++
+ const commandMetadata = require(cwd + `/output/src/cmd/${commandFile.replace('ts', 'js')}`).metadata
+ const usage = commandMetadata.usage ? commandMetadata.usage : commandFile
+ cm.log(`${commandFile.replace('.js', '')}\t${commandMetadata.description}`)
+ //cm.log(`description: ${commandMetadata.description}`)
+ //cm.log(`usage: ${usage}`)
+ }
+ }
+
+ return 1;
+ } catch (err) {
+ cm.error("An error occured while running this command: " + err)
+ return 0
+ }
+}
\ No newline at end of file
diff --git a/src/cmd/idGen.ts b/src/cmd/idGen.ts
new file mode 100644
index 0000000..d6ff798
--- /dev/null
+++ b/src/cmd/idGen.ts
@@ -0,0 +1,37 @@
+import { ConsoleCommandData } from '../types/Command';
+import { parseInt } from '../util';
+import GenerateID from '../util/idGen';
+import { Clear } from '../util/Prompt'
+
+export const metadata: ConsoleCommandData = {
+ name: 'idGen',
+ description: "Generates an ID.",
+ usage: "idGen [amount]",
+ aliases: ["idgen"]
+}
+
+export function Main(args: Array): string {
+ if (!args[0])
+ return "Insufficient arguments.";
+
+ try {
+ let bs = parseInt(args[0])
+ let quantity = 1
+ var generated = 0;
+ var ids = []
+ if (args[1]) {
+ quantity = Number.parseInt(args[1])
+ }
+ // the for loop didnt work (??)
+ while (generated < quantity) {
+ generated++
+ ids.push(`${generated}) ${GenerateID(bs)}`)
+ }
+ return `Generated ID(s): \n${ids.join('\n')}`
+ } catch (err: any) {
+ if (err.includes('is not an integer'))
+ return "Invalid arguments. Command usage:\nidGen [amount]"
+ else
+ return `An error occured while running the command: ${err}`
+ }
+}
\ No newline at end of file
diff --git a/src/cmd/register.ts b/src/cmd/register.ts
new file mode 100644
index 0000000..e4d4b76
--- /dev/null
+++ b/src/cmd/register.ts
@@ -0,0 +1,55 @@
+import { ConsoleCommandData } from "../types/Command"
+import bcrypt from 'bcrypt'
+import { db } from "../util/database";
+import Logger from "../util/Logger";
+const cm = new Logger("CLIENT/register.js")
+export const metadata: ConsoleCommandData = {
+ name: 'register',
+ description: "Creates an account.",
+ usage: "register ",
+ aliases: ["reg"]
+}
+
+export async function Main(args: Array): Promise {
+ if (args.length < 2)
+ return "Missing arguments. Run /help register for help with this command.";
+
+ const username = args[0]
+ const password = args[1]
+ const passwordHash = await bcrypt.hash(password, 12);
+
+ db.get(
+ `SELECT username FROM users WHERE username = ?`,
+ [username],
+ (err: any, row: any) => {
+ var output = ""
+ if (err) {
+ console.error(err);
+ return "Database error!"
+ }
+
+ if (row) {
+ output = `The username ${username} is already taken!`;
+ cm.log(output)
+ } else {
+ db.run(
+ `INSERT INTO users (username, password_hash) VALUES (?, ?)`,
+ [username, passwordHash],
+ (err: any) => {
+ if (err) {
+ console.error(err);
+ output = "Error creating account!";
+ } else {
+ output = "Account created.";
+ }
+ cm.log(output)
+ }
+ );
+ }
+
+ return output
+ }
+ );
+
+ return 0;
+}
\ No newline at end of file
diff --git a/src/cmd/restart.ts b/src/cmd/restart.ts
new file mode 100644
index 0000000..2e41acb
--- /dev/null
+++ b/src/cmd/restart.ts
@@ -0,0 +1,32 @@
+import proc, { exec, execSync } from 'child_process'
+import { ConsoleCommandData } from '../types/Command';
+import { server } from '..';
+import { db } from '../util/database';
+
+export const metadata: ConsoleCommandData = {
+ name: 'restart',
+ description: "Restarts the application.",
+ usage: false,
+ aliases: ["re", "reboot"]
+}
+
+
+export function run(command: string) {
+ try {
+ execSync(command, { stdio: 'inherit' })
+ } catch (err) {
+ //c.error(`Caught exception while rebuilding the project. E:\n${err}`)
+ process.exit(0) // probably a sigint?
+ }
+}
+
+export function Main(args: Array): string {
+ try {
+ server.close();
+ db.close();
+ } catch { } // if this fails then the server and db are probably closed already
+
+ run("npm run quick");
+
+ return "The new process has crashed. Run \"restart\" to rebuild the application again."
+}
\ No newline at end of file
diff --git a/src/cmd/say.ts b/src/cmd/say.ts
new file mode 100644
index 0000000..981998e
--- /dev/null
+++ b/src/cmd/say.ts
@@ -0,0 +1,17 @@
+import { ConsoleCommandData } from '../types/Command';
+import { Clear } from '../util/Prompt'
+
+export const metadata: ConsoleCommandData = {
+ name: 'say',
+ description: "Repeats the input.",
+ usage: "say ",
+}
+
+
+// test cmd (why)
+export function Main(args: Array): string {
+ if (!args)
+ return Clear;
+
+ return args.join(' ')
+}
\ No newline at end of file
diff --git a/src/components/pages_text.ts b/src/components/pages_text.ts
new file mode 100644
index 0000000..ce5500a
--- /dev/null
+++ b/src/components/pages_text.ts
@@ -0,0 +1,44 @@
+var pages: { name: string, addr: string }[] = [
+ { name: 'staticfiles', addr: '/staticfiles' },
+ { name: 'upload', addr: '/upload'},
+ { name: 'posts', addr: '/posts' },
+ { name: "home", addr: '/' }
+]
+
+/*
+
+*/
+
+const pages_string = (activepage: string) => {
+ var npages = [...pages];
+ var base = `
+ `
+
+ return base;
+}
+
+export default pages_string;
\ No newline at end of file
diff --git a/src/components/styles.ts b/src/components/styles.ts
new file mode 100644
index 0000000..fb52d79
--- /dev/null
+++ b/src/components/styles.ts
@@ -0,0 +1,304 @@
+const styles_string = (add: string = null, override: string = null) => {
+ var out = `
+ `
+
+ if (override)
+ out = override;
+
+ return out
+}
+
+export default styles_string;
+
+export const parse_date_string = `
+ function parseDate(input, useRtf = true, showTime = true, timeZone) {
+
+ const date = new Date(input);
+ const now = new Date();
+
+ const tz = timeZone || Intl.DateTimeFormat().resolvedOptions().timeZone;
+
+ const diffSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
+
+ if (useRtf) {
+ const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
+
+ if (diffSeconds < 60) return rtf.format(-diffSeconds, "second");
+ if (diffSeconds < 3600) return rtf.format(-Math.floor(diffSeconds / 60), "minute");
+ if (diffSeconds < 86400) return rtf.format(-Math.floor(diffSeconds / 3600), "hour");
+ if (diffSeconds < 604800) return rtf.format(-Math.floor(diffSeconds / 86400), "day");
+ }
+
+ return date.toLocaleString("en-US", {
+ month: "long",
+ day: "numeric",
+ year: "numeric",
+ ...(showTime && {
+ hour: "numeric",
+ minute: "2-digit",
+ hour12: true,
+ timeZoneName: "short"
+ }),
+ timeZone: tz
+ });
+ }
+
+
+ function ParseDateC(date) {
+ const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
+
+ const formatted = new Intl.DateTimeFormat("en-US", {
+ timeZone,
+ year: "numeric",
+ month: "long",
+ day: "numeric",
+ hour: "numeric",
+ minute: "2-digit",
+ hour12: true
+ }).format(date);
+
+ const result = formatted.replace(",", "")
+ .replace(/(\d{4})/, "$1 at");
+
+ return result
+ }
+
+ ${/*function toIso(input) {
+ const date = input instanceof Date ? input : new Date(input);
+
+ if (isNaN(date)) {
+ throw new Error("Invalid date");
+ }
+
+ const pad = n => String(n).padStart(2, "0");
+
+ const year = date.getFullYear();
+ const month = pad(date.getMonth() + 1);
+ const day = pad(date.getDate());
+ const hour = pad(date.getHours());
+ const minute = pad(date.getMinutes());
+ const second = pad(date.getSeconds());
+
+ const offsetMinutes = -date.getTimezoneOffset();
+ const sign = offsetMinutes >= 0 ? "+" : "-";
+ const abs = Math.abs(offsetMinutes);
+ const offsetHour = pad(Math.floor(abs / 60));
+ const offsetMinute = pad(abs % 60);
+
+ return \`\${year}-\${month}-\${day}T\${hour}:\${minute}:\${second}\${sign}\${offsetHour}:\${offsetMinute}\`
+ }*/""}
+
+ function toIso(input) {
+ const date = new Date(input);
+ if (isNaN(date)) throw new Error("Invalid date");
+
+ const pad = n => String(n).padStart(2, "0");
+
+ const year = date.getFullYear();
+ const month = pad(date.getMonth() + 1);
+ const day = pad(date.getDate());
+ const hour = pad(date.getHours());
+ const minute = pad(date.getMinutes());
+ const second = pad(date.getSeconds());
+
+ const offsetMinutes = -date.getTimezoneOffset(); // offset in minutes
+ const sign = offsetMinutes >= 0 ? "+" : "-";
+ const absOffset = Math.abs(offsetMinutes);
+ const offsetHour = pad(Math.floor(absOffset / 60));
+ const offsetMinute = pad(absOffset % 60);
+
+ return \`\${year}\-\${month}-\${day}T\${hour}:\${minute}:\${second}\${sign}\${offsetHour}:\${offsetMinute}\`;
+ }
+ `
\ No newline at end of file
diff --git a/src/favicon.ico b/src/favicon.ico
new file mode 100644
index 0000000..9be32da
Binary files /dev/null and b/src/favicon.ico differ
diff --git a/src/index.ts b/src/index.ts
new file mode 100644
index 0000000..2c6f1e5
--- /dev/null
+++ b/src/index.ts
@@ -0,0 +1,186 @@
+const dev = false
+
+import express, { Express } from "express";
+const serveIndex = require('serve-index');
+import { Prompt } from "./util/Prompt";
+import multer from 'multer'
+import Logger from "./util/Logger";
+const s = new Logger("Server", "blue")
+import morgan from 'morgan'
+import fs from 'fs'
+import SetRndPasswd from "./util/SetPasswd";
+import path from "path";
+import * as util from './util'
+var missingDirectories = true;
+
+if (util.mkDirsManIfNotExists([
+ path.join(process.cwd(), "pvfiles"),
+ path.join(process.cwd(), "uploads"),
+ path.join(process.cwd(), "staticfiles")
+]))
+ missingDirectories = false;
+else {
+ s.log("Missing directories. Please ensure the folders 'pvfiles', 'uploads', and 'staticfiles' exist in the application's root folder.")
+ process.exit(1)
+}
+
+if (!fs.existsSync(path.join(process.cwd(), ".env"))) {
+ const secret = PasswordGen(24)
+
+ const rString = `
+ port=80
+ altregion=us # the displayed region in each page's header
+
+ servicename=File Server # the displayed service name in each page's header
+ description=A simple file server built with Express. # the displayed description in the home page
+
+ bootmessage=Welcome!
+
+ session_secret=${secret} # the session secret used for logins. you can generate a new secret using the idGen command.
+ enable_register=false # it's advised to keep this off unless you're hosting a public instance
+
+ postwrite_notice=Hi! # the notice on top of the post form
+ postwrite_html=false # if enabled, users can use html in posts. this is unsafe! you can set it to true if you trust your users.
+ `
+
+ fs.writeFileSync(path.join(process.cwd(), ".env"), rString);
+
+ s.log("A .env file has been created in the root directory. If you'd like, you can edit its values.")
+}
+
+var errorHandler = require('express-error-handler'),
+ handler = errorHandler({
+ static: {
+ '404': "/404"
+ }
+ })
+
+console.clear()
+
+s.log('Preparing the application...')
+
+import dotenv from "dotenv";
+dotenv.config();
+
+export const app: Express = express();
+
+export const port = process.env.port || 3000;
+
+var storage: multer.StorageEngine
+export var upload: multer.Multer;
+
+export var server: Server;
+
+import moment from 'moment-timezone'
+import { Server } from "http";
+import session from "express-session";
+import PasswordGen from "./util/passwordGen";
+
+var accessLogStream = fs.createWriteStream('./pvfiles/interactions.log', { flags: 'a' });
+
+app.enable("trust proxy");
+try {
+
+ // upload handler
+ storage = multer.diskStorage({
+ destination: (req, file, cb) =>
+ cb(null, "uploads"),
+ filename: (req, file, cb) =>
+ cb(null, `${Date.now()}_${file.originalname}`)
+ })
+ upload = multer({storage})
+
+ app.use(
+ session({
+ name: "session",
+ secret: process.env.session_secret,
+ resave: false,
+ saveUninitialized: false,
+ cookie: {
+ httpOnly: true,
+ sameSite: "lax",
+ secure: dev
+ }
+ })
+ );
+ //app.use("/uploads", express.static("uploads"));
+
+ app.use('/staticfiles', express.static('staticfiles'), serveIndex('staticfiles', { 'icons': true }))
+ server = app.listen(port, () => {
+ s.log(`Server started at 127.0.0.1:${port}`)
+ s.log(`${process.env.bootmessage || "Ready!"}`)
+ Prompt()
+ }).on('error', (e: any) => {
+ if (e.code === "EADDRINUSE") {
+ console.log('\n\n')
+ s.log(`Address already in use. Your port (${port}) might be in use. Use another port or make it available.`)
+ process.exit(0)
+ }
+ })
+ // Setup listeners
+ app.get('/api/ping/assist', (req, res, next) => {
+ const timestamp = Date.now();
+ res.json({ timestamp });
+ });
+
+ // SET LOG PASSWORDS
+ //SetRndPasswd('../pvfiles/app.log')
+ s.log(`The AccessKey for interactions.log is ${SetRndPasswd('./pvfiles/interactions.log')}`)
+
+ function removeURLParameter(url: string | undefined, parameterName: string) {
+ //prefer to use l.search if you have a location/link object
+ var urlparts: any = url?.split('?');
+ if (urlparts.length >= 2) {
+
+ var prefix = encodeURIComponent(parameterName) + '=';
+ var pars = urlparts[1].split(/[&;]/g);
+
+ //reverse iteration as may be destructive
+ for (var i = pars.length; i-- > 0;) {
+ //idiom for string.startsWith
+ if (pars[i].lastIndexOf(prefix, 0) !== -1) {
+ pars.splice(i, 1);
+ }
+ }
+
+ return urlparts[0] + (pars.length > 0 ? '?' + pars.join('&') : '');
+ }
+ return url;
+ }
+
+ morgan.token('path', (req, res) => {
+ return removeURLParameter(req?.url, "password")
+ })
+
+ morgan.token('datenow', (req, res) => {
+ return `${moment().format("MM/DD/YYYY h:mm:ss A")} ${Intl.DateTimeFormat().resolvedOptions().timeZone || ""}`;
+ })
+ app.use(morgan(
+ ':datenow - :method by :remote-addr in :path :status :response-time ms - content length :res[content-length] bytes',
+ {
+ stream: accessLogStream
+ }
+ ));
+
+ // set up listeners
+ const listeners = fs.readdirSync('./src/listeners')
+ for (let count = 0; count < listeners.length; count++) {
+ const listenerName = listeners[count];
+
+ require(`${process.cwd()}/output/src/listeners/${listenerName.replaceAll("ts", "js")}`).Api(app)
+ }
+
+ // set up pages
+ const pages = fs.readdirSync('./src/pages')
+ for (let count = 0; count < pages.length; count++) {
+ const pageFilename = pages[count];
+
+ if (pageFilename.endsWith('.ts'))
+ require(`${process.cwd()}/output/src/pages/${pageFilename.replaceAll(".ts", ".js")}`).Main(app)
+ // in case a base html file was there
+ }
+
+ app.use(handler)
+} catch (err: any) {
+ console.error(err)
+}
\ No newline at end of file
diff --git a/src/listeners/GetApi.ts b/src/listeners/GetApi.ts
new file mode 100644
index 0000000..546ba79
--- /dev/null
+++ b/src/listeners/GetApi.ts
@@ -0,0 +1,58 @@
+import { Express, Request, Response } from "express";
+import Logger from "../util/Logger";
+export var latency: number = 0;
+var c = new Logger("Server(Api)", "gray")
+var ce = new Logger("Api(Send)", "italic")
+
+
+export function Api(app: Express) {
+ c.log('GetApi loaded.')
+
+ app.get('/api/:apiID', (req: Request, res: Response) => {
+ switch (req.params.apiID) {
+ case "logout":
+ req.session.destroy((err: any) => {
+ if (err) {
+ c.error(`User failed to log out: ${err}`,)
+ res.status(500).send("Failed to log out.")
+ }
+
+ res.clearCookie("session")
+ res.redirect("/login")
+ })
+
+ break;
+
+ /*case "ping":
+ const startTime = Date.now();
+
+ fetch(`${process.env.https}://${ipAddress}:${require('..').port}/api/ping/assist`)
+ .then(response => response.json())
+ .then(data => {
+ const endTime = Date.now();
+ latency = endTime - startTime;
+ })
+ .catch(error => console.error('Error:', error));
+ res.send(`${latency}`)
+
+ break;*/
+
+ /*case "send":
+ const message: any = req.query.msg || undefined
+ const sender: any = req.query.sender || "API call"
+
+ if (message) {
+ ce.log(`${sender}: ${message}`)
+ res.status(200).send("Sent message as a string to internal logs.
Message content: " + message + "
Sender: " + sender)
+ } else {
+ res.status(500).send("No message provided.")
+ }
+
+ break;*/ // wtf
+
+ default:
+ res.status(404).send(`An API named ${req.params.apiID} could not be found.`)
+ break;
+ }
+ })
+}
\ No newline at end of file
diff --git a/src/listeners/GetPrivateFile.ts b/src/listeners/GetPrivateFile.ts
new file mode 100644
index 0000000..9b94b5b
--- /dev/null
+++ b/src/listeners/GetPrivateFile.ts
@@ -0,0 +1,124 @@
+import { Express, Request, Response } from "express";
+import Logger from "../util/Logger";
+import fs from "fs";
+import SetRndPasswd from "../util/SetPasswd";
+export var latency: number = 0;
+var c = new Logger("Server(Pvf)", "gray")
+var ce = new Logger("Server(PvfSetup)", "gray")
+export function findInArray(array: any[], find: any) {
+ for (const item of array) {
+ if (item === find)
+ return item
+ }
+
+ return null
+}
+
+import dotenv from 'dotenv'
+import styles_string from "../components/styles";
+import pages_string from "../components/pages_text";
+import { bytesToSize } from "../util";
+
+dotenv.config()
+
+export function Api(app: Express) {
+
+ var files: any[] = []
+
+ /*if (process.platform === "win32")
+ var paths: string[] = [
+ `${__dirname.replace('\\src\\listeners', '')}\\..\\pvfiles\\:filename`,
+ ]
+ else*/
+ var paths: string[] = [
+ `${process.cwd()}/pvfiles/:filename`,
+ ]
+
+ for (const watchPath of paths) {
+ var cleanPath = watchPath.replace(':filename', '');
+
+ ce.log(`Checking contents of ${cleanPath}`)
+
+ for (const filename of fs.readdirSync(`${cleanPath}`)) {
+ files.push(
+ `${filename.toString()}`,
+ );
+ ce.log(`Found a PV file: ${filename}`)
+ }
+
+ // VALIDATE
+ for (const filename of files) {
+ if (!filename.endsWith(".acck") && !fs.existsSync(`./pvfiles/${filename}.acck`)) {
+ ce.log(`File ${filename} has no access key file! Creating one now!`)
+ SetRndPasswd(`./pvfiles/${filename}`)
+ ce.log(`An access key has been set for ${filename} ; use getPvf ${filename} to get the new access key.`)
+ }
+ }
+ app.get('/pvfiles/:filename', (req: Request, res: Response) => {
+ var filename: string = req.params.filename;
+ var password = req.query.AccessKey
+ var direct = req.query.direct === "true" ? true : false || false;
+ var dl = req.query.dl === "true" ? true : false || false;
+ var fullPath: string = `${cleanPath}${findInArray(files, filename)}`;
+
+ if (files.includes(filename) && fs.existsSync(fullPath)) {
+ if (files.includes(filename + '.acck') && fs.existsSync(`${fullPath}.acck`)) {
+ if (password === fs.readFileSync(`${fullPath}.acck`, { encoding: 'utf8' })) {
+ var here_direct = `/pvfiles/${filename}?AccessKey=${password}&direct=true`
+ var here_dl = `/pvfiles/${filename}?AccessKey=${password}&dl=true`;
+
+ if (direct) {
+ c.log(`A private file was sent to ${req.ip}: ${fullPath}`)
+ res.status(200).sendFile(fullPath)
+ } else if (dl) {
+ c.log(`A private file was downloaded by ${req.ip}: ${fullPath}`)
+ res.status(200).download(fullPath)
+ if (fullPath.endsWith(".pdf"))
+ res.status(200).sendFile(fullPath) // show the pdf since most browsers just show it when given a link
+ } else {
+ const stat = fs.statSync(fullPath)
+ const size = bytesToSize(stat.size)
+
+ res.status(200).send(
+ `
+
+ ${filename}
+
+
+ ${styles_string()}
+ ${process.env.servicename || "File Server"}
+
+ ${/*
Local version: ${require('../updateInfo').GameVersion}
*/""}
+
Server region: ${'(alternate) ' + process.env.altregion}
+
+
+ ${filename} ${size}
+
+
+
+
+
+
+
+ ${pages_string("/pvfiles")}
+ `
+ )
+ }
+ } else if (!password) {
+ res.status(500).send(`Cannot access ${filename} without an AccessKey.`)
+ } else
+ res.status(500).send('AccessKey entered is incorrect.')
+ } else
+ res.status(404).send("File not found.")
+ } else
+ res.status(404).send(
+ `File not found.`)
+
+ /* Searched: ${fullPath}
+
Input filename: ${filename}
+
File array: ${files.toString()}
+
Input filename matches file: ${fs.existsSync.log(fullPath)} | but matches an entry in the files array? ${files.includes(filename)}*/
+ })
+ c.log(`GetPrivateFile loaded.`)
+ }
+}
\ No newline at end of file
diff --git a/src/pages/404.ts b/src/pages/404.ts
new file mode 100644
index 0000000..58f3c36
--- /dev/null
+++ b/src/pages/404.ts
@@ -0,0 +1,85 @@
+import { Express, Request, Response } from "express";
+import Logger from '../util/Logger'
+var c = new Logger("Server(Pages)", "gray")
+
+import dotenv from "dotenv";
+dotenv.config();
+
+
+export async function Main(app: Express) {
+ c.log('Found page "404".')
+
+ app.get('/404', async (req: Request, res: Response) => {
+ res.status(404).send(
+ /*`
+
+ Update server for Journey to Nowhere. Local version: ${require('../gameinfo').GameVersion}
+
Server region: ${ipd?.country || '(alternate) ' + process.env.altregion}
+
Server ping: ${latency}ms
+
+ `*/
+ `
+
+
+
+ ${process.env.servicename || "File Server"}
+
+ ${/*
Local version: ${require('../updateInfo').GameVersion}
*/""}
+
Server region: ${'(alternate) ' + process.env.altregion}
+
+
+
+ ${process.env.notfound || "The file you were looking for does not exist on the server."}
+
+`
+ )
+ })
+}
\ No newline at end of file
diff --git a/src/pages/login.ts b/src/pages/login.ts
new file mode 100644
index 0000000..dacfc5d
--- /dev/null
+++ b/src/pages/login.ts
@@ -0,0 +1,99 @@
+import { Express, Request, Response } from "express";
+import Logger from "../util/Logger";
+import "../updateInfo";
+import bcrypt from "bcrypt";
+import { db } from "../util/database";
+
+import dotenv from "dotenv";
+import { User } from "../types/User";
+import pages_string from "../components/pages_text";
+import styles_string from "../components/styles";
+dotenv.config();
+
+const c = new Logger("Server(Pages)", "gray");
+
+const login_string = (alert: string = "
", active_page: string = "/login"): string => {
+ return `
+
+
+ ${process.env.servicename || "File Server"} | Login
+
+
+ ${styles_string()}
+
+ ${process.env.servicename || "File Server"}
+
+
+
Server region: ${"(alternate) " + process.env.altregion}
+
+
+
+
+
+ Login
+
+
+ ${`${alert}
` ? alert : ""}
+
+
+
+
+
+ ${pages_string(active_page)}
+
+`
+}
+export async function Main(app: Express) {
+ c.log('Found page "login".');
+
+ app.use(require("express").urlencoded({ extended: true }));
+
+ app.get("/login", async (req: Request, res: Response) => {
+ if (req.session.user)
+ res.redirect("/")
+ else
+ res.send(login_string());
+ });
+
+ app.post("/login", async (req: Request, res: Response) => {
+ const { username, password } = req.body;
+
+ if (!username || !password) {
+ return res.status(400).send(login_string("Missing username or password!"));
+ }
+
+ db.get(
+ `SELECT * FROM users WHERE username = ?`,
+ [username],
+ async (err, user: User) => {
+ if (err) {
+ return res.status(500).send(login_string("Database error"));
+ }
+
+ if (!user) {
+ return res.status(401).send(login_string("Invalid username or password."));
+ }
+
+ const valid = await bcrypt.compare(password, user.password_hash);
+ if (!valid) {
+ return res.status(401).send(login_string("Invalid username or password."));
+ }
+
+ req.session.user = {
+ id: user.id,
+ username: user.username
+ };
+
+ res.redirect("/");
+ }
+ );
+ });
+}
diff --git a/src/pages/posts.ts b/src/pages/posts.ts
new file mode 100644
index 0000000..3d0c1a0
--- /dev/null
+++ b/src/pages/posts.ts
@@ -0,0 +1,314 @@
+import { Express, Request, Response } from "express";
+import Logger from '../util/Logger'
+var c = new Logger("Server(Pages)", "gray")
+
+import dotenv from "dotenv";
+import * as dbs from '../util/database'
+import { User, UserSession } from "../types/User";
+import { PostRow } from "../types/PostRow";
+import GenerateID from "../util/idGen";
+import { convertToIso, parseDate } from "../util/dateParse";
+import styles_string, { parse_date_string } from "../components/styles";
+import pages_string from "../components/pages_text";
+import { escapeHtml } from "../util";
+const db = dbs.db;
+
+dotenv.config();
+
+
+const heads = (active_page: string, title: string, content: string, extraoverride: string = null, notitleset = false): string => {
+ return `
+
+${notitleset ? "" : `${process.env.servicename || "File Server"} | ${title}`}
+
+ ${styles_string(`
+ .postlist_r {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 20px;
+ }
+
+ .postlist {
+ flex: 1 1 250px;
+ background-color: rgba(0,0,0,0.03);
+ padding: 15px;
+ border-radius: 8px;
+ box-sizing: border-box;
+ max-width: 400px;
+ max-height: 150px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .selectarr {
+ flex-wrap: wrap;
+ display: flex;
+ gap: 20px;
+ }
+
+ .selectarr select {
+ width: 100%;
+ }
+
+ .c_col {
+ flex: 1;
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ }
+ `)}
+
+ ${process.env.servicename || "File Server"}
+
+
+
Server region: ${"(alternate) " + process.env.altregion}
+
+
+
+
+ ${title ? `
+ ${title}
+
` : ""}
+
+ ${content}
+
+ ${pages_string(active_page)}
+
+
+`
+}
+
+
+const newpost_string = (alert: string = null) => heads("/posts/new", "New Post", `
+
+
+
+`);
+
+export function getUsernameById(userId: number): Promise {
+ return new Promise((resolve, reject) => {
+ db.get(
+ `SELECT username FROM users WHERE id = ?`,
+ [userId],
+ (err, row: User) => {
+ if (err) return reject(err);
+ resolve(row?.username ?? "Unknown");
+ }
+ );
+ });
+}
+
+async function posts_string(user?: UserSession): Promise {
+ return new Promise((resolve, reject) => {
+ var out = "";
+ let sql = `
+ SELECT *
+ FROM posts
+ WHERE visibility = 'public'
+ `;
+ let params: any[] = [];
+
+ if (user) {
+ sql += `
+ OR (user_id = ?)
+ `;
+ params.push(user.id);
+ }
+
+ sql += ` ORDER BY created_at DESC`;
+
+ db.all(sql, params, async (err, rows: PostRow[]) => {
+ if (err) return reject(err);
+ var times = 0;
+ for (const p of rows) {
+ times++;
+ var title = escapeHtml(p.title);
+ var content = escapeHtml(p.content)
+
+ out += `
+
+
${title} ${p.visibility == "public" ? "" : `(${p.visibility})`}
+
by ${await getUsernameById(p.user_id)} ()
+ ${content}
+
+ `
+ };
+
+ resolve(out || "No posts yet.");
+ });
+ });
+}
+
+export async function Main(app: Express) {
+ c.log('Found page "upload".')
+
+ app.get("/posts", async (req: Request, res: Response) => {
+ const posts = await posts_string(req.session.user);
+
+ res.send(heads("/posts", "Posts", `
+ ${req.session.user ? `
+
+
+ ` : `Log in to create posts.
`}
+
+
+
+ ${posts}
+
+ `));
+ });
+
+ app.get("/posts/new", (req, res) => {
+ if (!req.session.user)
+ return res.status(401).send(heads("Posts", `
+ New Post
+
+
You must be logged in to create a post.
+
+ `, `
+ Log in | Register
+ `))
+ else {
+ return res.send(newpost_string());
+ }
+ })
+
+ app.post("/posts", (req, res) => {
+ if (!req.session.user)
+ return res.status(401).send("Login required");
+ var { title, content, visibility, font }: PostRow = req.body;
+
+ switch (font) {
+ case "Verdana":
+ case "monospace":
+ case "Bookman Old Style":
+ break;
+ default:
+ return res.status(400).send(`Invalid font ${font}`)
+ }
+
+ const hashId = GenerateID(12);
+
+ if (process.env.postwrite_html !== "true") {
+ title = escapeHtml(title);
+ content = escapeHtml(content);
+ }
+
+ db.run(
+ `INSERT INTO posts (user_id, hash_id, title, content, visibility, font)
+ VALUES (?, ?, ?, ?, ?, ?)`,
+ [req.session.user.id, hashId, title, content, visibility, font],
+ () => res.redirect(`/posts`)
+ );
+ });
+
+ // honestly this could just be a get request as well
+ app.post("/posts/:id/delete", (req: Request, res: Response) => {
+ if (!req.session.user) return res.status(401).send("Login required");
+
+ const postId = req.params.id;
+
+ // Check ownership first
+ db.get(`SELECT user_id FROM posts WHERE id = ?`, [postId], (err, row: any) => {
+ if (err) return res.status(500).send("Database error");
+ if (!row) return res.status(404).send("Post not found");
+
+ if (row.user_id !== req.session.user.id) {
+ return res.status(403).send("You cannot delete this post");
+ }
+
+ // Bye
+ db.run(`DELETE FROM posts WHERE id = ?`, [postId], (err) => {
+ if (err) return res.status(500).send("Database error");
+ res.redirect("/posts");
+ });
+ });
+ });
+
+ app.get("/posts/:id", (req, res) => {
+ db.get(
+ `SELECT * FROM posts WHERE hash_id = ?`,
+ [req.params.id],
+ async (err, post: PostRow) => {
+ if (!post) return res.status(404).send(heads("/posts/:", "Posts", "Post not found."));
+
+ if (
+ post.visibility === "private" &&
+ (!req.session.user || req.session.user.id !== post.user_id)
+ ) {
+ return res.status(404).send(heads("/posts/:", "Posts", "
Post not found.
"));
+ }
+
+ var font = 'Verdana'
+ if (post.font)
+ font = post.font
+ const user = req.session?.user;
+
+ res.send(heads("/posts/:", null, `
+
+ ${post.title}
+
+
+ ${post.title}
+ by ${await getUsernameById(post.user_id)} ${user && user.id === post.user_id ? `
+ |
+ ` : ""}
+
+
+
+ ${post.content.replaceAll("\n", "
")}
+
+
+ Back
+
+ `, null, true));
+ }
+ );
+ });
+}
\ No newline at end of file
diff --git a/src/pages/register.ts b/src/pages/register.ts
new file mode 100644
index 0000000..f72125a
--- /dev/null
+++ b/src/pages/register.ts
@@ -0,0 +1,179 @@
+import { Express, Request, Response } from "express";
+import Logger from "../util/Logger";
+import "../updateInfo";
+import bcrypt from "bcrypt";
+import { db } from "../util/database";
+
+import dotenv from "dotenv";
+import styles_string from "../components/styles";
+import pages_string from "../components/pages_text";
+dotenv.config();
+
+const c = new Logger("Server(Pages)", "gray");
+
+function register_string(alert: string = "
"): string {
+ return `
+
+
+ ${process.env.servicename || "File Server"} | Registration
+
+
+
+ ${styles_string()}
+
+ ${process.env.servicename || "File Server"}
+
+
+
Server region: ${"(alternate) " + process.env.altregion}
+
+
+
+
+
+ Registration
+
+
+ ${`${alert}
` ? alert : ""}
+
+
+
+
+
+
+
+ ${pages_string("/register")}
+
+`
+}
+export async function Main(app: Express) {
+ c.log('Found page "register".');
+
+ app.use(require("express").urlencoded({ extended: true }));
+
+ app.get("/register", async (req: Request, res: Response) => {
+ if (req.session.user)
+ res.redirect("/")
+ else
+ res.send(register_string());
+ });
+
+ app.post("/register", async (req: Request, res: Response) => {
+ if (process.env.enable_register == "true") {
+ const { username, password } = req.body;
+
+ if (!username || !password) {
+ return res.send(register_string("Missing username or password."));
+ }
+
+ try {
+ const passwordHash = await bcrypt.hash(password, 12);
+
+ db.run(
+ `INSERT INTO users (username, password_hash) VALUES (?, ?)`,
+ [username, passwordHash],
+ (err: any) => {
+ var isnameerror = false;
+ if (err) {
+ if (err.message.includes("UNIQUE")) {
+ isnameerror = true;
+ }
+ }
+ if (!isnameerror)
+ c.log(`User registered: ${username}`);
+
+ if (isnameerror)
+ res.send(register_string(`The username ${username} is already taken!`))
+ else
+ res.send(`
+
+
+
+
+ ${process.env.servicename || "File Server"}
+
+
Server region: ${'(alternate) ' + process.env.altregion}
+
+
+ ${process.env.description || "A simple file server built with Express."}
+
+
+
+ `);
+ }
+ );
+ } catch (err) {
+ c.error("Hashing error");
+ res.status(500).send("Internal server error.");
+ }
+ } else
+ res.status(401).send(register_string(`${process.env.servicename || "File Server"} does not support account registration.`))
+ });
+}
\ No newline at end of file
diff --git a/src/pages/root.ts b/src/pages/root.ts
new file mode 100644
index 0000000..ae981f0
--- /dev/null
+++ b/src/pages/root.ts
@@ -0,0 +1,52 @@
+import { Express, Request, Response, json } from "express";
+import Logger from '../util/Logger'
+import '../updateInfo'
+var c = new Logger("Server(Pages)", "gray")
+
+import dotenv from "dotenv";
+import styles_string from "../components/styles";
+import pages_string from "../components/pages_text";
+dotenv.config();
+
+export async function Main(app: Express) {
+ c.log('Found page "root".')
+
+ app.get('/', async (req: Request, res: Response) => {
+ res.send(
+ /*`
+
+ Update server for Journey to Nowhere. Local version: ${require('../gameinfo').GameVersion}
+
Server region: ${ipd?.country || '(alternate) ' + process.env.altregion}
+
Server ping: ${latency}ms
+
+ `*/
+ `
+
+ ${process.env.servicename || "File Server"}
+
+
+
+ ${styles_string()}
+
+ ${process.env.servicename || "File Server"}
+
+ ${/*
Local version: ${require('../updateInfo').GameVersion}
*/""}
+
Server region: ${'(alternate) ' + process.env.altregion}
+
+
+ ${process.env.description || "A simple file server built with Express."}
+
+
${req.session.user ? `
+ Logged in as ${req.session.user.username}.
+
+ ` : `
+ Log in | Register
+ `}
+
+
+`
+ )
+ })
+}
\ No newline at end of file
diff --git a/src/pages/upload.ts b/src/pages/upload.ts
new file mode 100644
index 0000000..afd3b39
--- /dev/null
+++ b/src/pages/upload.ts
@@ -0,0 +1,176 @@
+import { Express, Request, Response } from "express";
+import Logger from '../util/Logger'
+var c = new Logger("Server(Pages)", "gray")
+
+import dotenv from "dotenv";
+import { upload } from "..";
+import * as dbs from '../util/database'
+import { User, UserSession } from "../types/User";
+const db = dbs.db_file;
+
+dotenv.config();
+
+const upload_string = (alert: string = null, formreplace: string = null, files: string = null): string => {
+ return `
+
+
+ ${process.env.servicename || "File Server"} | Uploads
+
+
+ ${styles_string()}
+
+ ${process.env.servicename || "File Server"}
+
+
+
Server region: ${"(alternate) " + process.env.altregion}
+
+
+
+
+
+ Upload
+
+
+ ${alert ? alert : ""}
+
+ ${formreplace ? formreplace : `
+
+ `}
+ ${formreplace ? "" : "
"}
+ All Uploads
+ ${files ? files : "None yet!
"}
+
+ ${pages_string("/upload")}
+
+
+`
+}
+
+const upload_string_home = async (alert: string, formreplace: string = null, user: UserSession): Promise => {
+ var files = await files_string(user)
+ return upload_string(alert, formreplace, files);
+}
+
+async function files_string(user: UserSession): Promise {
+ return new Promise((resolve, reject) => {
+ let out = "";
+ let filect = 1;
+
+ db.all(
+ `SELECT * FROM files WHERE user_id = ? ORDER BY upload_date DESC`,
+ [user.id],
+ (err, files) => {
+ if (err) {
+ return reject("database error");
+ }
+ var times = 0;
+ files.forEach((f: any) => {
+ times++;
+ out += `
+
+ `;
+ filect++;
+ });
+
+ resolve(out);
+ }
+ );
+ });
+}
+
+import path from 'path'
+import fs from 'fs'
+import styles_string, { parse_date_string } from "../components/styles";
+import pages_string from "../components/pages_text";
+import { convertToIso, parseDate } from "../util/dateParse";
+
+export async function Main(app: Express) {
+ c.log('Found page "upload".')
+
+ app.post("/delete", (req, res) => {
+ if (!req.session.user) {
+ return res.status(401).send(upload_string("", "You must be logged in to use this feature."))
+ }
+ else {
+ const { fileId } = req.body;
+ const userId = req.session.user.id;
+
+ db.get(
+ `SELECT * FROM files WHERE id = ? AND user_id = ?`,
+ [fileId, userId],
+ (err, file: any) => {
+ if (err) {
+ return res.status(500).send("Database error");
+ }
+
+ if (!file) {
+ return res.status(403).send("You are not authorized to delete this file.");
+ }
+
+ const filePath = path.join("uploads", file.stored_name);
+ fs.unlink(filePath, (fsErr) => {
+ if (fsErr && fsErr.code !== "ENOENT") {
+ console.error("Filesystem delete error:", fsErr);
+ return res.status(500).send("Error deleting file.");
+ }
+
+ db.run(
+ `DELETE FROM files WHERE id = ?`,
+ [fileId],
+ (delErr) => {
+ if (delErr) {
+ return res.status(500).send("Database delete error.");
+ }
+ res.redirect("/upload");
+ }
+ );
+ });
+ }
+ );
+ }
+ });
+
+
+ app.get("/upload", async (req: Request, res: Response) => {
+ if (!req.session.user)
+ return res.status(401).send(upload_string("", "You must be logged in to use this feature."))
+ else {
+ const user = req.session.user;
+ return res.send(await upload_string_home(null, null, user))
+ }
+ });
+
+ app.post("/upload", upload.single("file"), async (req: Request, res: Response) => {
+ if (!req.session.user)
+ return res.status(401).send(upload_string("", "You must be logged in to use this feature."))
+ else {
+ const user = req.session.user;
+ if (!req.file) {
+ return res.status(400).send(await upload_string_home("No file uploaded.", null, user));
+ }
+
+ const sql = `
+ INSERT INTO files (user_id, original_name, stored_name)
+ VALUES (?, ?, ?)
+ `;
+ db.run(sql, [user.id, req.file.originalname, req.file.filename], async (err) => {
+ if (err) {
+ return res.status(500).send("Database error");
+ }
+ res.status(200).send(await upload_string_home(`Uploaded ${req.file.originalname}!`, null, user))
+ });
+ }
+ });
+}
\ No newline at end of file
diff --git a/src/pages/uploads.ts b/src/pages/uploads.ts
new file mode 100644
index 0000000..9001984
--- /dev/null
+++ b/src/pages/uploads.ts
@@ -0,0 +1,95 @@
+import { Express, Request, Response } from 'express'
+import Logger from '../util/Logger'
+const ce = new Logger("Server(Uploads)", "gray");
+
+import path from 'path'
+import fs from 'fs'
+import styles_string, { parse_date_string } from '../components/styles';
+import pages_string from '../components/pages_text';
+import { getUsernameById } from './posts';
+import { db_file } from '../util/database';
+import { Upload } from '../types/Upload';
+import { convertToIso, parseDate } from '../util/dateParse';
+import { bytesToSize } from '../util';
+
+function getUploadByFilename(filename: string): Promise {
+ try {
+ return new Promise((resolve, reject) => {
+ db_file.get(
+ `
+ SELECT files.original_name, files.stored_name, files.upload_date, files.user_id
+ FROM main.files
+ JOIN udb.users ON udb.users.id = files.user_id
+ WHERE files.stored_name = ?
+ `,
+ [filename],
+ (err, row: Upload) => {
+ if (err) reject(err);
+ resolve(row ?? null);
+ }
+ );
+ })
+ } catch (err) {
+ // if the cb fails somehow
+ return null
+ }
+}
+
+export async function Main(app: Express) {
+ var cleanPath = path.join(process.cwd(), "uploads")
+
+ ce.log(`Processing uploads...`)
+
+ app.get('/uploads/:filename', async (req: Request, res: Response) => {
+ var filename: string = req.params.filename;
+ var direct = req.query.direct === "true" ? true : false || false;
+ var dl = req.query.dl === "true" ? true : false || false;
+ var fullPath: string = path.join(cleanPath, filename);
+ var useUploadInfo = true;
+ if (fs.existsSync(fullPath)) {
+ const stat = fs.statSync(fullPath)
+ const size = bytesToSize(stat.size)
+
+ var upload: Upload = null;
+ upload = await getUploadByFilename(filename);
+ if (!upload)
+ useUploadInfo = false;
+
+ if (direct) {
+ res.status(200).sendFile(fullPath)
+ } else if (dl) {
+ res.status(200).download(fullPath)
+ } else {
+ res.status(200).send(`
+
+ ${useUploadInfo ? upload.original_name : filename}
+
+
+ ${styles_string()}
+ ${process.env.servicename || "File Server"}
+
+ ${/*
Local version: ${require('../updateInfo').GameVersion}
*/""}
+
Server region: ${'(alternate) ' + process.env.altregion}
+
+
+ ${useUploadInfo ? upload.original_name : filename} ${size}
+
${useUploadInfo ? `uploaded by ${await getUsernameById(upload.user_id)} at
+
` : ""}
+ ${/*
${useUploadInfo ? `uploaded by ${await getUsernameById(upload.user_id)} at ${parseDate(upload.upload_date, false, true)}
` : ""}
*/""}
+
+
+ ${pages_string("/pvfiles")}
+ `)
+ }
+ } else
+ res.status(404).send(
+ `File not found.`)
+ })
+}
\ No newline at end of file
diff --git a/src/types/Command.ts b/src/types/Command.ts
new file mode 100644
index 0000000..b506de6
--- /dev/null
+++ b/src/types/Command.ts
@@ -0,0 +1,6 @@
+export interface ConsoleCommandData {
+ name: string,
+ description: string,
+ usage: string | boolean,
+ aliases?: string[] // should be boolean but makes issues
+}
\ No newline at end of file
diff --git a/src/types/PostRow.ts b/src/types/PostRow.ts
new file mode 100644
index 0000000..a22b15a
--- /dev/null
+++ b/src/types/PostRow.ts
@@ -0,0 +1,10 @@
+export interface PostRow {
+ id: number
+ hash_id: string
+ user_id: number
+ title: string
+ content: string
+ visibility: "public" | "unlisted" | "private"
+ font: 'Verdana' | 'monospace' | 'Bookman Old Style'
+ created_at: string
+};
\ No newline at end of file
diff --git a/src/types/Upload.ts b/src/types/Upload.ts
new file mode 100644
index 0000000..53200b9
--- /dev/null
+++ b/src/types/Upload.ts
@@ -0,0 +1,7 @@
+export interface Upload {
+ id: number,
+ user_id: number,
+ original_name: string,
+ stored_name: string,
+ upload_date: string
+}
\ No newline at end of file
diff --git a/src/types/User.ts b/src/types/User.ts
new file mode 100644
index 0000000..fb4c980
--- /dev/null
+++ b/src/types/User.ts
@@ -0,0 +1,11 @@
+export interface User {
+ id: number;
+ username: string;
+ password_hash: string;
+ created_at: string;
+}
+
+export interface UserSession {
+ id: number,
+ username: string;
+}
\ No newline at end of file
diff --git a/src/types/express-session.d.ts b/src/types/express-session.d.ts
new file mode 100644
index 0000000..a46f093
--- /dev/null
+++ b/src/types/express-session.d.ts
@@ -0,0 +1,10 @@
+import "express-session";
+
+declare module "express-session" {
+ interface SessionData {
+ user?: {
+ id: number;
+ username: string;
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/util/Logger.ts b/src/util/Logger.ts
new file mode 100644
index 0000000..4f67e50
--- /dev/null
+++ b/src/util/Logger.ts
@@ -0,0 +1,66 @@
+import 'colorts/lib/string';
+
+export enum VerboseLevel {
+ NONE = 0, // No logging except for errors
+ WARNS = 1, // Log warns
+ ALL = 2, // Warns and (useless) debug
+ VERBL = 3, // Warns, debug and verbose
+ VERBH = 4, // Warns, debug, verbose and very verbose (thanks copilot this is so funny)
+}
+
+type Color = 'red' | 'green' | 'yellow' | 'blue' | 'magenta' | 'cyan' | 'white' | 'gray' | 'black' | 'italic' | 'bold' | 'underline' | 'strikethrough' | 'inverse' | 'bgRed' | 'bgGreen' | 'bgYellow' | 'bgBlue' | 'bgMagenta' | 'bgCyan' | 'bgWhite' | 'bgBlack' | 'bgGray' | 'bgItalic';
+
+export default class Logger {
+ public static VERBOSE_LEVEL: VerboseLevel = 1;
+
+ constructor(public name: string, public color: Color = 'blue') {
+ this.name = name;
+ this.color = color;
+ }
+
+ private getDate(): string {
+ return new Date().toLocaleTimeString();
+ }
+
+ private raw(...args: string[]) {
+ // @ts-ignore - Element implicitly has an 'any' type because index expression is not of type 'number'
+ console.log(`[${this.getDate().white.bold}] <${this.name[this.color].bold}>`, ...args);
+ }
+
+ public log(...args: string[]) {
+ this.raw(...args);
+ }
+
+ public trail(...args: any[]) {
+ console.log(`\t↳ ${args.join(' ').gray}`);
+ }
+
+ public error(e: Error | string, stack: boolean = true) {
+ if (typeof e === 'string') e = new Error(e);
+ console.log(`[${this.getDate().white.bold}] ${`ERROR<${this.name}>`.bgRed.bold}`, e.message);
+ if (e.stack && stack) this.trail(e.stack);
+ }
+
+ public warn(...args: string[]) {
+ if (Logger.VERBOSE_LEVEL < VerboseLevel.WARNS) return;
+ console.log(`[${this.getDate().white.bold}] ${`WARN<${this.name}>`.bgYellow.bold}`, ...args);
+ }
+
+ public debug(...args: any) {
+ if (Logger.VERBOSE_LEVEL < VerboseLevel.ALL) return;
+ console.log(`[${this.getDate().white.bold}] ${`DEBUG<${this.name}>`.bgBlue.bold}`, ...args);
+ this.trail(new Error().stack!.split('\n').slice(2).join('\n'));
+ }
+
+ public verbL(...args: any) {
+ if (Logger.VERBOSE_LEVEL < VerboseLevel.VERBL) return;
+ console.log(`[${this.getDate().white.bold}] ${`VERBL<${this.name}>`.bgCyan.bold}`, ...args);
+ this.trail(new Error().stack!.split('\n').slice(2).join('\n'));
+ }
+
+ public verbH(...args: any) {
+ if (Logger.VERBOSE_LEVEL < VerboseLevel.VERBH) return;
+ console.log(`[${this.getDate().white.bold}] ${`VERBH<${this.name}>`.bgCyan.bold}`, ...args);
+ this.trail(new Error().stack!.split('\n').slice(2).join('\n'));
+ }
+}
\ No newline at end of file
diff --git a/src/util/Logger2.ts b/src/util/Logger2.ts
new file mode 100644
index 0000000..bee1434
--- /dev/null
+++ b/src/util/Logger2.ts
@@ -0,0 +1,68 @@
+import 'colorts/lib/string';
+// crepesr ♥
+
+
+export enum VerboseLevel {
+ NONE = 0, // No logging except for errors
+ WARNS = 1, // Log warns
+ ALL = 2, // Warns and (useless) debug
+ VERBL = 3, // Warns, debug and verbose
+ VERBH = 4, // Warns, debug, verbose and very verbose (thanks copilot this is so funny)
+}
+
+type Color = 'red' | 'green' | 'yellow' | 'blue' | 'magenta' | 'cyan' | 'white' | 'gray' | 'black' | 'italic' | 'bold' | 'underline' | 'strikethrough' | 'inverse' | 'bgRed' | 'bgGreen' | 'bgYellow' | 'bgBlue' | 'bgMagenta' | 'bgCyan' | 'bgWhite' | 'bgBlack' | 'bgGray' | 'bgItalic';
+
+export default class Logger {
+ public static VERBOSE_LEVEL: VerboseLevel = 1;
+
+ constructor(public name: string, public color: Color = 'blue') {
+ this.name = name;
+ this.color = color;
+ }
+
+ private getDate(): string {
+ return new Date().toLocaleTimeString();
+ }
+
+ private raw(...args: string[]) {
+ // @ts-ignore - Element implicitly has an 'any' type because index expression is not of type 'number'
+ console.log(`[${this.getDate().white.bold}] <${this.name[this.color].bold}>`, ...args);
+ }
+
+ public log(...args: string[]) {
+ this.raw(...args);
+ }
+
+ public trail(...args: any[]) {
+ console.log(`\t↳ ${args.join(' ').gray}`);
+ }
+
+ public error(e: Error | string, stack: boolean = true) {
+ if (typeof e === 'string') e = new Error(e);
+ console.log(`[${this.getDate().white.bold}] ${`ERROR<${this.name}>`.bgRed.bold}`, e.message);
+ if (e.stack && stack) this.trail(e.stack);
+ }
+
+ public warn(...args: string[]) {
+ if (Logger.VERBOSE_LEVEL < VerboseLevel.WARNS) return;
+ console.log(`[${this.getDate().white.bold}] ${`WARN<${this.name}>`.bgYellow.bold}`, ...args);
+ }
+
+ public debug(...args: any) {
+ if (Logger.VERBOSE_LEVEL < VerboseLevel.ALL) return;
+ console.log(`[${this.getDate().white.bold}] ${`DEBUG<${this.name}>`.bgBlue.bold}`, ...args);
+ this.trail(new Error().stack!.split('\n').slice(2).join('\n'));
+ }
+
+ public verbL(...args: any) {
+ if (Logger.VERBOSE_LEVEL < VerboseLevel.VERBL) return;
+ console.log(`[${this.getDate().white.bold}] ${`VERBL<${this.name}>`.bgCyan.bold}`, ...args);
+ this.trail(new Error().stack!.split('\n').slice(2).join('\n'));
+ }
+
+ public verbH(...args: any) {
+ if (Logger.VERBOSE_LEVEL < VerboseLevel.VERBH) return;
+ console.log(`[${this.getDate().white.bold}] ${`VERBH<${this.name}>`.bgCyan.bold}`, ...args);
+ this.trail(new Error().stack!.split('\n').slice(2).join('\n'));
+ }
+}
\ No newline at end of file
diff --git a/src/util/Prompt.ts b/src/util/Prompt.ts
new file mode 100644
index 0000000..b2b50c8
--- /dev/null
+++ b/src/util/Prompt.ts
@@ -0,0 +1,315 @@
+// import from jtw 🤤
+// all log comments here r for debug dont remove
+
+
+import readline from 'readline'
+import Logger from './Logger'
+import fs from 'fs'
+import path from 'node:path'
+import { ConsoleCommandData } from '../types/Command'
+
+var c = new Logger("COMMAND", "yellow")
+
+const rl = readline.createInterface({
+ input: process.stdin as unknown as NodeJS.ReadableStream,
+ output: process.stdout as unknown as NodeJS.WritableStream
+})
+
+const originalWrite = process.stdout.write.bind(process.stdout)
+let inWritePatch = false
+
+process.stdout.write = (chunk: any, encoding?: any, cb?: any) => {
+ if (inWritePatch) {
+ // Don't recurse
+ return originalWrite(chunk, encoding, cb)
+ }
+
+ inWritePatch = true
+
+ try {
+ readline.clearLine(process.stdout as unknown as NodeJS.WritableStream, 0)
+ readline.cursorTo(process.stdout as unknown as NodeJS.WritableStream, 0)
+
+ const result = originalWrite(chunk, encoding, cb)
+
+ rl.prompt(true)
+ return result
+ } finally {
+ inWritePatch = false
+ }
+}
+
+// helper
+export function splitStr(q: string): Array {
+ return q.split(" ")
+}
+
+// clears last printed line
+// right now this just prints another line...
+export function removeLine() {
+ readline.moveCursor(process.stdout as unknown as NodeJS.WritableStream, 0, -1)
+ readline.clearLine(process.stdout as unknown as NodeJS.WritableStream, 1)
+}
+
+// repeat
+export function Prompt() {
+ rl.prompt()
+
+ rl.once("line", cmd => {
+ if (!cmd.trim()) {
+ Prompt()
+ } else {
+ HandlePrompt(splitStr(cmd.trim()))
+ Prompt()
+ }
+ })
+}
+
+export const Clear: string = "$clear"
+export var CommandAliases: ConsoleCommandData[] = []
+export var CommandFiles: string[] = []
+export var AllFilesProcessed: boolean = false
+export var AllAliasesProcessed: boolean = false
+
+function removePreviousLine() {
+ readline.moveCursor(process.stdout, 0, -1);
+ readline.clearLine(process.stdout, 0);
+ readline.cursorTo(process.stdout, 0);
+}
+
+export async function HandlePrompt(args: Array) {
+ let inputName = args[0]//.toLowerCase() // i don't know why i did this this is actually why setpvf was removed
+ let cmdArgs: Array = args.slice(1)
+ let error = false
+
+ try {
+ removePreviousLine();
+ console.log(`> ${inputName} ${cmdArgs.join(' ')}`);
+ const directoryPath = `${process.cwd()}/output/src/cmd/`; // Replace with your directory path
+
+ try {
+ const files = fs.readdirSync(directoryPath, 'utf-8');
+ //c.log(`HI ${files[0]}`)
+ if (!AllFilesProcessed)
+ for (let i = 0; i < files.length; i++) {
+ var file = files[i]
+ let check = true
+ for (var file2 in CommandFiles)
+ if (file2.includes(file))
+ check = false
+
+ if (!file.includes('.map') && check) {
+ //c.log(file)
+ CommandFiles.push(file.replace(".ts", ".js"))
+ //c.log(`${file}`)
+ }
+ }
+ AllFilesProcessed = true
+ //c.log(CommandFiles.length.toString() + " CommandFile")
+
+ } catch (err) {
+ console.error('Error reading directory:', err);
+ }
+ if (!AllAliasesProcessed)
+ for (let i = 0; i < CommandFiles.length; i++) {
+ var CommandFile = CommandFiles[i]
+ const file: ConsoleCommandData = require(`${process.cwd()}/output/src/cmd/${CommandFile}`).metadata
+ //c.log(file.name)
+ if (file.aliases)
+ CommandAliases.push(file)
+ AllAliasesProcessed = true
+ }
+ for (let i = 0; i < CommandAliases.length; i++) {
+ const cmd = CommandAliases[i];
+
+ if (cmd.aliases) {
+ //c.log('checked aaa')
+ for (let i = 0; i < cmd.aliases.length; i++) {
+ var alias = cmd.aliases[i]
+ //c.log(alias)
+ if (alias === inputName)
+ inputName = cmd.name
+ }
+ }
+ }
+
+ let cs = new Logger(`CLIENT/${inputName}.js+resp`)
+
+ //c.log(CommandAliases.length.toString() + " Alias")
+ let response = await require(`${process.cwd()}/output/src/cmd/${inputName}.js`).Main(cmdArgs)
+
+ if (response === Clear)
+ console.clear()
+ else if (response == 0 || response == 1 || !response)
+ "" // do nothing, an error occured.
+ else
+ cs.log(response)
+ } catch (err) {
+ error = true
+ if (err.toString().includes("Cannot find module"))
+ c.log(`Command ${inputName} not found.`)
+ else
+ c.error(`${err}`)
+ }
+}
+
+/*// import from jtw 🤤
+// all log comments here r for debug dont remove
+
+
+import readline from 'readline'
+import Logger from './Logger'
+import { ConsoleCommandData } from '../types/Command'
+import fs from 'fs'
+
+var c = new Logger("COMMAND", "yellow")
+
+const rl = readline.createInterface({
+ input: process.stdin as unknown as NodeJS.ReadableStream,
+ output: process.stdout as unknown as NodeJS.WritableStream
+})
+
+const originalWrite = process.stdout.write.bind(process.stdout)
+let inWritePatch = false
+
+process.stdout.write = (chunk: any, encoding?: any, cb?: any) => {
+ if (inWritePatch) {
+ // Don't recurse
+ return originalWrite(chunk, encoding, cb)
+ }
+
+ inWritePatch = true
+
+ try {
+ readline.clearLine(process.stdout as unknown as NodeJS.WritableStream, 0)
+ readline.cursorTo(process.stdout as unknown as NodeJS.WritableStream, 0)
+
+ const result = originalWrite(chunk, encoding, cb)
+
+ rl.prompt(true)
+ return result
+ } finally {
+ inWritePatch = false
+ }
+}
+
+// helper
+export function splitStr(q: string): Array {
+ return q.split(" ")
+}
+
+// clears last printed line
+// right now this just prints another line...
+export function removeLine() {
+ readline.moveCursor(process.stdout as unknown as NodeJS.WritableStream, 0, -1)
+ readline.clearLine(process.stdout as unknown as NodeJS.WritableStream, 1)
+}
+
+// repeat
+export function Prompt() {
+ rl.prompt()
+
+ rl.once("line", async (cmd) => {
+ if (!cmd.trim()) {
+ Prompt()
+ } else {
+ await HandlePrompt(splitStr(cmd.trim()))
+ Prompt()
+ }
+ })
+}
+
+// For exit/Runtime
+export function EmergencyPrompt() {
+ rl.prompt()
+
+ rl.once("line", async (cmd) => {
+ if (!cmd.trim()) {
+ Prompt()
+ } else {
+ if (splitStr(cmd.trim())[0].toLowerCase() === 'exit')
+ process.exit(0)
+ Prompt()
+ }
+ })
+}
+
+export const Clear: string = "$clear"
+export var CommandAliases: ConsoleCommandData[] = []
+export var CommandFiles: string[] = []
+export var AllFilesProcessed: boolean = false
+export var AllAliasesProcessed: boolean = false
+
+export async function HandlePrompt(args: Array) {
+ let inputName = args[0].toLowerCase()
+ let cmdArgs: Array = args.slice(1)
+ let error = false
+ //c.log(`${process.cwd()}`)
+ try {
+ let cs = new Logger(`CLIENT/${inputName}.js+resp`)
+ c.log(`${inputName} ${cmdArgs.join(' ')}`)
+ const directoryPath = process.cwd() + '/output/src/cmd'; // Replace with your directory path
+
+ try {
+ const files = fs.readdirSync(directoryPath, 'utf-8');
+ //c.log(`HI ${files[0]}`)
+ if (!AllFilesProcessed)
+ for (let i = 0; i < files.length; i++) {
+ var file = files[i]
+ let check = true
+ for (var file2 in CommandFiles)
+ if (file2.includes(file))
+ check = false
+
+ if (!file.includes('.map') && check) {
+ //c.log(file)
+ CommandFiles.push(file.replace(".ts", ".js"))
+ //c.log(`${file}`)
+ }
+ }
+ AllFilesProcessed = true
+ //c.log(CommandFiles.length.toString() + " CommandFile")
+
+ } catch (err) {
+ console.error('Error reading directory:', err);
+ }
+ if (!AllAliasesProcessed)
+ for (let i = 0; i < CommandFiles.length; i++) {
+ var CommandFile = CommandFiles[i]
+ const file: ConsoleCommandData = require(`${process.cwd()}/output/src/cmd/${CommandFile}`).metadata
+ //c.log(file.name)
+ if (file.aliases)
+ CommandAliases.push(file)
+ AllAliasesProcessed = true
+ }
+ for (let i = 0; i < CommandAliases.length; i++) {
+ const cmd = CommandAliases[i];
+
+ if (cmd.aliases) {
+ //c.log('checked aaa')
+ for (let i = 0; i < cmd.aliases.length; i++) {
+ var alias = cmd.aliases[i]
+ //c.log(alias)
+ if (alias === inputName)
+ inputName = cmd.name
+ }
+ }
+ }
+
+ //c.log(CommandAliases.length.toString() + " Alias")
+ let response = await require(`${process.cwd()}/output/src/cmd/${inputName}.js`).Main(cmdArgs)
+
+ if (response === Clear)
+ console.clear()
+ else if (response == 0 || response == 1)
+ "" // do nothing, an error occured.
+ else
+ cs.log(response)
+ } catch (err) {
+ //@ts-ignore
+ if (err.toString().includes("Cannot find module" || "ENOENT"))
+ c.log(`Command ${inputName} not found.`)
+ else
+ c.error(`${err}`)
+ }
+}*/
\ No newline at end of file
diff --git a/src/util/Reload.ts b/src/util/Reload.ts
new file mode 100644
index 0000000..62d0b3f
--- /dev/null
+++ b/src/util/Reload.ts
@@ -0,0 +1,12 @@
+const Reload = () => setTimeout(function () {
+ process.on("exit", function () {
+ require("child_process").spawn(process.argv.shift(), process.argv, {
+ cwd: process.cwd(),
+ detached : true,
+ stdio: "inherit"
+ });
+ });
+ process.exit();
+}, 5000);
+
+export default Reload;
\ No newline at end of file
diff --git a/src/util/SetPasswd.ts b/src/util/SetPasswd.ts
new file mode 100644
index 0000000..42a69e1
--- /dev/null
+++ b/src/util/SetPasswd.ts
@@ -0,0 +1,18 @@
+import fs from 'fs/promises'
+import PasswordGen from './passwordGen'
+import Logger from './Logger'
+const l = new Logger("Server(PvfPw)", "gray")
+
+export const getFileNameFromPath = (path: string) => {
+ return path.replace(/^.*[\\/]/, '')
+}
+
+export default function SetRndPasswd(OriginalFilePath: string): string | void {
+ const Password = PasswordGen(48);
+
+ fs.writeFile(`${OriginalFilePath}.acck`, Password)
+ //l.log(`Wrote password for ${getFileNameFromPath(OriginalFilePath)}.passwd: ${Password} `)
+ l.log(`Wrote AccessKey for ${OriginalFilePath}`)
+
+ return Password;
+}
diff --git a/src/util/database.ts b/src/util/database.ts
new file mode 100644
index 0000000..20a80a9
--- /dev/null
+++ b/src/util/database.ts
@@ -0,0 +1,61 @@
+import sqlite3, { Database } from "sqlite3";
+import path from "path";
+
+var db: Database = null;
+var db_file: Database = null;
+var failed = false;
+
+try {
+ const dbPath = path.join(process.cwd(), "users.db");
+ db = new sqlite3.Database(dbPath);
+ db.serialize(() => {
+ db.run(`
+ CREATE TABLE IF NOT EXISTS users (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ username TEXT UNIQUE NOT NULL,
+ password_hash TEXT NOT NULL,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
+ )
+ `);
+
+ db.run(`
+ CREATE TABLE IF NOT EXISTS posts (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ hash_id TEXT UNIQUE,
+ user_id INTEGER NOT NULL,
+ title TEXT NOT NULL,
+ content TEXT NOT NULL,
+ visibility TEXT NOT NULL DEFAULT 'public',
+ font TEXT NOT NULL DEFAULT 'Verdana',
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
+ );
+ `)
+ });
+
+ const dbPath_f = path.join(process.cwd(), "files.db")
+ db_file = new sqlite3.Database(dbPath_f);
+
+ db_file.serialize(() => {
+ db_file.run(`
+ CREATE TABLE IF NOT EXISTS files (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id INTEGER NOT NULL,
+ original_name TEXT NOT NULL,
+ stored_name TEXT NOT NULL,
+ upload_date DATETIME DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (user_id) REFERENCES users(id)
+ )
+ `);
+ db_file.run(
+ `ATTACH DATABASE ? AS udb`,
+ [path.join(process.cwd(), "users.db")]
+ )
+ });
+} catch (err) {
+ failed = true;
+ console.error(`Failed to initialize databases:\n${err}`)
+}
+
+export {
+ db, db_file
+}
\ No newline at end of file
diff --git a/src/util/dateParse.ts b/src/util/dateParse.ts
new file mode 100644
index 0000000..29ef7bd
--- /dev/null
+++ b/src/util/dateParse.ts
@@ -0,0 +1,39 @@
+export function parseDate(input: string, useRtf = true, showTime: boolean = true, timeZone?: string): string {
+ const date = new Date(input.replace(" ", "T"));
+ const now = new Date();
+
+ const tz = timeZone || Intl.DateTimeFormat().resolvedOptions().timeZone;
+
+ const diffSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
+
+ if (useRtf) {
+ const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
+
+ if (diffSeconds < 60) return rtf.format(-diffSeconds, "second");
+ if (diffSeconds < 3600) return rtf.format(-Math.floor(diffSeconds / 60), "minute");
+ if (diffSeconds < 86400) return rtf.format(-Math.floor(diffSeconds / 3600), "hour");
+ if (diffSeconds < 604800) return rtf.format(-Math.floor(diffSeconds / 86400), "day");
+ }
+ return date.toLocaleString("en-US", {
+ month: "long",
+ day: "numeric",
+ year: "numeric",
+ ...(showTime && {
+ hour: "numeric",
+ minute: "2-digit",
+ hour12: true,
+ timeZoneName: "short"
+ }),
+ timeZone: tz
+ });
+}
+
+import { DateTime } from "luxon";
+
+export function convertToIso(dbTime: string, serverZone: string): string {
+ const zone = serverZone || Intl.DateTimeFormat().resolvedOptions().timeZone;
+
+ const dt = DateTime.fromFormat(dbTime, "yyyy-MM-dd HH:mm:ss", { zone });
+
+ return dt.toISO();
+}
diff --git a/src/util/dbg.ts b/src/util/dbg.ts
new file mode 100644
index 0000000..245f8bb
--- /dev/null
+++ b/src/util/dbg.ts
@@ -0,0 +1,19 @@
+import sqlite3, { Database } from "sqlite3";
+
+export function db(dbname: string = null, dbliteral: Database) {
+ if (dbliteral)
+ var db = dbliteral
+ else
+ var db = new sqlite3.Database(dbname);
+ db.all("PRAGMA database_list;", (err, rows) => {
+ if (err) {
+ console.error("Error getting DB list:", err);
+ return;
+ }
+
+ console.log("Attached databases:");
+ rows.forEach((row: any) => {
+ console.log(`- name: ${row.name}, file: ${row.file}`);
+ });
+ });
+}
\ No newline at end of file
diff --git a/src/util/idGen.ts b/src/util/idGen.ts
new file mode 100644
index 0000000..83eadae
--- /dev/null
+++ b/src/util/idGen.ts
@@ -0,0 +1,21 @@
+import crypto from "crypto"
+import Logger from "./Logger"
+
+const c = new Logger("Server(Pwd)")
+
+export default function GenerateID(bs: number): string {
+ try {
+ const buffer = crypto.randomBytes(bs)
+
+ let id = buffer.toString("base64")
+
+ id = id.replace(/[+/=]/g, "")
+
+ return id || "Failed to generate an ID"
+ } catch (err) {
+ c.log(
+ "Server cannot generate a unique ID."
+ )
+ return "Failed to generate an ID"
+ }
+}
\ No newline at end of file
diff --git a/src/util/index.ts b/src/util/index.ts
new file mode 100644
index 0000000..6a781d0
--- /dev/null
+++ b/src/util/index.ts
@@ -0,0 +1,88 @@
+export function parseInt(args: any): any {
+ const n = Number.parseInt(args)
+ if (Number.isInteger(n))
+ return n
+ else
+ throw new Error(`${args} is not an integer`) // so its easier
+}
+
+export function escapeHtml(input: string): string {
+ return input
+ .replaceAll(/&/g, "&")
+ .replaceAll(//g, ">")
+ .replaceAll(/"/g, """)
+ .replaceAll(/'/g, "'");
+}
+
+export function bytesToSize(bytes: number, decimals = 2) {
+ if (!Number(bytes)) {
+ return '0 B';
+ }
+
+ const kbToBytes = 1000;
+ const dm = decimals < 0 ? 0 : decimals;
+ const sizes = [
+ 'B',
+ 'KB',
+ 'MB',
+ 'GB',
+ 'TB',
+ 'PB',
+ 'EB',
+ 'ZB',
+ 'YB',
+ ];
+
+ const index = Math.floor(
+ Math.log(bytes) / Math.log(kbToBytes),
+ );
+
+ return `${parseFloat(
+ (bytes / Math.pow(kbToBytes, index)).toFixed(dm),
+ )} ${sizes[index]}`;
+}
+
+function waitUntil(conditionFn: () => boolean, interval = 100): Promise {
+ return new Promise((resolve) => {
+ const timer = setInterval(() => {
+ try {
+ if (conditionFn()) {
+ clearInterval(timer);
+ resolve();
+ }
+ } catch (err) {
+ clearInterval(timer);
+ }
+ }, interval);
+ });
+}
+
+import fs from 'fs'
+export function mkDirManIfNotExists(path: string): boolean {
+ try {
+ if (!fs.existsSync(path))
+ fs.mkdir(path, () => { return true });
+
+ return true
+ } catch {
+ return false
+ }
+}
+
+export async function mkDirsManIfNotExists(paths: string[]): Promise {
+ try {
+ var pass = false
+
+ for (const path in paths) {
+ await waitUntil(() => pass = true)
+
+ pass = false
+ if (!fs.existsSync(path))
+ fs.mkdir(path, () => pass = true);
+ }
+ return true
+ } catch {
+ return false
+ }
+}
\ No newline at end of file
diff --git a/src/util/passwordGen.ts b/src/util/passwordGen.ts
new file mode 100644
index 0000000..ac88ab6
--- /dev/null
+++ b/src/util/passwordGen.ts
@@ -0,0 +1,19 @@
+import chp from 'child_process'
+import Logger from './Logger'
+import GenerateID from './idGen'
+const c = new Logger("Server(Pwd)")
+
+// ill replace this later....
+export default function PasswordGen(bs: number): string {
+ try {
+ const cmdOutput: string = GenerateID(bs);
+ if (!cmdOutput) {
+ c.log(`Password generate output is empty...`)
+ return null;
+ } else
+ return cmdOutput;
+ } catch (err) {
+ c.log("Server cannot generate a safe password.")
+ return null;
+ }
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..291903d
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "compilerOptions": {
+ "target": "esnext",
+ "module": "NodeNext",
+ "sourceMap": true,
+ "experimentalDecorators": true,
+ "rootDir": "./",
+ "outDir": "output",
+ "strict": true,
+ "moduleResolution":"nodenext",
+ "noImplicitAny": true,
+ "esModuleInterop": true,
+ "noImplicitThis": false,
+ "resolveJsonModule":true,
+ "strictNullChecks":false,
+ "noImplicitReturns":false,
+ "skipLibCheck": true
+ },
+ "exclude": ["node_modules"]
+}
\ No newline at end of file