Initial commit

This commit is contained in:
Maki
2026-01-04 18:00:24 +08:00
commit a081f51cc6
42 changed files with 2962 additions and 0 deletions
+14
View File
@@ -0,0 +1,14 @@
/node_modules
/dist
/build
package-lock.json
/pvfiles/**/**.log
/pvfiles/**/**.log.acck
/output
/uploads
files.db
users.db
+8
View File
@@ -0,0 +1,8 @@
# [jtw-storage-site](#jtw-storage-site)
express storage site
### Requirements/Prerequisites
- Node.js 18^
- Good internet
Use `reg <user> <password>` to create accounts. This application may not always be safe.
+67
View File
@@ -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"
}
}
+14
View File
@@ -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>): string {
console.clear()
return "Cleared console."
}
+81
View File
@@ -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 <username>",
aliases: [ "del" ]
}
export async function Main(args: Array<string>): Promise<any> {
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.`)
}
)
}
)
}
)
}
)
})
}
+30
View File
@@ -0,0 +1,30 @@
import { ConsoleCommandData } from "../types/Command";
export const metadata: ConsoleCommandData = {
name: "eval",
description: "Evaluates JavaScript code.",
usage: `eval ${'<code>'['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}`
}
+39
View File
@@ -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>): 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()
}
+62
View File
@@ -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<string>) {
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
}
}
+37
View File
@@ -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 <bit length> [amount]",
aliases: ["idgen"]
}
export function Main(args: Array<string>): 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 <bytesize> [amount]"
else
return `An error occured while running the command: ${err}`
}
}
+55
View File
@@ -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 <username> <password>",
aliases: ["reg"]
}
export async function Main(args: Array<string>): Promise<any> {
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;
}
+32
View File
@@ -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>): 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."
}
+17
View File
@@ -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 <args...>",
}
// test cmd (why)
export function Main(args: Array<string>): string {
if (!args)
return Clear;
return args.join(' ')
}
+44
View File
@@ -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: '/' }
]
/*
<div class="extra">
Pages<br>
) <a href="/staticfiles">/staticfiles</a><br>
) <a href="/">Home</a>
</div>
*/
const pages_string = (activepage: string) => {
var npages = [...pages];
var base = `
<div class="extra">
Pages<br>
`
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 += `) <a href="${page.addr}">${page.name}</a><br>
`
}
npages = pages; // reset the values because if u cycle through register and login itll repeat itself ;-;
base += `</div>`
return base;
}
export default pages_string;
+304
View File
@@ -0,0 +1,304 @@
const styles_string = (add: string = null, override: string = null) => {
var out = `<style>
h1 {
font-size: 50
}
body {
margin-right: 10%;
margin-left: 10%;
margin-bottom: 10%;
text-align: left;
color: black;
font-family: Verdana, sans-serif;
}
a:link {
color: #3c72a3
}
a:visited {
color: #3c72a3
}
@media screen and (prefers-color-scheme: dark) {
body {
background-color: #383737;
color: #b0acac;
}
.tonly {
color: #b0acac;
}
}
@media screen and (prefers-color-scheme: light) {
body {
background-color: #e3e3e3;
color: black;
}
.tonly {
color: black;
}
}
.description {
font-size: 32px
}
.details {
font-size: 20px
}
.extra {
font-size: 24px
}
.form-box {
font-size: 20px;
}
input {
font-size: 18px;
width: 30%;
padding: 6px;
margin: 5px 0;
}
button {
font-size: 18px;
padding: 8px 16px;
cursor: pointer;
}
.post-form {
max-width: 60%;
padding: 20px;
margin: 20px 0;
background-color: rgba(0, 0, 0, 0.03);
border-radius: 8px;
}
.post-form label {
display: block;
margin-top: 14px;
margin-bottom: 6px;
font-size: 16px;
font-weight: bold;
}
.post-form input[type="text"],
.post-form textarea,
.post-form select {
width: 100%;
box-sizing: border-box;
font-family: inherit;
font-size: 18px;
padding: 10px;
border-radius: 6px;
border: 1px solid #aaa;
background-color: #fff;
}
.post-form input {
font-family: inherit;
padding: 10px;
width: 100%;
box-sizing: border-box;
border-radius: 6px;
font-size: 18px
}
.post-form textarea {
resize: vertical;
min-height: 140px;
}
.post-form button {
margin-top: 18px;
font-size: 18px;
padding: 10px 18px;
border-radius: 6px;
border: none;
cursor: pointer;
background-color: #3c72a3;
color: white;
}
.poststylebtn {
margin-top: 18px;
font-size: 18px;
padding: 10px 18px;
border-radius: 6px;
border: none;
cursor: pointer;
background-color: #3c72a3;
color: white;
}
.post-form button:hover {
background-color: #2f5c86;
}
@media screen and (prefers-color-scheme: dark) {
.post-form {
background-color: #2f2f2f;
}
.post-form input,
.post-form textarea,
.post-form select {
background-color: #1f1f1f;
color: #b0acac;
border: 1px solid #555;
}
.post-form button {
background-color: #4a88c2;
}
.post-form button:hover {
background-color: #3a6d9b;
}
}
.tonly {
font-family: inherit;
background: none;
border: none;
margin: 0;
padding: 0;
font-size: inherit;
font-weight: bold;
}
.tonly-formf::file-selector-button {
font-family: inherit;
background: none;
border: none;
margin: 0;
color: inherit;
margin-right: 5%;
padding: 0;
font-size: inherit;
font-weight: bold;
}
.tonly-formf {
font-family: inherit;
font-size: 16px;
}
${add ?? add}
</style>
`
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}\`;
}
`
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

+186
View File
@@ -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)
}
+58
View File
@@ -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.<br>Message content: " + message + "<br>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;
}
})
}
+124
View File
@@ -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(
`<!DOCTYPE html>
<head>
<title>${filename}</title>
</head>
<body>
${styles_string()}
<h1>${process.env.servicename || "File Server"}</h1>
<div class="details">
${/*<p class="details">Local version: ${require('../updateInfo').GameVersion}</p>*/""}
<p>Server region: ${'(alternate) ' + process.env.altregion}</p>
</div>
<hr>
<span style="font-size: 24px">${filename}</span> &nbsp;&nbsp;${size}<br>
<br>
<span style="font-size: 18px;">
<a href='${here_direct}'><button class="tonly">Open file</button></a>
&nbsp;&nbsp;&nbsp;
<a href='${here_dl}'><button class="tonly">Download file</button></a>
</span>
<br><br>
${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}
<br>Input filename: ${filename}
<br>File array: ${files.toString()}
<br>Input filename matches file: ${fs.existsSync.log(fullPath)} | but matches an entry in the files array? ${files.includes(filename)}*/
})
c.log(`GetPrivateFile loaded.`)
}
}
+85
View File
@@ -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}
<br>Server region: ${ipd?.country || '(alternate) ' + process.env.altregion}
<br>Server ping: ${latency}ms
`*/
`<!DOCTYPE html>
<body>
<style>
h1 {
font-size: 50
}
body {
margin-right: 10%;
margin-left: 10%;
margin-bottom: 10%;
text-align: left;
color: black;
font-family: Verdana, sans-serif;
}
a:link {
color: #3c72a3
}
a:visited {
color: #2a567d
}
@media screen and (prefers-color-scheme: dark) {
body {
background-color: #383737;
color: #b0acac;
}
}
@media screen and (prefers-color-scheme: light) {
body {
background-color: #e3e3e3;
color: black;
}
}
.description {
font-size: 32px
}
.details {
font-size: 20px
}
.extra {
font-size: 24px
}
</style>
<h1>${process.env.servicename || "File Server"}</h1>
<div class="details">
${/*<p class="details">Local version: ${require('../updateInfo').GameVersion}</p>*/""}
<p>Server region: ${'(alternate) ' + process.env.altregion}</p>
</div>
<br><hr><br>
<div class="description">
${process.env.notfound || "The file you were looking for does not exist on the server."}
</div>
</body>`
)
})
}
+99
View File
@@ -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 = "<br>", active_page: string = "/login"): string => {
return `<!DOCTYPE html>
<html>
<head>
<title>${process.env.servicename || "File Server"} | Login</title>
</head>
<body>
${styles_string()}
<h1>${process.env.servicename || "File Server"}</h1>
<div class="details">
<p>Server region: ${"(alternate) " + process.env.altregion}</p>
</div>
<hr><br>
<span class="description">
Login
</span>
${`<p id="alerts">${alert}</p>` ? alert : ""}
<form class="post-form" method="POST" action="/login">
<label>Username</label>
<input type="text" name="username" required><br>
<label>Password</label>
<input type="password" name="password" required><br>
<button type="submit">Log in</button>
</form>
<br>
${pages_string(active_page)}
</body>
</html>`
}
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("/");
}
);
});
}
+314
View File
@@ -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 `<!DOCTYPE html>
<html>
${notitleset ? "" : `<title>${process.env.servicename || "File Server"} | ${title}</title>`}
<body>
${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;
}
`)}
<h1>${process.env.servicename || "File Server"}</h1>
<div class="details">
<p>Server region: ${"(alternate) " + process.env.altregion}</p>
</div>
<hr><br>
${title ? `<div class="description">
${title}
</div>` : ""}
${content}
${pages_string(active_page)}
</body>
</html>`
}
const newpost_string = (alert: string = null) => heads("/posts/new", "New Post", `
<form id="changeable_parent" class="post-form" action="/posts" method="POST">
<label>Post title:</label>
<input id="post_changeable" name="title" placeholder="New Post" required><br>
<div class="selectarr">
<div class="c_col">
<label>Visibility:</label>
<select name="visibility">
<option value="public">Public</option>
<option value="unlisted">Unlisted</option>
<option value="private">Private</option>
</select>
</div>
<div class="c_col">
<label>Font:</label>
<select name="font" id="font_sel">
<option value="Verdana">Default</option>
<option value="monospace">Mono</option>
<option value="Bookman Old Style">Serif</option>
</select>
</div>
</div>
<label>Post content:</label>
<textarea id="post_changeable" name="content" placeholder="..." rows="15" cols="75" required></textarea><br>
<button type="submit" class="tonly">Post</button>
</form>
</div>
<script>
const fontSelect = document.getElementById("font_sel");
const postContent = document.getElementById("changeable_parent");
fontSelect.addEventListener("change", () => {
postContent.style.fontFamily = fontSelect.value;
});
postContent.style.fontFamily = fontSelect.value;
</script>
`);
export function getUsernameById(userId: number): Promise<string> {
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<string> {
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 += `
<div class="postlist" onClick="location.href='/posts/${p.hash_id}'">
<a href="/posts/${p.hash_id}">${title}</a> ${p.visibility == "public" ? "" : `(${p.visibility})`} <br>
<small>by ${await getUsernameById(p.user_id)} (<span id="time_span${times}"></span><script>${parse_date_string}; document.getElementById("time_span${times}").innerHTML = parseDate(new Date(toIso("${p.created_at.replace(" ", "T") + ".000Z"}")));</script>)</small><br><br>
${content}
</div>
`
};
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 ? `
<br>
<form action="/posts/new" method="GET">
<button class="tonly">New Post</button>
</form>
` : `<p><a href="/login">Log in</a> to create posts.</p>`}
<hr>
<br>
<div class="postlist_r">
${posts}
</div><br><br>
`));
});
app.get("/posts/new", (req, res) => {
if (!req.session.user)
return res.status(401).send(heads("Posts", `
<title>New Post</title>
<div class="details">
<p>You must be logged in to create a post.</p>
</div>
`, `
<a href="/login">Log in</a> | <a href="/register">Register</a><br><br>
`))
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", "<br>Post not found.<br>"));
}
var font = 'Verdana'
if (post.font)
font = post.font
const user = req.session?.user;
res.send(heads("/posts/:", null, `
<head>
<title>${post.title}</title>
<style>
#font_change_target {
font-family: ${font};
}
</style>
</head>
<span id="font_change_target" style="font-size: 24px">${post.title}</span><br>
by ${await getUsernameById(post.user_id)} ${user && user.id === post.user_id ? `
| <form style="display: inline" action="/posts/${post.id}/delete" method="POST">
<button type="submit" onclick="return confirm('Delete ${post.title}?')" class="tonly">Delete Post</button>
</form>
` : ""}<br>
<small><span id="time_span"></span></small><br><br>
<script>
${parse_date_string}
document.getElementById("time_span").innerHTML = parseDate(new Date(toIso("${post.created_at.replace(" ", "T") + ".000Z"}")));
</script>
<span id="font_change_target">${post.content.replaceAll("\n", "<br>")}</span><br>
<br>
<a href=/posts>Back</a>
<br><br>
`, null, true));
}
);
});
}
+179
View File
@@ -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 = "<br>"): string {
return `<!DOCTYPE html>
<html>
<head>
<title>${process.env.servicename || "File Server"} | Registration</title>
</head>
<body>
${styles_string()}
<h1>${process.env.servicename || "File Server"}</h1>
<div class="details">
<p>Server region: ${"(alternate) " + process.env.altregion}</p>
</div>
<hr><br>
<span class="description">
Registration
</span>
${`<p id="alerts">${alert}</p>` ? alert : ""}
<div class="form-box">
<form class="post-form" method="POST" action="/register">
<label>Username</label>
<input type="text" name="username" required><br>
<label>Password</label>
<input type="password" name="password" required><br>
<button type="submit">Register</button>
</form>
</div>
<br>
${pages_string("/register")}
</body>
</html>`
}
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(`
<!DOCTYPE html>
<body>
<style>
h1 {
font-size: 50
}
body {
margin-right: 10%;
margin-left: 10%;
margin-bottom: 10%;
text-align: left;
color: black;
font-family: Verdana, sans-serif;
}
a:link {
color: #3c72a3
}
a:visited {
color: #2a567d
}
@media screen and (prefers-color-scheme: dark) {
body {
background-color: #383737;
color: #b0acac;
}
}
@media screen and (prefers-color-scheme: light) {
body {
background-color: #e3e3e3;
color: black;
}
}
.description {
font-size: 32px
}
.details {
font-size: 20px
}
.extra {
font-size: 24px
}
</style>
<h1>${process.env.servicename || "File Server"}</h1>
<div class="details">
<p>Server region: ${'(alternate) ' + process.env.altregion}</p>
</div><hr><br>
<div class="description">
${process.env.description || "A simple file server built with Express."}
</div>
<div class="extra">
Account created successfully!<br>
${req.session.user ? `
<p>Logged in as ${req.session.user.username}<br>
<a href=/api/logout>log out</a></p>
` : `
<a href=/login>Log in</a> | <a href=/register>Register</a><br>
`}
<br><br>
Pages<br>
) <a href=/staticfiles>/staticfiles</a><br>
) <a href="/">Home</a>
</div>
</body>
`);
}
);
} 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.`))
});
}
+52
View File
@@ -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}
<br>Server region: ${ipd?.country || '(alternate) ' + process.env.altregion}
<br>Server ping: ${latency}ms
`*/
`<!DOCTYPE html>
<head>
<title>${process.env.servicename || "File Server"}</title>
</head>
<body>
${styles_string()}
<h1>${process.env.servicename || "File Server"}</h1>
<div class="details">
${/*<p class="details">Local version: ${require('../updateInfo').GameVersion}</p>*/""}
<p>Server region: ${'(alternate) ' + process.env.altregion}</p>
</div><hr><br>
<div class="description">
${process.env.description || "A simple file server built with Express."}
</div>
<br>${req.session.user ? `
Logged in as ${req.session.user.username}.
<button class="tonly" onClick="if (confirm('Are you sure you want to log out of ${req.session.user.username}?')) location.href = '/api/logout'">Log out</button></p>
` : `
<a href=/login>Log in</a> | <a href=/register>Register</a><br>
`}
<br>
<div class="extra">
${pages_string("/")}
</div>
</body>`
)
})
}
+176
View File
@@ -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 `<!DOCTYPE html>
<html>
<head>
<title>${process.env.servicename || "File Server"} | Uploads</title>
</head>
<body>
${styles_string()}
<h1>${process.env.servicename || "File Server"}</h1>
<div class="details">
<p>Server region: ${"(alternate) " + process.env.altregion}</p>
</div>
<hr><br>
<div class="description">
Upload
</div>
<p id="alerts">${alert ? alert : ""}</p>
${formreplace ? formreplace : `<div class="form-box">
<form action="/upload" method="POST" enctype="multipart/form-data">
<input style="font-size: 16px" class="tonly-formf" type="file" name="file" required />
<button style="font-size: 16px" class="tonly" type="submit">Upload</button>
</form>
</div>`}
${formreplace ? "" : "<br>"}
<h2>All Uploads</h2>
${files ? files : "None yet!<br>"}<br>
${pages_string("/upload")}
</body>
</html>`
}
const upload_string_home = async (alert: string, formreplace: string = null, user: UserSession): Promise<string> => {
var files = await files_string(user)
return upload_string(alert, formreplace, files);
}
async function files_string(user: UserSession): Promise<string> {
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 += `
<form method="POST" action="/delete" style="display:inline;">
${filect}) <a href="/uploads/${f.stored_name}" target="_blank">${f.original_name}</a> - uploaded on
<span id="time_span${times}"></span>
<script>
${parse_date_string}
document.getElementById("time_span${times}").innerHTML = parseDate(new Date(toIso("${f.upload_date.replace(" ", "T") + ".000Z"}")), false, true);
</script>
<input type="hidden" name="fileId" value="${f.id}">
| <button class="tonly" onclick="return confirm('Delete ${f.original_name}?')" type="submit">Delete</button>
</form><br>
`;
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 <a href=/login>logged in</a> 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))
});
}
});
}
+95
View File
@@ -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<Upload> {
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(`<!DOCTYPE html>
<head>
<title>${useUploadInfo ? upload.original_name : filename}</title>
</head>
<body>
${styles_string()}
<h1>${process.env.servicename || "File Server"}</h1>
<div class="details">
${/*<p class="details">Local version: ${require('../updateInfo').GameVersion}</p>*/""}
<p>Server region: ${'(alternate) ' + process.env.altregion}</p>
</div>
<hr>
<span style="font-size: 24px">${useUploadInfo ? upload.original_name : filename}</span> &nbsp;&nbsp;${size}
<br>${useUploadInfo ? `uploaded by ${await getUsernameById(upload.user_id)} at <span id="time_span"></span>
<script>
${parse_date_string}
document.getElementById("time_span").innerHTML = parseDate(new Date(toIso("${upload.upload_date.replace(" ", "T") + ".000Z"}")), false, true);
</script><br>` : ""}<br>
${/*<br>${useUploadInfo ? `uploaded by ${await getUsernameById(upload.user_id)} at ${parseDate(upload.upload_date, false, true)}<br>` : ""}<br>*/""}
<div style="font-size: 18px;">
<a href='/uploads/${filename}?direct=true'><button class="tonly">Open file</button></a>
&nbsp;&nbsp;&nbsp;
<a href='/uploads/${filename}?dl=true'><button class="tonly">Download file</button></a>
</div>
<br>
${pages_string("/pvfiles")}
`)
}
} else
res.status(404).send(
`File not found.`)
})
}
+6
View File
@@ -0,0 +1,6 @@
export interface ConsoleCommandData {
name: string,
description: string,
usage: string | boolean,
aliases?: string[] // should be boolean but makes issues
}
+10
View File
@@ -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
};
+7
View File
@@ -0,0 +1,7 @@
export interface Upload {
id: number,
user_id: number,
original_name: string,
stored_name: string,
upload_date: string
}
+11
View File
@@ -0,0 +1,11 @@
export interface User {
id: number;
username: string;
password_hash: string;
created_at: string;
}
export interface UserSession {
id: number,
username: string;
}
+10
View File
@@ -0,0 +1,10 @@
import "express-session";
declare module "express-session" {
interface SessionData {
user?: {
id: number;
username: string;
};
}
}
+66
View File
@@ -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'));
}
}
+68
View File
@@ -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'));
}
}
+315
View File
@@ -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<string> {
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<string>) {
let inputName = args[0]//.toLowerCase() // i don't know why i did this this is actually why setpvf was removed
let cmdArgs: Array<string> = 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<string> {
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<string>) {
let inputName = args[0].toLowerCase()
let cmdArgs: Array<string> = 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}`)
}
}*/
+12
View File
@@ -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;
+18
View File
@@ -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;
}
+61
View File
@@ -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
}
+39
View File
@@ -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();
}
+19
View File
@@ -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}`);
});
});
}
+21
View File
@@ -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"
}
}
+88
View File
@@ -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, "&amp;")
.replaceAll(/</g, "&lt;")
.replaceAll(/>/g, "&gt;")
.replaceAll(/"/g, "&quot;")
.replaceAll(/'/g, "&#039;");
}
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<void> {
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<boolean> {
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
}
}
+19
View File
@@ -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;
}
}
+20
View File
@@ -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"]
}