From a081f51cc6398ea591399c524ca43db107f22eb4 Mon Sep 17 00:00:00 2001 From: Maki Date: Sun, 4 Jan 2026 18:00:24 +0800 Subject: [PATCH] Initial commit --- .gitignore | 14 ++ README.md | 8 + package.json | 67 +++++++ src/cmd/clear.ts | 14 ++ src/cmd/delete.ts | 81 ++++++++ src/cmd/eval.ts | 30 +++ src/cmd/exit.ts | 39 ++++ src/cmd/help.ts | 62 +++++++ src/cmd/idGen.ts | 37 ++++ src/cmd/register.ts | 55 ++++++ src/cmd/restart.ts | 32 ++++ src/cmd/say.ts | 17 ++ src/components/pages_text.ts | 44 +++++ src/components/styles.ts | 304 ++++++++++++++++++++++++++++++ src/favicon.ico | Bin 0 -> 4286 bytes src/index.ts | 186 +++++++++++++++++++ src/listeners/GetApi.ts | 58 ++++++ src/listeners/GetPrivateFile.ts | 124 +++++++++++++ src/pages/404.ts | 85 +++++++++ src/pages/login.ts | 99 ++++++++++ src/pages/posts.ts | 314 +++++++++++++++++++++++++++++++ src/pages/register.ts | 179 ++++++++++++++++++ src/pages/root.ts | 52 ++++++ src/pages/upload.ts | 176 ++++++++++++++++++ src/pages/uploads.ts | 95 ++++++++++ src/types/Command.ts | 6 + src/types/PostRow.ts | 10 + src/types/Upload.ts | 7 + src/types/User.ts | 11 ++ src/types/express-session.d.ts | 10 + src/util/Logger.ts | 66 +++++++ src/util/Logger2.ts | 68 +++++++ src/util/Prompt.ts | 315 ++++++++++++++++++++++++++++++++ src/util/Reload.ts | 12 ++ src/util/SetPasswd.ts | 18 ++ src/util/database.ts | 61 +++++++ src/util/dateParse.ts | 39 ++++ src/util/dbg.ts | 19 ++ src/util/idGen.ts | 21 +++ src/util/index.ts | 88 +++++++++ src/util/passwordGen.ts | 19 ++ tsconfig.json | 20 ++ 42 files changed, 2962 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 package.json create mode 100644 src/cmd/clear.ts create mode 100644 src/cmd/delete.ts create mode 100644 src/cmd/eval.ts create mode 100644 src/cmd/exit.ts create mode 100644 src/cmd/help.ts create mode 100644 src/cmd/idGen.ts create mode 100644 src/cmd/register.ts create mode 100644 src/cmd/restart.ts create mode 100644 src/cmd/say.ts create mode 100644 src/components/pages_text.ts create mode 100644 src/components/styles.ts create mode 100644 src/favicon.ico create mode 100644 src/index.ts create mode 100644 src/listeners/GetApi.ts create mode 100644 src/listeners/GetPrivateFile.ts create mode 100644 src/pages/404.ts create mode 100644 src/pages/login.ts create mode 100644 src/pages/posts.ts create mode 100644 src/pages/register.ts create mode 100644 src/pages/root.ts create mode 100644 src/pages/upload.ts create mode 100644 src/pages/uploads.ts create mode 100644 src/types/Command.ts create mode 100644 src/types/PostRow.ts create mode 100644 src/types/Upload.ts create mode 100644 src/types/User.ts create mode 100644 src/types/express-session.d.ts create mode 100644 src/util/Logger.ts create mode 100644 src/util/Logger2.ts create mode 100644 src/util/Prompt.ts create mode 100644 src/util/Reload.ts create mode 100644 src/util/SetPasswd.ts create mode 100644 src/util/database.ts create mode 100644 src/util/dateParse.ts create mode 100644 src/util/dbg.ts create mode 100644 src/util/idGen.ts create mode 100644 src/util/index.ts create mode 100644 src/util/passwordGen.ts create mode 100644 tsconfig.json 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: '/' } +] + +/* +
+ Pages
+ ) /staticfiles
+ ) Home +
+*/ + +const pages_string = (activepage: string) => { + var npages = [...pages]; + var base = ` +
+ Pages
+ + ` + + if (activepage == "/register") + npages.push({ name: "login", addr: "/login" }) + else if (activepage == "/login") + npages.push({ name: "register", addr: "/register" }) + + for (var i = 0; i < npages.length; i++) { + const page = npages[i]; + if (activepage !== page.addr) + base += `) ${page.name}
+ + ` + } + + npages = pages; // reset the values because if u cycle through register and login itll repeat itself ;-; + + 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 0000000000000000000000000000000000000000..9be32da69fa44680d59f2f35d79ed9d9d57314f3 GIT binary patch literal 4286 zcmcIoYfzL`7(QIou!}f0+4uu5;fz{p<7DDE4r+pO5wh$eh?hb%%pA2WFPTWfq@!6W zU}Z^`itTa<3rx|Cu?vV;Q;}&CG&ZG(T9T!r`}RC%_glW*b{DW(=i$8HIq&;C=RN2B z&UZ$o0`RX-AH~0FSEy3ADWwJg=u%?<_Uj+UBAOg1 z$AFVS6VMF&1+)NmK%Ah>K913czKmfkImnd{WC7H;51=Nsw*V)Ai-4Ka;Qp^$YEbJ3 zfZA~Y=k)EcQ(=laTav8z0Y2R??@kQMttd^)sH|?RD_Jb$B5F%l$mpOH6T{IS zi-*Re9{PLAb0h)W@zvL4#Ex&IZ$^QHJ-bbVHv2e6ANrd0$wf|b1NS<$TvKp9_d@## zpnbjRCnCp)UEj*h_b-*Zv1PwX$b(BnjZc-p zsJR-nL#8azWAtfb{|%iVo!ax!wzNwhwLR&uUBWZ1T3jkNl!-ze}&hZ%ANFx(03bajXOSoP!1UzC-%lC+Ng{`lo*9`QFoJA9*^c zPwfj{^$)wBf5!QveW}G0XG-D56v@oZmLZ!C%IynsBy`pq8S~LE!dfS2htA59p7URmJ2&o=RhzS<_>~md zx^|{K_Ds5T2iL&$Wv)Fk&4b!*>(BKZwzW!bocg>3On5>9P}`lNAD7_hRMh-z35uPk zb;j>M>(Zy)^Qlb|95>(L6O}5R5fg}5g?F^;Bg-|X-mBK!jrDJ>Kjad1;Jl1@?`s)d z(xBTx8}>@S)$hw4nHIV0)e0G9IUzBhU2@L_ak1zdU0g3iax0}j&cS{eyJf)I4`q0M zt&A?J*IZEt&bh}qYTxz#aemYbh@K<2OwW|Tn`|=i&_zi++FEN~axnJNRgd&x3}Xjx zwn?`cnVOT_9`SttTF)QkF|Pcy^j*1Ax=el=Yd>3h%w8uW3%-K}-hKDA^Yd)iF+I}O zX%2GrS-At;r_A{q=a1X@%X*?#!G%;N|CoaTV9K`qisvk{9P1A?##u&zi zEzOt6(vzCg!R@Mjy|$BWsEmJCpW*p)HJ^TZubwf zK5g=h+*K#H&fS1JPMud?`b6w>_|VsnHuGN#==%pRL9hwe{R_6SMyA#-v7cXdKleQS za1Lr8|F!CN|1#-w4!j$-o(G=%|NhzQ{nKL(WB;c)FfVfJ^`@rh&lp3?dw3;a2TXf# zT{bz$MNXgE##{^^w5$i-QyYQnodaXZq3_#h4VgAu`T{%~cu(cKS`WD0)9~zVUi4`I z@&Lw?qfI?u4vYnYfq?+eYJM-cm(76j{ib~!;23>)rZUDCt=E2=QbE5d)vZCPF0@zS gzYYj>oj)Vq2CfEhlKYZUy{{@2X; + 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."} +
+
+ Account created successfully!
+ ${req.session.user ? ` +

Logged in as ${req.session.user.username}
+ log out

+ ` : ` + Log in | Register
+ `} +

+ Pages
+ ) /staticfiles
+ ) Home +
+ + `); + } + ); + } 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
+ `} +
+
+ ${pages_string("/")} +
+` + ) + }) +} \ 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}) ${f.original_name} - uploaded on + + + + | +

+ `; + 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