Initial commit
This commit is contained in:
+14
@@ -0,0 +1,14 @@
|
||||
/node_modules
|
||||
/dist
|
||||
/build
|
||||
|
||||
package-lock.json
|
||||
|
||||
/pvfiles/**/**.log
|
||||
/pvfiles/**/**.log.acck
|
||||
|
||||
/output
|
||||
|
||||
/uploads
|
||||
files.db
|
||||
users.db
|
||||
@@ -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.
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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.`)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -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}`
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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}`
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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(' ')
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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}\`;
|
||||
}
|
||||
`
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
+186
@@ -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)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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> ${size}<br>
|
||||
<br>
|
||||
<span style="font-size: 18px;">
|
||||
<a href='${here_direct}'><button class="tonly">Open file</button></a>
|
||||
|
||||
<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.`)
|
||||
}
|
||||
}
|
||||
@@ -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>`
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -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("/");
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -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.`))
|
||||
});
|
||||
}
|
||||
@@ -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>`
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -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))
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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> ${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>
|
||||
|
||||
<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.`)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export interface ConsoleCommandData {
|
||||
name: string,
|
||||
description: string,
|
||||
usage: string | boolean,
|
||||
aliases?: string[] // should be boolean but makes issues
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
export interface Upload {
|
||||
id: number,
|
||||
user_id: number,
|
||||
original_name: string,
|
||||
stored_name: string,
|
||||
upload_date: string
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export interface User {
|
||||
id: number;
|
||||
username: string;
|
||||
password_hash: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface UserSession {
|
||||
id: number,
|
||||
username: string;
|
||||
}
|
||||
vendored
+10
@@ -0,0 +1,10 @@
|
||||
import "express-session";
|
||||
|
||||
declare module "express-session" {
|
||||
interface SessionData {
|
||||
user?: {
|
||||
id: number;
|
||||
username: string;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
@@ -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}`)
|
||||
}
|
||||
}*/
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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}`);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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, """)
|
||||
.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<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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
Reference in New Issue
Block a user