From f4e7dfa123d2ac9c47b4ceb39a6df3aa39b6c2fb Mon Sep 17 00:00:00 2001
From: linkability <146661751+linkability@users.noreply.github.com>
Date: Wed, 4 Oct 2023 01:36:52 +0100
Subject: [PATCH 001/173] api-v1: Adds initial empty files
---
package-lock.json | 39 +++--------------------------
src/server/routes/api/v1/account.ts | 0
src/server/routes/api/v1/admin.ts | 0
src/server/routes/api/v1/file.ts | 0
src/server/routes/api/v1/public.ts | 0
5 files changed, 3 insertions(+), 36 deletions(-)
create mode 100644 src/server/routes/api/v1/account.ts
create mode 100644 src/server/routes/api/v1/admin.ts
create mode 100644 src/server/routes/api/v1/file.ts
create mode 100644 src/server/routes/api/v1/public.ts
diff --git a/package-lock.json b/package-lock.json
index 455e4c3..01020d4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,17 +1,14 @@
{
"name": "monofile",
- "version": "1.3.0-beta",
+ "version": "1.4.0-dev",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "monofile",
- "version": "1.3.0-beta",
+ "version": "1.4.0-dev",
"license": "Unlicense",
"dependencies": {
- "@fontsource/fira-code": "^5.0.8",
- "@fontsource/inconsolata": "^5.0.8",
- "@fontsource/source-sans-pro": "^5.0.8",
"@types/body-parser": "^1.19.2",
"@types/express": "^4.17.14",
"@types/multer": "^1.4.7",
@@ -37,7 +34,7 @@
"svelte": "^3.55.1"
},
"engines": {
- "node": ">=v18"
+ "node": ">=v16.11"
}
},
"node_modules/@discordjs/builders": {
@@ -90,21 +87,6 @@
"node": ">=16.9.0"
}
},
- "node_modules/@fontsource/fira-code": {
- "version": "5.0.8",
- "resolved": "https://registry.npmjs.org/@fontsource/fira-code/-/fira-code-5.0.8.tgz",
- "integrity": "sha512-kp/tJUVnjaZeLHENMBFTTSgP2B7+/rIboeofuMfoGB40s2U0DKXNqQcOqIF5PtDhJ5QTG1LcviYXMnc1yG6oYQ=="
- },
- "node_modules/@fontsource/inconsolata": {
- "version": "5.0.8",
- "resolved": "https://registry.npmjs.org/@fontsource/inconsolata/-/inconsolata-5.0.8.tgz",
- "integrity": "sha512-KpBU6q1yCovfycaFprVEauh8U5RsWty3konFfUukyRRxZBK4Sf73XmGQc8iJ4CPrOP4dplGfdX2kjbRgdymajA=="
- },
- "node_modules/@fontsource/source-sans-pro": {
- "version": "5.0.8",
- "resolved": "https://registry.npmjs.org/@fontsource/source-sans-pro/-/source-sans-pro-5.0.8.tgz",
- "integrity": "sha512-5U2UvIYRkCMozZ388gCE73PEpa2MFgN/0t9O4a1FF7bGT/MIneQWSL1XpWZ8iMVYdh6ntxRf3iFA6slCIuFgkg=="
- },
"node_modules/@rollup/plugin-node-resolve": {
"version": "15.0.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.0.1.tgz",
@@ -1750,21 +1732,6 @@
"resolved": "https://registry.npmjs.org/@discordjs/util/-/util-0.1.0.tgz",
"integrity": "sha512-e7d+PaTLVQav6rOc2tojh2y6FE8S7REkqLldq1XF4soCx74XB/DIjbVbVLtBemf0nLW77ntz0v+o5DytKwFNLQ=="
},
- "@fontsource/fira-code": {
- "version": "5.0.8",
- "resolved": "https://registry.npmjs.org/@fontsource/fira-code/-/fira-code-5.0.8.tgz",
- "integrity": "sha512-kp/tJUVnjaZeLHENMBFTTSgP2B7+/rIboeofuMfoGB40s2U0DKXNqQcOqIF5PtDhJ5QTG1LcviYXMnc1yG6oYQ=="
- },
- "@fontsource/inconsolata": {
- "version": "5.0.8",
- "resolved": "https://registry.npmjs.org/@fontsource/inconsolata/-/inconsolata-5.0.8.tgz",
- "integrity": "sha512-KpBU6q1yCovfycaFprVEauh8U5RsWty3konFfUukyRRxZBK4Sf73XmGQc8iJ4CPrOP4dplGfdX2kjbRgdymajA=="
- },
- "@fontsource/source-sans-pro": {
- "version": "5.0.8",
- "resolved": "https://registry.npmjs.org/@fontsource/source-sans-pro/-/source-sans-pro-5.0.8.tgz",
- "integrity": "sha512-5U2UvIYRkCMozZ388gCE73PEpa2MFgN/0t9O4a1FF7bGT/MIneQWSL1XpWZ8iMVYdh6ntxRf3iFA6slCIuFgkg=="
- },
"@rollup/plugin-node-resolve": {
"version": "15.0.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.0.1.tgz",
diff --git a/src/server/routes/api/v1/account.ts b/src/server/routes/api/v1/account.ts
new file mode 100644
index 0000000..e69de29
diff --git a/src/server/routes/api/v1/admin.ts b/src/server/routes/api/v1/admin.ts
new file mode 100644
index 0000000..e69de29
diff --git a/src/server/routes/api/v1/file.ts b/src/server/routes/api/v1/file.ts
new file mode 100644
index 0000000..e69de29
diff --git a/src/server/routes/api/v1/public.ts b/src/server/routes/api/v1/public.ts
new file mode 100644
index 0000000..e69de29
From 70591c78e9a99a6816b0ca3f360442b0e4857d35 Mon Sep 17 00:00:00 2001
From: stringsplit <77242831+nbitzz@users.noreply.github.com>
Date: Tue, 3 Oct 2023 20:09:49 -0700
Subject: [PATCH 002/173] api-v1: apihandler
---
config.json | 2 +-
src/server/index.ts | 25 +-
src/server/routes/adminRoutes.ts | 235 --------------
src/server/routes/api/v1/account.ts | 13 +
src/server/routes/api/v1/admin.ts | 8 +
src/server/routes/api/v1/file.ts | 8 +
src/server/routes/api/v1/public.ts | 8 +
src/server/routes/authRoutes.ts | 465 ----------------------------
src/server/routes/fileApiRoutes.ts | 97 ------
src/server/routes/primaryApi.ts | 181 -----------
10 files changed, 45 insertions(+), 997 deletions(-)
delete mode 100644 src/server/routes/adminRoutes.ts
delete mode 100644 src/server/routes/authRoutes.ts
delete mode 100644 src/server/routes/fileApiRoutes.ts
delete mode 100644 src/server/routes/primaryApi.ts
diff --git a/config.json b/config.json
index 985642f..f2e8469 100644
--- a/config.json
+++ b/config.json
@@ -27,5 +27,5 @@
},
"trustProxy": true,
- "forceSSL": true
+ "forceSSL": false
}
\ No newline at end of file
diff --git a/src/server/index.ts b/src/server/index.ts
index 68819e6..6baf6c6 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -8,13 +8,10 @@ import ServeError from "./lib/errors"
import Files from "./lib/files"
import * as auth from "./lib/auth"
import * as Accounts from "./lib/accounts"
-
-import * as authRoutes from "./routes/authRoutes";
-import * as fileApiRoutes from "./routes/fileApiRoutes";
-import * as adminRoutes from "./routes/adminRoutes";
-import * as primaryApi from "./routes/primaryApi";
import { getAccount } from "./lib/middleware";
+import APIRouter from "./routes/api"
+
require("dotenv").config()
let pkg = require(`${process.cwd()}/package.json`)
@@ -46,19 +43,12 @@ app.get("/server",(req,res) => {
}))
})
-app
- .use("/auth",authRoutes.authRoutes)
- .use("/admin",adminRoutes.adminRoutes)
- .use("/files", fileApiRoutes.fileApiRoutes)
- .use(primaryApi.primaryApi)
// funcs
// init data
if (!fs.existsSync(__dirname+"/../.data/")) fs.mkdirSync(__dirname+"/../.data/")
-
-
// discord
let client = new Client({intents:[
@@ -68,12 +58,11 @@ let client = new Client({intents:[
let files = new Files(client,config)
-authRoutes.setFilesObj(files)
-adminRoutes.setFilesObj(files)
-fileApiRoutes.setFilesObj(files)
-primaryApi.setFilesObj(files)
-
-// routes (could probably make these use routers)
+let apiRouter = new APIRouter(files)
+apiRouter.loadAPIMethods().then(() => {
+ app.use(apiRouter.root)
+ console.log("API OK!")
+})
// index, clone
diff --git a/src/server/routes/adminRoutes.ts b/src/server/routes/adminRoutes.ts
deleted file mode 100644
index 510eec2..0000000
--- a/src/server/routes/adminRoutes.ts
+++ /dev/null
@@ -1,235 +0,0 @@
-import bodyParser from "body-parser";
-import { Router } from "express";
-import * as Accounts from "../lib/accounts";
-import * as auth from "../lib/auth";
-import bytes from "bytes"
-import {writeFile} from "fs";
-import { sendMail } from "../lib/mail";
-import { getAccount, requiresAccount, requiresAdmin, requiresPermissions } from "../lib/middleware"
-
-import ServeError from "../lib/errors";
-import Files from "../lib/files";
-
-let parser = bodyParser.json({
- type: ["text/plain","application/json"]
-})
-
-export let adminRoutes = Router();
-adminRoutes
- .use(getAccount)
- .use(requiresAccount)
- .use(requiresAdmin)
- .use(requiresPermissions("admin"))
-let files:Files
-
-export function setFilesObj(newFiles:Files) {
- files = newFiles
-}
-
-let config = require(`${process.cwd()}/config.json`)
-
-adminRoutes.post("/reset", parser, (req,res) => {
-
- let acc = res.locals.acc as Accounts.Account
-
- if (typeof req.body.target !== "string" || typeof req.body.password !== "string") {
- res.status(404)
- res.send()
- return
- }
-
- let targetAccount = Accounts.getFromUsername(req.body.target)
- if (!targetAccount) {
- res.status(404)
- res.send()
- return
- }
-
- Accounts.password.set ( targetAccount.id, req.body.password )
- auth.AuthTokens.filter(e => e.account == targetAccount?.id).forEach((v) => {
- auth.invalidate(v.token)
- })
-
- if (targetAccount.email) {
- sendMail(targetAccount.email, `Your login details have been updated`, `Hello there! This email is to notify you of a password change that an administrator, ${acc.username}, has initiated. You have been logged out of your devices. Thank you for using monofile.`).then(() => {
- res.send("OK")
- }).catch((err) => {})
- }
-
-
- res.send()
-
-})
-
-adminRoutes.post("/elevate", parser, (req,res) => {
-
- let acc = res.locals.acc as Accounts.Account
-
- if (typeof req.body.target !== "string") {
- res.status(404)
- res.send()
- return
- }
-
- let targetAccount = Accounts.getFromUsername(req.body.target)
- if (!targetAccount) {
- res.status(404)
- res.send()
- return
- }
-
- targetAccount.admin = true;
- Accounts.save()
- res.send()
-
-})
-
-adminRoutes.post("/delete", parser, (req,res) => {
-
- if (typeof req.body.target !== "string") {
- res.status(404)
- res.send()
- return
- }
-
- let targetFile = files.getFilePointer(req.body.target)
-
- if (!targetFile) {
- res.status(404)
- res.send()
- return
- }
-
- files.unlink(req.body.target).then(() => {
- res.status(200)
- }).catch(() => {
- res.status(500)
- }).finally(() => res.send())
-
-})
-
-adminRoutes.post("/delete_account", parser, async (req,res) => {
-
- let acc = res.locals.acc as Accounts.Account
-
- if (typeof req.body.target !== "string") {
- res.status(404)
- res.send()
- return
- }
-
- let targetAccount = Accounts.getFromUsername(req.body.target)
- if (!targetAccount) {
- res.status(404)
- res.send()
- return
- }
-
- let accId = targetAccount.id
-
- auth.AuthTokens.filter(e => e.account == accId).forEach((v) => {
- auth.invalidate(v.token)
- })
-
- let cpl = () => Accounts.deleteAccount(accId).then(_ => {
- if (targetAccount?.email) {
- sendMail(targetAccount.email, "Notice of account deletion", `Your account, ${targetAccount.username}, has been deleted by ${acc.username} for the following reason:
${req.body.reason || "(no reason specified)"}
Your files ${req.body.deleteFiles ? "have been deleted" : "have not been modified"}. Thank you for using monofile.`)
- }
- res.send("account deleted")
- })
-
- if (req.body.deleteFiles) {
- let f = targetAccount.files.map(e=>e) // make shallow copy so that iterating over it doesnt Die
- for (let v of f) {
- files.unlink(v,true).catch(err => console.error(err))
- }
-
- writeFile(process.cwd()+"/.data/files.json",JSON.stringify(files.files), (err) => {
- if (err) console.log(err)
- cpl()
- })
- } else cpl()
-})
-
-adminRoutes.post("/transfer", parser, (req,res) => {
-
- if (typeof req.body.target !== "string" || typeof req.body.owner !== "string") {
- res.status(404)
- res.send()
- return
- }
-
- let targetFile = files.getFilePointer(req.body.target)
- if (!targetFile) {
- res.status(404)
- res.send()
- return
- }
-
- let newOwner = Accounts.getFromUsername(req.body.owner || "")
-
- // clear old owner
-
- if (targetFile.owner) {
- let oldOwner = Accounts.getFromId(targetFile.owner)
- if (oldOwner) {
- Accounts.files.deindex(oldOwner.id, req.body.target)
- }
- }
-
- if (newOwner) {
- Accounts.files.index(newOwner.id, req.body.target)
- }
- targetFile.owner = newOwner ? newOwner.id : undefined;
-
- files.writeFile(req.body.target, targetFile).then(() => {
- res.send()
- }).catch(() => {
- res.status(500)
- res.send()
- }) // wasting a reassignment but whatee
-
-})
-
-adminRoutes.post("/idchange", parser, (req,res) => {
-
- if (typeof req.body.target !== "string" || typeof req.body.new !== "string") {
- res.status(400)
- res.send()
- return
- }
-
- let targetFile = files.getFilePointer(req.body.target)
- if (!targetFile) {
- res.status(404)
- res.send()
- return
- }
-
- if (files.getFilePointer(req.body.new)) {
- res.status(400)
- res.send()
- return
- }
-
- if (targetFile.owner) {
- Accounts.files.deindex(targetFile.owner, req.body.target)
- Accounts.files.index(targetFile.owner, req.body.new)
- }
- delete files.files[req.body.target]
-
- files.writeFile(req.body.new, targetFile).then(() => {
- res.send()
- }).catch(() => {
- files.files[req.body.target] = req.body.new
-
- if (targetFile.owner) {
- Accounts.files.deindex(targetFile.owner, req.body.new)
- Accounts.files.index(targetFile.owner, req.body.target)
- }
-
- res.status(500)
- res.send()
- })
-
-})
\ No newline at end of file
diff --git a/src/server/routes/api/v1/account.ts b/src/server/routes/api/v1/account.ts
index e69de29..242a9f1 100644
--- a/src/server/routes/api/v1/account.ts
+++ b/src/server/routes/api/v1/account.ts
@@ -0,0 +1,13 @@
+import { Router } from "express";
+import Files from "../../../lib/files";
+
+let router = Router()
+
+module.exports = function(files: Files) {
+
+ router.get("/", function(req,res) {
+ res.send("hello world!")
+ })
+
+ return router
+}
\ No newline at end of file
diff --git a/src/server/routes/api/v1/admin.ts b/src/server/routes/api/v1/admin.ts
index e69de29..8c8168d 100644
--- a/src/server/routes/api/v1/admin.ts
+++ b/src/server/routes/api/v1/admin.ts
@@ -0,0 +1,8 @@
+import { Router } from "express";
+import Files from "../../../lib/files";
+
+let router = Router()
+
+module.exports = function(files: Files) {
+ return router
+}
\ No newline at end of file
diff --git a/src/server/routes/api/v1/file.ts b/src/server/routes/api/v1/file.ts
index e69de29..8c8168d 100644
--- a/src/server/routes/api/v1/file.ts
+++ b/src/server/routes/api/v1/file.ts
@@ -0,0 +1,8 @@
+import { Router } from "express";
+import Files from "../../../lib/files";
+
+let router = Router()
+
+module.exports = function(files: Files) {
+ return router
+}
\ No newline at end of file
diff --git a/src/server/routes/api/v1/public.ts b/src/server/routes/api/v1/public.ts
index e69de29..8c8168d 100644
--- a/src/server/routes/api/v1/public.ts
+++ b/src/server/routes/api/v1/public.ts
@@ -0,0 +1,8 @@
+import { Router } from "express";
+import Files from "../../../lib/files";
+
+let router = Router()
+
+module.exports = function(files: Files) {
+ return router
+}
\ No newline at end of file
diff --git a/src/server/routes/authRoutes.ts b/src/server/routes/authRoutes.ts
deleted file mode 100644
index a8de6ff..0000000
--- a/src/server/routes/authRoutes.ts
+++ /dev/null
@@ -1,465 +0,0 @@
-import bodyParser from "body-parser";
-import { Router } from "express";
-import * as Accounts from "../lib/accounts";
-import * as auth from "../lib/auth";
-import { sendMail } from "../lib/mail";
-import { getAccount, noAPIAccess, requiresAccount, requiresPermissions } from "../lib/middleware"
-import { accountRatelimit } from "../lib/ratelimit"
-
-import ServeError from "../lib/errors";
-import Files, { FileVisibility, generateFileId, id_check_regex } from "../lib/files";
-
-import { writeFile } from "fs";
-
-let parser = bodyParser.json({
- type: ["text/plain","application/json"]
-})
-
-export let authRoutes = Router();
-authRoutes.use(getAccount)
-
-let config = require(`${process.cwd()}/config.json`)
-
-let files:Files
-
-export function setFilesObj(newFiles:Files) {
- files = newFiles
-}
-
-authRoutes.post("/login", parser, (req,res) => {
- if (typeof req.body.username != "string" || typeof req.body.password != "string") {
- ServeError(res,400,"please provide a username or password")
- return
- }
-
- if (auth.validate(req.cookies.auth)) return
-
- /*
- check if account exists
- */
-
- let acc = Accounts.getFromUsername(req.body.username)
-
- if (!acc) {
- ServeError(res,401,"username or password incorrect")
- return
- }
-
- if (!Accounts.password.check(acc.id,req.body.password)) {
- ServeError(res,401,"username or password incorrect")
- return
- }
-
- /*
- assign token
- */
-
- res.cookie("auth",auth.create(acc.id,(3*24*60*60*1000)))
- res.status(200)
- res.end()
-})
-
-authRoutes.post("/create", parser, (req,res) => {
- if (!config.accounts.registrationEnabled) {
- ServeError(res,403,"account registration disabled")
- return
- }
-
- if (auth.validate(req.cookies.auth)) return
-
- if (typeof req.body.username != "string" || typeof req.body.password != "string") {
- ServeError(res,400,"please provide a username or password")
- return
- }
-
- /*
- check if account exists
- */
-
- let acc = Accounts.getFromUsername(req.body.username)
-
- if (acc) {
- ServeError(res,400,"account with this username already exists")
- return
- }
-
- if (req.body.username.length < 3 || req.body.username.length > 20) {
- ServeError(res,400,"username must be over or equal to 3 characters or under or equal to 20 characters in length")
- return
- }
-
- if ((req.body.username.match(/[A-Za-z0-9_\-\.]+/) || [])[0] != req.body.username) {
- ServeError(res,400,"username contains invalid characters")
- return
- }
-
- if (req.body.password.length < 8) {
- ServeError(res,400,"password must be 8 characters or longer")
- return
- }
-
- Accounts.create(req.body.username,req.body.password)
- .then((newAcc) => {
- /*
- assign token
- */
-
- res.cookie("auth",auth.create(newAcc,(3*24*60*60*1000)))
- res.status(200)
- res.end()
- })
- .catch(() => {
- ServeError(res,500,"internal server error")
- })
-})
-
-authRoutes.post("/logout", (req,res) => {
- if (!auth.validate(req.cookies.auth)) {
- ServeError(res, 401, "not logged in")
- return
- }
-
- auth.invalidate(req.cookies.auth)
- res.send("logged out")
-})
-
-authRoutes.post("/dfv", requiresAccount, requiresPermissions("manage"), parser, (req,res) => {
- let acc = res.locals.acc as Accounts.Account
-
- if (['public','private','anonymous'].includes(req.body.defaultFileVisibility)) {
- acc.defaultFileVisibility = req.body.defaultFileVisibility
- Accounts.save()
- res.send(`dfv has been set to ${acc.defaultFileVisibility}`)
- } else {
- res.status(400)
- res.send("invalid dfv")
- }
-})
-
-authRoutes.post("/customcss", requiresAccount, requiresPermissions("customize"), parser, (req,res) => {
- let acc = res.locals.acc as Accounts.Account
-
- if (typeof req.body.fileId != "string") req.body.fileId = undefined;
-
- if (
-
- !req.body.fileId
- || (req.body.fileId.match(id_check_regex) == req.body.fileId
- && req.body.fileId.length <= config.maxUploadIdLength)
-
- ) {
- acc.customCSS = req.body.fileId || undefined
- if (!req.body.fileId) delete acc.customCSS
- Accounts.save()
- res.send(`custom css saved`)
- } else {
- res.status(400)
- res.send("invalid fileid")
- }
-})
-
-authRoutes.post("/embedcolor", requiresAccount, requiresPermissions("customize"), parser, (req,res) => {
- let acc = res.locals.acc as Accounts.Account
-
- if (typeof req.body.color != "string") req.body.color = undefined;
-
- if (
-
- !req.body.color
- || (req.body.color.toLowerCase().match(/[a-f0-9]+/) == req.body.color.toLowerCase())
- && req.body.color.length == 6
-
- ) {
- if (!acc.embed) acc.embed = {}
- acc.embed.color = req.body.color || undefined
- if (!req.body.color) delete acc.embed.color
- Accounts.save()
- res.send(`custom embed color saved`)
- } else {
- res.status(400)
- res.send("invalid hex code")
- }
-})
-
-authRoutes.post("/embedsize", requiresAccount, requiresPermissions("customize"), parser, (req,res) => {
- let acc = res.locals.acc as Accounts.Account
-
- if (typeof req.body.largeImage != "boolean") req.body.color = false;
-
- if (!acc.embed) acc.embed = {}
- acc.embed.largeImage = req.body.largeImage
- if (!req.body.largeImage) delete acc.embed.largeImage
- Accounts.save()
- res.send(`custom embed image size saved`)
-})
-
-authRoutes.post("/delete_account", requiresAccount, noAPIAccess, parser, async (req,res) => {
- let acc = res.locals.acc as Accounts.Account
-
- let accId = acc.id
-
- auth.AuthTokens.filter(e => e.account == accId).forEach((v) => {
- auth.invalidate(v.token)
- })
-
- let cpl = () => Accounts.deleteAccount(accId).then(_ => res.send("account deleted"))
-
- if (req.body.deleteFiles) {
- let f = acc.files.map(e=>e) // make shallow copy so that iterating over it doesnt Die
- for (let v of f) {
- files.unlink(v,true).catch(err => console.error(err))
- }
-
- writeFile(process.cwd()+"/.data/files.json",JSON.stringify(files.files), (err) => {
- if (err) console.log(err)
- cpl()
- })
- } else cpl()
-})
-
-authRoutes.post("/change_username", requiresAccount, noAPIAccess, parser, (req,res) => {
- let acc = res.locals.acc as Accounts.Account
-
- if (typeof req.body.username != "string" || req.body.username.length < 3 || req.body.username.length > 20) {
- ServeError(res,400,"username must be between 3 and 20 characters in length")
- return
- }
-
- let _acc = Accounts.getFromUsername(req.body.username)
-
- if (_acc) {
- ServeError(res,400,"account with this username already exists")
- return
- }
-
- if ((req.body.username.match(/[A-Za-z0-9_\-\.]+/) || [])[0] != req.body.username) {
- ServeError(res,400,"username contains invalid characters")
- return
- }
-
- acc.username = req.body.username
- Accounts.save()
-
- if (acc.email) {
- sendMail(acc.email, `Your login details have been updated`, `Hello there! Your username has been updated to ${req.body.username}. Please update your devices accordingly. Thank you for using monofile.`).then(() => {
- res.send("OK")
- }).catch((err) => {})
- }
-
- res.send("username changed")
-})
-
-// shit way to do this but...
-
-let verificationCodes = new Map()
-
-authRoutes.post("/request_email_change", requiresAccount, noAPIAccess, accountRatelimit({ requests: 4, per: 60*60*1000 }), parser, (req,res) => {
- let acc = res.locals.acc as Accounts.Account
-
-
- if (typeof req.body.email != "string" || !req.body.email) {
- ServeError(res,400, "supply an email")
- return
- }
-
- let vcode = verificationCodes.get(acc.id)
-
- // delete previous if any
- let e = vcode?.expiry
- if (e) clearTimeout(e)
- verificationCodes.delete(acc?.id||"")
-
- let code = generateFileId(12).toUpperCase()
-
- // set
-
- verificationCodes.set(acc.id, {
- code,
- email: req.body.email,
- expiry: setTimeout( () => verificationCodes.delete(acc?.id||""), 15*60*1000)
- })
-
- // this is a mess but it's fine
-
- sendMail(req.body.email, `Hey there, ${acc.username} - let's connect your email`, `Hello there! You are recieving this message because you decided to link your email, ${req.body.email.split("@")[0]}@${req.body.email.split("@")[1]}, to your account, ${acc.username}. If you would like to continue, please click here, or go to https://${req.header("Host")}/auth/confirm_email/${code}.`).then(() => {
- res.send("OK")
- }).catch((err) => {
- let e = verificationCodes.get(acc?.id||"")?.expiry
- if (e) clearTimeout(e)
- verificationCodes.delete(acc?.id||"")
- res.locals.undoCount();
- ServeError(res, 500, err?.toString())
- })
-})
-
-authRoutes.get("/confirm_email/:code", requiresAccount, noAPIAccess, (req,res) => {
- let acc = res.locals.acc as Accounts.Account
-
-
- let vcode = verificationCodes.get(acc.id)
-
- if (!vcode) { ServeError(res, 400, "nothing to confirm"); return }
-
- if (typeof req.params.code == "string" && req.params.code.toUpperCase() == vcode.code) {
- acc.email = vcode.email
- Accounts.save();
-
- let e = verificationCodes.get(acc?.id||"")?.expiry
- if (e) clearTimeout(e)
- verificationCodes.delete(acc?.id||"")
-
- res.redirect("/")
- } else {
- ServeError(res, 400, "invalid code")
- }
-})
-
-authRoutes.post("/remove_email", requiresAccount, noAPIAccess, (req,res) => {
- let acc = res.locals.acc as Accounts.Account
-
- if (acc.email) {
- delete acc.email;
- Accounts.save()
- res.send("email detached")
- }
- else ServeError(res, 400, "email not attached")
-})
-
-let pwReset = new Map()
-let prcIdx = new Map()
-
-authRoutes.post("/request_emergency_login", parser, (req,res) => {
- if (auth.validate(req.cookies.auth || "")) return
-
- if (typeof req.body.account != "string" || !req.body.account) {
- ServeError(res,400, "supply a username")
- return
- }
-
- let acc = Accounts.getFromUsername(req.body.account)
- if (!acc || !acc.email) {
- ServeError(res, 400, "this account either does not exist or does not have an email attached; please contact the server's admin for a reset if you would still like to access it")
- return
- }
-
- let pResetCode = pwReset.get(acc.id)
-
- if (pResetCode && pResetCode.requestedAt+(15*60*1000) > Date.now()) {
- ServeError(res, 429, `Please wait a few moments to request another emergency login.`)
- return
- }
-
-
- // delete previous if any
- let e = pResetCode?.expiry
- if (e) clearTimeout(e)
- pwReset.delete(acc?.id||"")
- prcIdx.delete(pResetCode?.code||"")
-
- let code = generateFileId(12).toUpperCase()
-
- // set
-
- pwReset.set(acc.id, {
- code,
- expiry: setTimeout( () => { pwReset.delete(acc?.id||""); prcIdx.delete(pResetCode?.code||"") }, 15*60*1000),
- requestedAt: Date.now()
- })
-
- prcIdx.set(code, acc.id)
-
- // this is a mess but it's fine
-
- sendMail(acc.email, `Emergency login requested for ${acc.username}`, `Hello there! You are recieving this message because you forgot your password to your monofile account, ${acc.username}. To log in, please click here, or go to https://${req.header("Host")}/auth/emergency_login/${code}. If it doesn't appear that you are logged in after visiting this link, please try refreshing. Once you have successfully logged in, you may reset your password.`).then(() => {
- res.send("OK")
- }).catch((err) => {
- let e = pwReset.get(acc?.id||"")?.expiry
- if (e) clearTimeout(e)
- pwReset.delete(acc?.id||"")
- prcIdx.delete(code||"")
- ServeError(res, 500, err?.toString())
- })
-})
-
-authRoutes.get("/emergency_login/:code", (req,res) => {
- if (auth.validate(req.cookies.auth || "")) {
- ServeError(res, 403, "already logged in")
- return
- }
-
- let vcode = prcIdx.get(req.params.code)
-
- if (!vcode) { ServeError(res, 400, "invalid emergency login code"); return }
-
- if (typeof req.params.code == "string" && vcode) {
- res.cookie("auth",auth.create(vcode,(3*24*60*60*1000)))
- res.redirect("/")
-
- let e = pwReset.get(vcode)?.expiry
- if (e) clearTimeout(e)
- pwReset.delete(vcode)
- prcIdx.delete(req.params.code)
- } else {
- ServeError(res, 400, "invalid code")
- }
-})
-
-authRoutes.post("/change_password", requiresAccount, noAPIAccess, parser, (req,res) => {
- let acc = res.locals.acc as Accounts.Account
-
- if (typeof req.body.password != "string" || req.body.password.length < 8) {
- ServeError(res,400,"password must be 8 characters or longer")
- return
- }
-
- let accId = acc.id
-
- Accounts.password.set(accId,req.body.password)
-
- auth.AuthTokens.filter(e => e.account == accId).forEach((v) => {
- auth.invalidate(v.token)
- })
-
- if (acc.email) {
- sendMail(acc.email, `Your login details have been updated`, `Hello there! This email is to notify you of a password change that you have initiated. You have been logged out of your devices. Thank you for using monofile.`).then(() => {
- res.send("OK")
- }).catch((err) => {})
- }
-
- res.send("password changed - logged out all sessions")
-})
-
-authRoutes.post("/logout_sessions", requiresAccount, noAPIAccess, (req,res) => {
- let acc = res.locals.acc as Accounts.Account
-
- let accId = acc.id
-
- auth.AuthTokens.filter(e => e.account == accId).forEach((v) => {
- auth.invalidate(v.token)
- })
-
- res.send("logged out all sessions")
-})
-
-authRoutes.get("/me", requiresAccount, requiresPermissions("user"), (req,res) => {
- let acc = res.locals.acc as Accounts.Account
-
- let sessionToken = auth.tokenFor(req)
- let accId = acc.id
- res.send({
- ...acc,
- sessionCount: auth.AuthTokens.filter(e => e.type != "App" && e.account == accId && (e.expire > Date.now() || !e.expire)).length,
- sessionExpires: auth.AuthTokens.find(e => e.token == sessionToken)?.expire,
- password: undefined,
- email:
- auth.getType(sessionToken) == "User" || auth.getPermissions(sessionToken)?.includes("email")
- ? acc.email
- : undefined
- })
-})
-
-authRoutes.get("/customCSS", (req,res) => {
- let acc = res.locals.acc
- if (acc?.customCSS) res.redirect(`/file/${acc.customCSS}`)
- else res.send("")
-})
diff --git a/src/server/routes/fileApiRoutes.ts b/src/server/routes/fileApiRoutes.ts
deleted file mode 100644
index f64d141..0000000
--- a/src/server/routes/fileApiRoutes.ts
+++ /dev/null
@@ -1,97 +0,0 @@
-import bodyParser from "body-parser";
-import { Router } from "express";
-import * as Accounts from "../lib/accounts";
-import * as auth from "../lib/auth";
-import bytes from "bytes"
-import {writeFile} from "fs";
-
-import ServeError from "../lib/errors";
-import Files from "../lib/files";
-import { getAccount, requiresAccount, requiresPermissions } from "../lib/middleware";
-
-let parser = bodyParser.json({
- type: ["text/plain","application/json"]
-})
-
-export let fileApiRoutes = Router();
-let files:Files
-
-export function setFilesObj(newFiles:Files) {
- files = newFiles
-}
-
-let config = require(`${process.cwd()}/config.json`)
-
-fileApiRoutes.use(getAccount);
-
-fileApiRoutes.get("/list", requiresAccount, requiresPermissions("user"), (req,res) => {
-
- let acc = res.locals.acc as Accounts.Account
-
- if (!acc) return
- let accId = acc.id
-
- res.send(acc.files.map((e) => {
- let fp = files.getFilePointer(e)
- if (!fp) { Accounts.files.deindex(accId, e); return null }
- return {
- ...fp,
- messageids: null,
- owner: null,
- id:e
- }
- }).filter(e=>e))
-
-})
-
-fileApiRoutes.post("/manage", parser, requiresPermissions("manage"), (req,res) => {
-
- let acc = res.locals.acc as Accounts.Account
-
- if (!acc) return
- if (!req.body.target || !(typeof req.body.target == "object") || req.body.target.length < 1) return
-
- let modified = 0
-
- req.body.target.forEach((e:string) => {
- if (!acc.files.includes(e)) return
-
- let fp = files.getFilePointer(e)
-
- if (fp.reserved) {
- return
- }
-
- switch( req.body.action ) {
- case "delete":
- files.unlink(e, true)
- modified++;
- break;
-
- case "changeFileVisibility":
- if (!["public","anonymous","private"].includes(req.body.value)) return;
- files.files[e].visibility = req.body.value;
- modified++;
- break;
-
- case "setTag":
- if (!req.body.value) delete files.files[e].tag
- else {
- if (req.body.value.toString().length > 30) return
- files.files[e].tag = req.body.value.toString().toLowerCase()
- }
- modified++;
- break;
- }
- })
-
- Accounts.save().then(() => {
- writeFile(process.cwd()+"/.data/files.json",JSON.stringify(files.files), (err) => {
- if (err) console.log(err)
- res.contentType("text/plain")
- res.send(`modified ${modified} files`)
- })
- }).catch((err) => console.error(err))
-
-
-})
\ No newline at end of file
diff --git a/src/server/routes/primaryApi.ts b/src/server/routes/primaryApi.ts
deleted file mode 100644
index 33558d8..0000000
--- a/src/server/routes/primaryApi.ts
+++ /dev/null
@@ -1,181 +0,0 @@
-import bodyParser from "body-parser";
-import express, { Router } from "express";
-import * as Accounts from "../lib/accounts";
-import * as auth from "../lib/auth";
-import axios, { AxiosResponse } from "axios"
-import { type Range } from "range-parser";
-import multer, {memoryStorage} from "multer"
-
-import ServeError from "../lib/errors";
-import Files from "../lib/files";
-import { getAccount, requiresPermissions } from "../lib/middleware";
-
-let parser = bodyParser.json({
- type: ["text/plain","application/json"]
-})
-
-export let primaryApi = Router();
-let files:Files
-
-export function setFilesObj(newFiles:Files) {
- files = newFiles
-}
-
-const multerSetup = multer({storage:memoryStorage()})
-
-let config = require(`${process.cwd()}/config.json`)
-
-primaryApi.use(getAccount);
-
-primaryApi.get(["/file/:fileId", "/cpt/:fileId/*", "/:fileId"], async (req:express.Request,res:express.Response) => {
-
- let acc = res.locals.acc as Accounts.Account
-
- let file = files.getFilePointer(req.params.fileId)
- res.setHeader("Access-Control-Allow-Origin", "*")
- res.setHeader("Content-Security-Policy","sandbox allow-scripts")
- if (req.query.attachment == "1") res.setHeader("Content-Disposition", "attachment")
-
- if (file) {
-
- if (file.visibility == "private" && acc?.id != file.owner) {
- ServeError(res,403,"you do not own this file")
- return
- }
-
- let range: Range | undefined
-
- res.setHeader("Content-Type",file.mime)
- if (file.sizeInBytes) {
- res.setHeader("Content-Length",file.sizeInBytes)
-
- if (file.chunkSize) {
- let rng = req.range(file.sizeInBytes)
- if (rng) {
-
- // error handling
- if (typeof rng == "number") {
- res.status(rng == -1 ? 416 : 400).send()
- return
- }
- if (rng.type != "bytes") {
- res.status(400).send();
- return
- }
-
- // set ranges var
- let rngs = Array.from(rng)
- if (rngs.length != 1) { res.status(400).send(); return }
- range = rngs[0]
-
- }
- }
- }
-
- // supports ranges
-
-
- files.readFileStream(req.params.fileId, range).then(async stream => {
-
- if (range) {
- res.status(206)
- res.header("Content-Length", (range.end-range.start + 1).toString())
- res.header("Content-Range", `bytes ${range.start}-${range.end}/${file.sizeInBytes}`)
- }
- stream.pipe(res)
-
- }).catch((err) => {
- ServeError(res,err.status,err.message)
- })
-
- } else {
- ServeError(res, 404, "file not found")
- }
-
-})
-
-primaryApi.head(["/file/:fileId", "/cpt/:fileId/*", "/:fileId"], (req: express.Request, res:express.Response) => {
- let file = files.getFilePointer(req.params.fileId)
- res.setHeader("Access-Control-Allow-Origin", "*")
- res.setHeader("Content-Security-Policy","sandbox allow-scripts")
- if (req.query.attachment == "1") res.setHeader("Content-Disposition", "attachment")
- if (!file) {
- res.status(404)
- res.send()
- } else {
- res.setHeader("Content-Type",file.mime)
- if (file.sizeInBytes) {
- res.setHeader("Content-Length",file.sizeInBytes)
- }
- if (file.chunkSize) {
- res.setHeader("Accept-Ranges", "bytes")
- }
- }
-})
-
-// upload handlers
-
-primaryApi.post("/upload", requiresPermissions("upload"), multerSetup.single('file'), async (req,res) => {
-
- let acc = res.locals.acc as Accounts.Account
-
- if (req.file) {
- try {
- let prm = req.header("monofile-params")
- let params:{[key:string]:any} = {}
- if (prm) {
- params = JSON.parse(prm)
- }
-
- files.uploadFile({
- owner: acc?.id,
-
- uploadId:params.uploadId,
- name:req.file.originalname,
- mime:req.file.mimetype
- },req.file.buffer)
- .then((uID) => res.send(uID))
- .catch((stat) => {
- res.status(stat.status);
- res.send(`[err] ${stat.message}`)
- })
- } catch {
- res.status(400)
- res.send("[err] bad request")
- }
- } else {
- res.status(400)
- res.send("[err] bad request")
- }
-})
-
-primaryApi.post("/clone", requiresPermissions("upload"), bodyParser.json({type: ["text/plain","application/json"]}) ,(req,res) => {
-
- let acc = res.locals.acc as Accounts.Account
-
- try {
- axios.get(req.body.url,{responseType:"arraybuffer"}).then((data:AxiosResponse) => {
-
- files.uploadFile({
- owner: acc?.id,
-
- name:req.body.url.split("/")[req.body.url.split("/").length-1] || "generic",
- mime:data.headers["content-type"],
- uploadId:req.body.uploadId
- },Buffer.from(data.data))
- .then((uID) => res.send(uID))
- .catch((stat) => {
- res.status(stat.status);
- res.send(`[err] ${stat.message}`)
- })
-
- }).catch((err) => {
- console.log(err)
- res.status(400)
- res.send(`[err] failed to fetch data`)
- })
- } catch {
- res.status(500)
- res.send("[err] an error occured")
- }
-})
\ No newline at end of file
From 1ed1acca1c367d9576e4db000355c247dacb3bee Mon Sep 17 00:00:00 2001
From: stringsplit <77242831+nbitzz@users.noreply.github.com>
Date: Tue, 3 Oct 2023 20:10:08 -0700
Subject: [PATCH 003/173] api-v1: apihandler
---
src/server/routes/api.ts | 78 ++++
src/server/routes/api/v0/adminRoutes.ts | 236 +++++++++++
src/server/routes/api/v0/api.json | 10 +
src/server/routes/api/v0/authRoutes.ts | 464 ++++++++++++++++++++++
src/server/routes/api/v0/fileApiRoutes.ts | 98 +++++
src/server/routes/api/v0/primaryApi.ts | 181 +++++++++
src/server/routes/api/v1/api.json | 7 +
7 files changed, 1074 insertions(+)
create mode 100644 src/server/routes/api.ts
create mode 100644 src/server/routes/api/v0/adminRoutes.ts
create mode 100644 src/server/routes/api/v0/api.json
create mode 100644 src/server/routes/api/v0/authRoutes.ts
create mode 100644 src/server/routes/api/v0/fileApiRoutes.ts
create mode 100644 src/server/routes/api/v0/primaryApi.ts
create mode 100644 src/server/routes/api/v1/api.json
diff --git a/src/server/routes/api.ts b/src/server/routes/api.ts
new file mode 100644
index 0000000..3286a4a
--- /dev/null
+++ b/src/server/routes/api.ts
@@ -0,0 +1,78 @@
+import { Router } from "express";
+import { readFile, readdir } from "fs/promises";
+import Files from "../lib/files";
+
+const APIDirectory = __dirname+"/api"
+
+interface APIMount {
+ file: string
+ to: string
+}
+
+type APIMountResolvable = string | APIMount
+
+interface APIDefinition {
+ name: string
+ baseURL: string
+ mount: APIMountResolvable[]
+}
+
+function resolveMount(mount: APIMountResolvable): APIMount {
+ return typeof mount == "string" ? { file: mount, to: "/"+mount } : mount
+}
+
+class APIVersion {
+ readonly definition: APIDefinition;
+ readonly apiPath: string;
+ readonly root: Router = Router();
+
+ constructor(definition: APIDefinition, files: Files) {
+
+ this.definition = definition;
+ this.apiPath = APIDirectory + "/" + definition.name
+
+ for (let _mount of definition.mount) {
+ let mount = resolveMount(_mount)
+ // no idea if there's a better way to do this but this is all i can think of
+ let route = require(`${this.apiPath}/${mount.file}.js`) as (files:Files)=>Router
+ this.root.use(mount.to, route(files))
+ }
+ }
+}
+
+export default class APIRouter {
+
+ readonly files: Files
+ readonly root: Router = Router();
+
+ constructor(files: Files) {
+ this.files = files;
+ }
+
+ /**
+ * @description Mounts an APIDefinition to the APIRouter.
+ * @param definition Definition to mount.
+ */
+
+ private mount(definition: APIDefinition) {
+
+ console.log(`mounting APIDefinition ${definition.name}`)
+
+ this.root.use(
+ definition.baseURL,
+ (new APIVersion(definition, this.files)).root
+ )
+
+ }
+
+ async loadAPIMethods() {
+
+ let files = await readdir(APIDirectory)
+ for (let v of files) { /// temporary. need to figure out something else for this
+ let def = JSON.parse((await readFile(`${process.cwd()}/src/server/routes/api/${v}/api.json`)).toString()) as APIDefinition
+ this.mount(def)
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/server/routes/api/v0/adminRoutes.ts b/src/server/routes/api/v0/adminRoutes.ts
new file mode 100644
index 0000000..fa4445e
--- /dev/null
+++ b/src/server/routes/api/v0/adminRoutes.ts
@@ -0,0 +1,236 @@
+import bodyParser from "body-parser";
+import { Router } from "express";
+import * as Accounts from "../../../lib/accounts";
+import * as auth from "../../../lib/auth";
+import bytes from "bytes"
+import {writeFile} from "fs";
+import { sendMail } from "../../../lib/mail";
+import { getAccount, requiresAccount, requiresAdmin, requiresPermissions } from "../../../lib/middleware"
+
+import ServeError from "../../../lib/errors";
+import Files from "../../../lib/files";
+
+let parser = bodyParser.json({
+ type: ["text/plain","application/json"]
+})
+
+export let adminRoutes = Router();
+adminRoutes
+ .use(getAccount)
+ .use(requiresAccount)
+ .use(requiresAdmin)
+ .use(requiresPermissions("admin"))
+
+let config = require(`${process.cwd()}/config.json`)
+
+module.exports = function(files: Files) {
+
+
+ adminRoutes.post("/reset", parser, (req,res) => {
+
+ let acc = res.locals.acc as Accounts.Account
+
+ if (typeof req.body.target !== "string" || typeof req.body.password !== "string") {
+ res.status(404)
+ res.send()
+ return
+ }
+
+ let targetAccount = Accounts.getFromUsername(req.body.target)
+ if (!targetAccount) {
+ res.status(404)
+ res.send()
+ return
+ }
+
+ Accounts.password.set ( targetAccount.id, req.body.password )
+ auth.AuthTokens.filter(e => e.account == targetAccount?.id).forEach((v) => {
+ auth.invalidate(v.token)
+ })
+
+ if (targetAccount.email) {
+ sendMail(targetAccount.email, `Your login details have been updated`, `Hello there! This email is to notify you of a password change that an administrator, ${acc.username}, has initiated. You have been logged out of your devices. Thank you for using monofile.`).then(() => {
+ res.send("OK")
+ }).catch((err) => {})
+ }
+
+
+ res.send()
+
+ })
+
+ adminRoutes.post("/elevate", parser, (req,res) => {
+
+ let acc = res.locals.acc as Accounts.Account
+
+ if (typeof req.body.target !== "string") {
+ res.status(404)
+ res.send()
+ return
+ }
+
+ let targetAccount = Accounts.getFromUsername(req.body.target)
+ if (!targetAccount) {
+ res.status(404)
+ res.send()
+ return
+ }
+
+ targetAccount.admin = true;
+ Accounts.save()
+ res.send()
+
+ })
+
+ adminRoutes.post("/delete", parser, (req,res) => {
+
+ if (typeof req.body.target !== "string") {
+ res.status(404)
+ res.send()
+ return
+ }
+
+ let targetFile = files.getFilePointer(req.body.target)
+
+ if (!targetFile) {
+ res.status(404)
+ res.send()
+ return
+ }
+
+ files.unlink(req.body.target).then(() => {
+ res.status(200)
+ }).catch(() => {
+ res.status(500)
+ }).finally(() => res.send())
+
+ })
+
+ adminRoutes.post("/delete_account", parser, async (req,res) => {
+
+ let acc = res.locals.acc as Accounts.Account
+
+ if (typeof req.body.target !== "string") {
+ res.status(404)
+ res.send()
+ return
+ }
+
+ let targetAccount = Accounts.getFromUsername(req.body.target)
+ if (!targetAccount) {
+ res.status(404)
+ res.send()
+ return
+ }
+
+ let accId = targetAccount.id
+
+ auth.AuthTokens.filter(e => e.account == accId).forEach((v) => {
+ auth.invalidate(v.token)
+ })
+
+ let cpl = () => Accounts.deleteAccount(accId).then(_ => {
+ if (targetAccount?.email) {
+ sendMail(targetAccount.email, "Notice of account deletion", `Your account, ${targetAccount.username}, has been deleted by ${acc.username} for the following reason:
${req.body.reason || "(no reason specified)"}
Your files ${req.body.deleteFiles ? "have been deleted" : "have not been modified"}. Thank you for using monofile.`)
+ }
+ res.send("account deleted")
+ })
+
+ if (req.body.deleteFiles) {
+ let f = targetAccount.files.map(e=>e) // make shallow copy so that iterating over it doesnt Die
+ for (let v of f) {
+ files.unlink(v,true).catch(err => console.error(err))
+ }
+
+ writeFile(process.cwd()+"/.data/files.json",JSON.stringify(files.files), (err) => {
+ if (err) console.log(err)
+ cpl()
+ })
+ } else cpl()
+ })
+
+ adminRoutes.post("/transfer", parser, (req,res) => {
+
+ if (typeof req.body.target !== "string" || typeof req.body.owner !== "string") {
+ res.status(404)
+ res.send()
+ return
+ }
+
+ let targetFile = files.getFilePointer(req.body.target)
+ if (!targetFile) {
+ res.status(404)
+ res.send()
+ return
+ }
+
+ let newOwner = Accounts.getFromUsername(req.body.owner || "")
+
+ // clear old owner
+
+ if (targetFile.owner) {
+ let oldOwner = Accounts.getFromId(targetFile.owner)
+ if (oldOwner) {
+ Accounts.files.deindex(oldOwner.id, req.body.target)
+ }
+ }
+
+ if (newOwner) {
+ Accounts.files.index(newOwner.id, req.body.target)
+ }
+ targetFile.owner = newOwner ? newOwner.id : undefined;
+
+ files.writeFile(req.body.target, targetFile).then(() => {
+ res.send()
+ }).catch(() => {
+ res.status(500)
+ res.send()
+ }) // wasting a reassignment but whatee
+
+ })
+
+ adminRoutes.post("/idchange", parser, (req,res) => {
+
+ if (typeof req.body.target !== "string" || typeof req.body.new !== "string") {
+ res.status(400)
+ res.send()
+ return
+ }
+
+ let targetFile = files.getFilePointer(req.body.target)
+ if (!targetFile) {
+ res.status(404)
+ res.send()
+ return
+ }
+
+ if (files.getFilePointer(req.body.new)) {
+ res.status(400)
+ res.send()
+ return
+ }
+
+ if (targetFile.owner) {
+ Accounts.files.deindex(targetFile.owner, req.body.target)
+ Accounts.files.index(targetFile.owner, req.body.new)
+ }
+ delete files.files[req.body.target]
+
+ files.writeFile(req.body.new, targetFile).then(() => {
+ res.send()
+ }).catch(() => {
+ files.files[req.body.target] = req.body.new
+
+ if (targetFile.owner) {
+ Accounts.files.deindex(targetFile.owner, req.body.new)
+ Accounts.files.index(targetFile.owner, req.body.target)
+ }
+
+ res.status(500)
+ res.send()
+ })
+
+ })
+
+ return adminRoutes
+}
\ No newline at end of file
diff --git a/src/server/routes/api/v0/api.json b/src/server/routes/api/v0/api.json
new file mode 100644
index 0000000..ad0bffb
--- /dev/null
+++ b/src/server/routes/api/v0/api.json
@@ -0,0 +1,10 @@
+{
+ "name": "v0",
+ "baseURL": "/",
+ "mount": [
+ { "file": "primaryApi", "to": "/" },
+ { "file": "adminRoutes", "to": "/admin" },
+ { "file": "authRoutes", "to": "/auth" },
+ { "file": "fileApiRoutes", "to": "/files" }
+ ]
+}
\ No newline at end of file
diff --git a/src/server/routes/api/v0/authRoutes.ts b/src/server/routes/api/v0/authRoutes.ts
new file mode 100644
index 0000000..a2dd3fb
--- /dev/null
+++ b/src/server/routes/api/v0/authRoutes.ts
@@ -0,0 +1,464 @@
+import bodyParser from "body-parser";
+import { Router } from "express";
+import * as Accounts from "../../../lib/accounts";
+import * as auth from "../../../lib/auth";
+import { sendMail } from "../../../lib/mail";
+import { getAccount, noAPIAccess, requiresAccount, requiresPermissions } from "../../../lib/middleware"
+import { accountRatelimit } from "../../../lib/ratelimit"
+
+import ServeError from "../../../lib/errors";
+import Files, { FileVisibility, generateFileId, id_check_regex } from "../../../lib/files";
+
+import { writeFile } from "fs";
+
+let parser = bodyParser.json({
+ type: ["text/plain","application/json"]
+})
+
+export let authRoutes = Router();
+authRoutes.use(getAccount)
+
+let config = require(`${process.cwd()}/config.json`)
+
+module.exports = function(files: Files) {
+
+ authRoutes.post("/login", parser, (req,res) => {
+ if (typeof req.body.username != "string" || typeof req.body.password != "string") {
+ ServeError(res,400,"please provide a username or password")
+ return
+ }
+
+ if (auth.validate(req.cookies.auth)) return
+
+ /*
+ check if account exists
+ */
+
+ let acc = Accounts.getFromUsername(req.body.username)
+
+ if (!acc) {
+ ServeError(res,401,"username or password incorrect")
+ return
+ }
+
+ if (!Accounts.password.check(acc.id,req.body.password)) {
+ ServeError(res,401,"username or password incorrect")
+ return
+ }
+
+ /*
+ assign token
+ */
+
+ res.cookie("auth",auth.create(acc.id,(3*24*60*60*1000)))
+ res.status(200)
+ res.end()
+ })
+
+ authRoutes.post("/create", parser, (req,res) => {
+ if (!config.accounts.registrationEnabled) {
+ ServeError(res,403,"account registration disabled")
+ return
+ }
+
+ if (auth.validate(req.cookies.auth)) return
+
+ if (typeof req.body.username != "string" || typeof req.body.password != "string") {
+ ServeError(res,400,"please provide a username or password")
+ return
+ }
+
+ /*
+ check if account exists
+ */
+
+ let acc = Accounts.getFromUsername(req.body.username)
+
+ if (acc) {
+ ServeError(res,400,"account with this username already exists")
+ return
+ }
+
+ if (req.body.username.length < 3 || req.body.username.length > 20) {
+ ServeError(res,400,"username must be over or equal to 3 characters or under or equal to 20 characters in length")
+ return
+ }
+
+ if ((req.body.username.match(/[A-Za-z0-9_\-\.]+/) || [])[0] != req.body.username) {
+ ServeError(res,400,"username contains invalid characters")
+ return
+ }
+
+ if (req.body.password.length < 8) {
+ ServeError(res,400,"password must be 8 characters or longer")
+ return
+ }
+
+ Accounts.create(req.body.username,req.body.password)
+ .then((newAcc) => {
+ /*
+ assign token
+ */
+
+ res.cookie("auth",auth.create(newAcc,(3*24*60*60*1000)))
+ res.status(200)
+ res.end()
+ })
+ .catch(() => {
+ ServeError(res,500,"internal server error")
+ })
+ })
+
+ authRoutes.post("/logout", (req,res) => {
+ if (!auth.validate(req.cookies.auth)) {
+ ServeError(res, 401, "not logged in")
+ return
+ }
+
+ auth.invalidate(req.cookies.auth)
+ res.send("logged out")
+ })
+
+ authRoutes.post("/dfv", requiresAccount, requiresPermissions("manage"), parser, (req,res) => {
+ let acc = res.locals.acc as Accounts.Account
+
+ if (['public','private','anonymous'].includes(req.body.defaultFileVisibility)) {
+ acc.defaultFileVisibility = req.body.defaultFileVisibility
+ Accounts.save()
+ res.send(`dfv has been set to ${acc.defaultFileVisibility}`)
+ } else {
+ res.status(400)
+ res.send("invalid dfv")
+ }
+ })
+
+ authRoutes.post("/customcss", requiresAccount, requiresPermissions("customize"), parser, (req,res) => {
+ let acc = res.locals.acc as Accounts.Account
+
+ if (typeof req.body.fileId != "string") req.body.fileId = undefined;
+
+ if (
+
+ !req.body.fileId
+ || (req.body.fileId.match(id_check_regex) == req.body.fileId
+ && req.body.fileId.length <= config.maxUploadIdLength)
+
+ ) {
+ acc.customCSS = req.body.fileId || undefined
+ if (!req.body.fileId) delete acc.customCSS
+ Accounts.save()
+ res.send(`custom css saved`)
+ } else {
+ res.status(400)
+ res.send("invalid fileid")
+ }
+ })
+
+ authRoutes.post("/embedcolor", requiresAccount, requiresPermissions("customize"), parser, (req,res) => {
+ let acc = res.locals.acc as Accounts.Account
+
+ if (typeof req.body.color != "string") req.body.color = undefined;
+
+ if (
+
+ !req.body.color
+ || (req.body.color.toLowerCase().match(/[a-f0-9]+/) == req.body.color.toLowerCase())
+ && req.body.color.length == 6
+
+ ) {
+ if (!acc.embed) acc.embed = {}
+ acc.embed.color = req.body.color || undefined
+ if (!req.body.color) delete acc.embed.color
+ Accounts.save()
+ res.send(`custom embed color saved`)
+ } else {
+ res.status(400)
+ res.send("invalid hex code")
+ }
+ })
+
+ authRoutes.post("/embedsize", requiresAccount, requiresPermissions("customize"), parser, (req,res) => {
+ let acc = res.locals.acc as Accounts.Account
+
+ if (typeof req.body.largeImage != "boolean") req.body.color = false;
+
+ if (!acc.embed) acc.embed = {}
+ acc.embed.largeImage = req.body.largeImage
+ if (!req.body.largeImage) delete acc.embed.largeImage
+ Accounts.save()
+ res.send(`custom embed image size saved`)
+ })
+
+ authRoutes.post("/delete_account", requiresAccount, noAPIAccess, parser, async (req,res) => {
+ let acc = res.locals.acc as Accounts.Account
+
+ let accId = acc.id
+
+ auth.AuthTokens.filter(e => e.account == accId).forEach((v) => {
+ auth.invalidate(v.token)
+ })
+
+ let cpl = () => Accounts.deleteAccount(accId).then(_ => res.send("account deleted"))
+
+ if (req.body.deleteFiles) {
+ let f = acc.files.map(e=>e) // make shallow copy so that iterating over it doesnt Die
+ for (let v of f) {
+ files.unlink(v,true).catch(err => console.error(err))
+ }
+
+ writeFile(process.cwd()+"/.data/files.json",JSON.stringify(files.files), (err) => {
+ if (err) console.log(err)
+ cpl()
+ })
+ } else cpl()
+ })
+
+ authRoutes.post("/change_username", requiresAccount, noAPIAccess, parser, (req,res) => {
+ let acc = res.locals.acc as Accounts.Account
+
+ if (typeof req.body.username != "string" || req.body.username.length < 3 || req.body.username.length > 20) {
+ ServeError(res,400,"username must be between 3 and 20 characters in length")
+ return
+ }
+
+ let _acc = Accounts.getFromUsername(req.body.username)
+
+ if (_acc) {
+ ServeError(res,400,"account with this username already exists")
+ return
+ }
+
+ if ((req.body.username.match(/[A-Za-z0-9_\-\.]+/) || [])[0] != req.body.username) {
+ ServeError(res,400,"username contains invalid characters")
+ return
+ }
+
+ acc.username = req.body.username
+ Accounts.save()
+
+ if (acc.email) {
+ sendMail(acc.email, `Your login details have been updated`, `Hello there! Your username has been updated to ${req.body.username}. Please update your devices accordingly. Thank you for using monofile.`).then(() => {
+ res.send("OK")
+ }).catch((err) => {})
+ }
+
+ res.send("username changed")
+ })
+
+ // shit way to do this but...
+
+ let verificationCodes = new Map()
+
+ authRoutes.post("/request_email_change", requiresAccount, noAPIAccess, accountRatelimit({ requests: 4, per: 60*60*1000 }), parser, (req,res) => {
+ let acc = res.locals.acc as Accounts.Account
+
+
+ if (typeof req.body.email != "string" || !req.body.email) {
+ ServeError(res,400, "supply an email")
+ return
+ }
+
+ let vcode = verificationCodes.get(acc.id)
+
+ // delete previous if any
+ let e = vcode?.expiry
+ if (e) clearTimeout(e)
+ verificationCodes.delete(acc?.id||"")
+
+ let code = generateFileId(12).toUpperCase()
+
+ // set
+
+ verificationCodes.set(acc.id, {
+ code,
+ email: req.body.email,
+ expiry: setTimeout( () => verificationCodes.delete(acc?.id||""), 15*60*1000)
+ })
+
+ // this is a mess but it's fine
+
+ sendMail(req.body.email, `Hey there, ${acc.username} - let's connect your email`, `Hello there! You are recieving this message because you decided to link your email, ${req.body.email.split("@")[0]}@${req.body.email.split("@")[1]}, to your account, ${acc.username}. If you would like to continue, please click here, or go to https://${req.header("Host")}/auth/confirm_email/${code}.`).then(() => {
+ res.send("OK")
+ }).catch((err) => {
+ let e = verificationCodes.get(acc?.id||"")?.expiry
+ if (e) clearTimeout(e)
+ verificationCodes.delete(acc?.id||"")
+ res.locals.undoCount();
+ ServeError(res, 500, err?.toString())
+ })
+ })
+
+ authRoutes.get("/confirm_email/:code", requiresAccount, noAPIAccess, (req,res) => {
+ let acc = res.locals.acc as Accounts.Account
+
+
+ let vcode = verificationCodes.get(acc.id)
+
+ if (!vcode) { ServeError(res, 400, "nothing to confirm"); return }
+
+ if (typeof req.params.code == "string" && req.params.code.toUpperCase() == vcode.code) {
+ acc.email = vcode.email
+ Accounts.save();
+
+ let e = verificationCodes.get(acc?.id||"")?.expiry
+ if (e) clearTimeout(e)
+ verificationCodes.delete(acc?.id||"")
+
+ res.redirect("/")
+ } else {
+ ServeError(res, 400, "invalid code")
+ }
+ })
+
+ authRoutes.post("/remove_email", requiresAccount, noAPIAccess, (req,res) => {
+ let acc = res.locals.acc as Accounts.Account
+
+ if (acc.email) {
+ delete acc.email;
+ Accounts.save()
+ res.send("email detached")
+ }
+ else ServeError(res, 400, "email not attached")
+ })
+
+ let pwReset = new Map()
+ let prcIdx = new Map()
+
+ authRoutes.post("/request_emergency_login", parser, (req,res) => {
+ if (auth.validate(req.cookies.auth || "")) return
+
+ if (typeof req.body.account != "string" || !req.body.account) {
+ ServeError(res,400, "supply a username")
+ return
+ }
+
+ let acc = Accounts.getFromUsername(req.body.account)
+ if (!acc || !acc.email) {
+ ServeError(res, 400, "this account either does not exist or does not have an email attached; please contact the server's admin for a reset if you would still like to access it")
+ return
+ }
+
+ let pResetCode = pwReset.get(acc.id)
+
+ if (pResetCode && pResetCode.requestedAt+(15*60*1000) > Date.now()) {
+ ServeError(res, 429, `Please wait a few moments to request another emergency login.`)
+ return
+ }
+
+
+ // delete previous if any
+ let e = pResetCode?.expiry
+ if (e) clearTimeout(e)
+ pwReset.delete(acc?.id||"")
+ prcIdx.delete(pResetCode?.code||"")
+
+ let code = generateFileId(12).toUpperCase()
+
+ // set
+
+ pwReset.set(acc.id, {
+ code,
+ expiry: setTimeout( () => { pwReset.delete(acc?.id||""); prcIdx.delete(pResetCode?.code||"") }, 15*60*1000),
+ requestedAt: Date.now()
+ })
+
+ prcIdx.set(code, acc.id)
+
+ // this is a mess but it's fine
+
+ sendMail(acc.email, `Emergency login requested for ${acc.username}`, `Hello there! You are recieving this message because you forgot your password to your monofile account, ${acc.username}. To log in, please click here, or go to https://${req.header("Host")}/auth/emergency_login/${code}. If it doesn't appear that you are logged in after visiting this link, please try refreshing. Once you have successfully logged in, you may reset your password.`).then(() => {
+ res.send("OK")
+ }).catch((err) => {
+ let e = pwReset.get(acc?.id||"")?.expiry
+ if (e) clearTimeout(e)
+ pwReset.delete(acc?.id||"")
+ prcIdx.delete(code||"")
+ ServeError(res, 500, err?.toString())
+ })
+ })
+
+ authRoutes.get("/emergency_login/:code", (req,res) => {
+ if (auth.validate(req.cookies.auth || "")) {
+ ServeError(res, 403, "already logged in")
+ return
+ }
+
+ let vcode = prcIdx.get(req.params.code)
+
+ if (!vcode) { ServeError(res, 400, "invalid emergency login code"); return }
+
+ if (typeof req.params.code == "string" && vcode) {
+ res.cookie("auth",auth.create(vcode,(3*24*60*60*1000)))
+ res.redirect("/")
+
+ let e = pwReset.get(vcode)?.expiry
+ if (e) clearTimeout(e)
+ pwReset.delete(vcode)
+ prcIdx.delete(req.params.code)
+ } else {
+ ServeError(res, 400, "invalid code")
+ }
+ })
+
+ authRoutes.post("/change_password", requiresAccount, noAPIAccess, parser, (req,res) => {
+ let acc = res.locals.acc as Accounts.Account
+
+ if (typeof req.body.password != "string" || req.body.password.length < 8) {
+ ServeError(res,400,"password must be 8 characters or longer")
+ return
+ }
+
+ let accId = acc.id
+
+ Accounts.password.set(accId,req.body.password)
+
+ auth.AuthTokens.filter(e => e.account == accId).forEach((v) => {
+ auth.invalidate(v.token)
+ })
+
+ if (acc.email) {
+ sendMail(acc.email, `Your login details have been updated`, `Hello there! This email is to notify you of a password change that you have initiated. You have been logged out of your devices. Thank you for using monofile.`).then(() => {
+ res.send("OK")
+ }).catch((err) => {})
+ }
+
+ res.send("password changed - logged out all sessions")
+ })
+
+ authRoutes.post("/logout_sessions", requiresAccount, noAPIAccess, (req,res) => {
+ let acc = res.locals.acc as Accounts.Account
+
+ let accId = acc.id
+
+ auth.AuthTokens.filter(e => e.account == accId).forEach((v) => {
+ auth.invalidate(v.token)
+ })
+
+ res.send("logged out all sessions")
+ })
+
+ authRoutes.get("/me", requiresAccount, requiresPermissions("user"), (req,res) => {
+ let acc = res.locals.acc as Accounts.Account
+
+ let sessionToken = auth.tokenFor(req)
+ let accId = acc.id
+ res.send({
+ ...acc,
+ sessionCount: auth.AuthTokens.filter(e => e.type != "App" && e.account == accId && (e.expire > Date.now() || !e.expire)).length,
+ sessionExpires: auth.AuthTokens.find(e => e.token == sessionToken)?.expire,
+ password: undefined,
+ email:
+ auth.getType(sessionToken) == "User" || auth.getPermissions(sessionToken)?.includes("email")
+ ? acc.email
+ : undefined
+ })
+ })
+
+ authRoutes.get("/customCSS", (req,res) => {
+ let acc = res.locals.acc
+ if (acc?.customCSS) res.redirect(`/file/${acc.customCSS}`)
+ else res.send("")
+ })
+
+ return authRoutes
+}
\ No newline at end of file
diff --git a/src/server/routes/api/v0/fileApiRoutes.ts b/src/server/routes/api/v0/fileApiRoutes.ts
new file mode 100644
index 0000000..bb1953c
--- /dev/null
+++ b/src/server/routes/api/v0/fileApiRoutes.ts
@@ -0,0 +1,98 @@
+import bodyParser from "body-parser";
+import { Router } from "express";
+import * as Accounts from "../../../lib/accounts";
+import * as auth from "../../../lib/auth";
+import bytes from "bytes"
+import {writeFile} from "fs";
+
+import ServeError from "../../../lib/errors";
+import Files from "../../../lib/files";
+import { getAccount, requiresAccount, requiresPermissions } from "../../../lib/middleware";
+
+let parser = bodyParser.json({
+ type: ["text/plain","application/json"]
+})
+
+export let fileApiRoutes = Router();
+
+let config = require(`${process.cwd()}/config.json`)
+
+
+module.exports = function(files: Files) {
+
+ fileApiRoutes.use(getAccount);
+
+ fileApiRoutes.get("/list", requiresAccount, requiresPermissions("user"), (req,res) => {
+
+ let acc = res.locals.acc as Accounts.Account
+
+ if (!acc) return
+ let accId = acc.id
+
+ res.send(acc.files.map((e) => {
+ let fp = files.getFilePointer(e)
+ if (!fp) { Accounts.files.deindex(accId, e); return null }
+ return {
+ ...fp,
+ messageids: null,
+ owner: null,
+ id:e
+ }
+ }).filter(e=>e))
+
+ })
+
+ fileApiRoutes.post("/manage", parser, requiresPermissions("manage"), (req,res) => {
+
+ let acc = res.locals.acc as Accounts.Account
+
+ if (!acc) return
+ if (!req.body.target || !(typeof req.body.target == "object") || req.body.target.length < 1) return
+
+ let modified = 0
+
+ req.body.target.forEach((e:string) => {
+ if (!acc.files.includes(e)) return
+
+ let fp = files.getFilePointer(e)
+
+ if (fp.reserved) {
+ return
+ }
+
+ switch( req.body.action ) {
+ case "delete":
+ files.unlink(e, true)
+ modified++;
+ break;
+
+ case "changeFileVisibility":
+ if (!["public","anonymous","private"].includes(req.body.value)) return;
+ files.files[e].visibility = req.body.value;
+ modified++;
+ break;
+
+ case "setTag":
+ if (!req.body.value) delete files.files[e].tag
+ else {
+ if (req.body.value.toString().length > 30) return
+ files.files[e].tag = req.body.value.toString().toLowerCase()
+ }
+ modified++;
+ break;
+ }
+ })
+
+ Accounts.save().then(() => {
+ writeFile(process.cwd()+"/.data/files.json",JSON.stringify(files.files), (err) => {
+ if (err) console.log(err)
+ res.contentType("text/plain")
+ res.send(`modified ${modified} files`)
+ })
+ }).catch((err) => console.error(err))
+
+
+ })
+
+ return fileApiRoutes
+}
\ No newline at end of file
diff --git a/src/server/routes/api/v0/primaryApi.ts b/src/server/routes/api/v0/primaryApi.ts
new file mode 100644
index 0000000..3881b75
--- /dev/null
+++ b/src/server/routes/api/v0/primaryApi.ts
@@ -0,0 +1,181 @@
+import bodyParser from "body-parser";
+import express, { Router } from "express";
+import * as Accounts from "../../../lib/accounts";
+import * as auth from "../../../lib/auth";
+import axios, { AxiosResponse } from "axios"
+import { type Range } from "range-parser";
+import multer, {memoryStorage} from "multer"
+
+import ServeError from "../../../lib/errors";
+import Files from "../../../lib/files";
+import { getAccount, requiresPermissions } from "../../../lib/middleware";
+
+let parser = bodyParser.json({
+ type: ["text/plain","application/json"]
+})
+
+export let primaryApi = Router();
+
+const multerSetup = multer({storage:memoryStorage()})
+
+let config = require(`${process.cwd()}/config.json`)
+
+primaryApi.use(getAccount);
+
+module.exports = function(files: Files) {
+
+ primaryApi.get(["/file/:fileId", "/cpt/:fileId/*", "/:fileId"], async (req:express.Request,res:express.Response) => {
+
+ let acc = res.locals.acc as Accounts.Account
+
+ let file = files.getFilePointer(req.params.fileId)
+ res.setHeader("Access-Control-Allow-Origin", "*")
+ res.setHeader("Content-Security-Policy","sandbox allow-scripts")
+ if (req.query.attachment == "1") res.setHeader("Content-Disposition", "attachment")
+
+ if (file) {
+
+ if (file.visibility == "private" && acc?.id != file.owner) {
+ ServeError(res,403,"you do not own this file")
+ return
+ }
+
+ let range: Range | undefined
+
+ res.setHeader("Content-Type",file.mime)
+ if (file.sizeInBytes) {
+ res.setHeader("Content-Length",file.sizeInBytes)
+
+ if (file.chunkSize) {
+ let rng = req.range(file.sizeInBytes)
+ if (rng) {
+
+ // error handling
+ if (typeof rng == "number") {
+ res.status(rng == -1 ? 416 : 400).send()
+ return
+ }
+ if (rng.type != "bytes") {
+ res.status(400).send();
+ return
+ }
+
+ // set ranges var
+ let rngs = Array.from(rng)
+ if (rngs.length != 1) { res.status(400).send(); return }
+ range = rngs[0]
+
+ }
+ }
+ }
+
+ // supports ranges
+
+
+ files.readFileStream(req.params.fileId, range).then(async stream => {
+
+ if (range) {
+ res.status(206)
+ res.header("Content-Length", (range.end-range.start + 1).toString())
+ res.header("Content-Range", `bytes ${range.start}-${range.end}/${file.sizeInBytes}`)
+ }
+ stream.pipe(res)
+
+ }).catch((err) => {
+ ServeError(res,err.status,err.message)
+ })
+
+ } else {
+ ServeError(res, 404, "file not found")
+ }
+
+ })
+
+ primaryApi.head(["/file/:fileId", "/cpt/:fileId/*", "/:fileId"], (req: express.Request, res:express.Response) => {
+ let file = files.getFilePointer(req.params.fileId)
+ res.setHeader("Access-Control-Allow-Origin", "*")
+ res.setHeader("Content-Security-Policy","sandbox allow-scripts")
+ if (req.query.attachment == "1") res.setHeader("Content-Disposition", "attachment")
+ if (!file) {
+ res.status(404)
+ res.send()
+ } else {
+ res.setHeader("Content-Type",file.mime)
+ if (file.sizeInBytes) {
+ res.setHeader("Content-Length",file.sizeInBytes)
+ }
+ if (file.chunkSize) {
+ res.setHeader("Accept-Ranges", "bytes")
+ }
+ }
+ })
+
+ // upload handlers
+
+ primaryApi.post("/upload", requiresPermissions("upload"), multerSetup.single('file'), async (req,res) => {
+
+ let acc = res.locals.acc as Accounts.Account
+
+ if (req.file) {
+ try {
+ let prm = req.header("monofile-params")
+ let params:{[key:string]:any} = {}
+ if (prm) {
+ params = JSON.parse(prm)
+ }
+
+ files.uploadFile({
+ owner: acc?.id,
+
+ uploadId:params.uploadId,
+ name:req.file.originalname,
+ mime:req.file.mimetype
+ },req.file.buffer)
+ .then((uID) => res.send(uID))
+ .catch((stat) => {
+ res.status(stat.status);
+ res.send(`[err] ${stat.message}`)
+ })
+ } catch {
+ res.status(400)
+ res.send("[err] bad request")
+ }
+ } else {
+ res.status(400)
+ res.send("[err] bad request")
+ }
+ })
+
+ primaryApi.post("/clone", requiresPermissions("upload"), bodyParser.json({type: ["text/plain","application/json"]}) ,(req,res) => {
+
+ let acc = res.locals.acc as Accounts.Account
+
+ try {
+ axios.get(req.body.url,{responseType:"arraybuffer"}).then((data:AxiosResponse) => {
+
+ files.uploadFile({
+ owner: acc?.id,
+
+ name:req.body.url.split("/")[req.body.url.split("/").length-1] || "generic",
+ mime:data.headers["content-type"],
+ uploadId:req.body.uploadId
+ },Buffer.from(data.data))
+ .then((uID) => res.send(uID))
+ .catch((stat) => {
+ res.status(stat.status);
+ res.send(`[err] ${stat.message}`)
+ })
+
+ }).catch((err) => {
+ console.log(err)
+ res.status(400)
+ res.send(`[err] failed to fetch data`)
+ })
+ } catch {
+ res.status(500)
+ res.send("[err] an error occured")
+ }
+ })
+
+ return primaryApi
+}
\ No newline at end of file
diff --git a/src/server/routes/api/v1/api.json b/src/server/routes/api/v1/api.json
new file mode 100644
index 0000000..5c20663
--- /dev/null
+++ b/src/server/routes/api/v1/api.json
@@ -0,0 +1,7 @@
+{
+ "name": "v1",
+ "baseURL": "/api/v1",
+ "mount": [
+ "account", "admin", "file", "public"
+ ]
+}
\ No newline at end of file
From 9a589d36384f53543c96c9778231c0b324079526 Mon Sep 17 00:00:00 2001
From: stringsplit <77242831+nbitzz@users.noreply.github.com>
Date: Tue, 3 Oct 2023 20:16:05 -0700
Subject: [PATCH 004/173] clear up internal libs (@Jack5079)
---
src/server/lib/accounts.ts | 29 ++++++++++++++---------------
src/server/lib/mail.ts | 23 +++++++++--------------
2 files changed, 23 insertions(+), 29 deletions(-)
diff --git a/src/server/lib/accounts.ts b/src/server/lib/accounts.ts
index c9f755f..d456841 100644
--- a/src/server/lib/accounts.ts
+++ b/src/server/lib/accounts.ts
@@ -35,23 +35,22 @@ export interface Account {
* @returns A Promise which returns the new account's ID
*/
-export function create(username:string,pwd:string,admin:boolean=false):Promise {
- return new Promise((resolve,reject) => {
- let accId = crypto.randomBytes(12).toString("hex")
+export async function create(username:string,pwd:string,admin:boolean=false):Promise {
+ let accId = crypto.randomBytes(12).toString("hex")
- Accounts.push(
- {
- id: accId,
- username: username,
- password: password.hash(pwd),
- files: [],
- admin: admin,
- defaultFileVisibility: "public"
- }
- )
+ Accounts.push(
+ {
+ id: accId,
+ username: username,
+ password: password.hash(pwd),
+ files: [],
+ admin: admin,
+ defaultFileVisibility: "public"
+ }
+ )
- save().then(() => resolve(accId))
- })
+ await save()
+ return accId
}
/**
diff --git a/src/server/lib/mail.ts b/src/server/lib/mail.ts
index bd9ce07..3ee1b38 100644
--- a/src/server/lib/mail.ts
+++ b/src/server/lib/mail.ts
@@ -27,19 +27,14 @@ transport =
* @returns Promise which resolves to the output from nodemailer.transport.sendMail
*/
export function sendMail(to: string, subject: string, content: string) {
- return new Promise((resolve,reject) => {
- transport.sendMail({
- to,
- subject,
- "from": mailConfig.send.from,
- "html": `monofile accounts Gain control of your uploads. ${
- content
- .replace(/\/g, `@`)
- .replace(/\/g,``)
- }
If you do not believe that you are the intended recipient of this email, please disregard this message.`
- }, (err, info) => {
- if (err) reject(err)
- else resolve(info)
- })
+ return transport.sendMail({
+ to,
+ subject,
+ "from": mailConfig.send.from,
+ "html": `monofile accounts Gain control of your uploads. ${
+ content
+ .replace(/\/g, `@`)
+ .replace(/\/g,``)
+ }
If you do not believe that you are the intended recipient of this email, please disregard this message.`
})
}
\ No newline at end of file
From 5a84f766cc6237139ca0ac18790330a12b03faff Mon Sep 17 00:00:00 2001
From: stringsplit <77242831+nbitzz@users.noreply.github.com>
Date: Tue, 3 Oct 2023 20:45:49 -0700
Subject: [PATCH 005/173] =?UTF-8?q?IMPORTANT:=20readd=20British(?=
=?UTF-8?q?=F0=9F=9A=AB)=20joke?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
assets/icons/pound.svg | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/assets/icons/pound.svg b/assets/icons/pound.svg
index 22cba99..dcfc516 100644
--- a/assets/icons/pound.svg
+++ b/assets/icons/pound.svg
@@ -1 +1,7 @@
+
\ No newline at end of file
From ab617461fa5b9f565bcd44177f7dfb198b175b02 Mon Sep 17 00:00:00 2001
From: stringsplit <77242831+nbitzz@users.noreply.github.com>
Date: Tue, 3 Oct 2023 22:02:37 -0700
Subject: [PATCH 006/173] housekeeping
---
src/server/routes/api.ts | 2 +-
src/server/routes/api/v0/adminRoutes.ts | 2 +-
src/server/routes/api/v0/authRoutes.ts | 2 +-
src/server/routes/api/v0/fileApiRoutes.ts | 2 +-
src/server/routes/api/v0/primaryApi.ts | 28 ++++++++++++++++++++---
src/server/routes/api/v1/account.ts | 5 ----
6 files changed, 29 insertions(+), 12 deletions(-)
diff --git a/src/server/routes/api.ts b/src/server/routes/api.ts
index 3286a4a..1374d7d 100644
--- a/src/server/routes/api.ts
+++ b/src/server/routes/api.ts
@@ -68,7 +68,7 @@ export default class APIRouter {
async loadAPIMethods() {
let files = await readdir(APIDirectory)
- for (let v of files) { /// temporary. need to figure out something else for this
+ for (let v of files) { /// temporary (hopefully). need to figure out something else for this
let def = JSON.parse((await readFile(`${process.cwd()}/src/server/routes/api/${v}/api.json`)).toString()) as APIDefinition
this.mount(def)
}
diff --git a/src/server/routes/api/v0/adminRoutes.ts b/src/server/routes/api/v0/adminRoutes.ts
index fa4445e..243b719 100644
--- a/src/server/routes/api/v0/adminRoutes.ts
+++ b/src/server/routes/api/v0/adminRoutes.ts
@@ -23,7 +23,7 @@ adminRoutes
let config = require(`${process.cwd()}/config.json`)
-module.exports = function(files: Files) {
+module.exports = function(files: Files) {
adminRoutes.post("/reset", parser, (req,res) => {
diff --git a/src/server/routes/api/v0/authRoutes.ts b/src/server/routes/api/v0/authRoutes.ts
index a2dd3fb..b33684f 100644
--- a/src/server/routes/api/v0/authRoutes.ts
+++ b/src/server/routes/api/v0/authRoutes.ts
@@ -20,7 +20,7 @@ authRoutes.use(getAccount)
let config = require(`${process.cwd()}/config.json`)
-module.exports = function(files: Files) {
+module.exports = function(files: Files) {
authRoutes.post("/login", parser, (req,res) => {
if (typeof req.body.username != "string" || typeof req.body.password != "string") {
diff --git a/src/server/routes/api/v0/fileApiRoutes.ts b/src/server/routes/api/v0/fileApiRoutes.ts
index bb1953c..96cd176 100644
--- a/src/server/routes/api/v0/fileApiRoutes.ts
+++ b/src/server/routes/api/v0/fileApiRoutes.ts
@@ -18,7 +18,7 @@ export let fileApiRoutes = Router();
let config = require(`${process.cwd()}/config.json`)
-module.exports = function(files: Files) {
+module.exports = function(files: Files) {
fileApiRoutes.use(getAccount);
diff --git a/src/server/routes/api/v0/primaryApi.ts b/src/server/routes/api/v0/primaryApi.ts
index 3881b75..afdcec8 100644
--- a/src/server/routes/api/v0/primaryApi.ts
+++ b/src/server/routes/api/v0/primaryApi.ts
@@ -35,9 +35,16 @@ module.exports = function(files: Files) {
if (file) {
- if (file.visibility == "private" && acc?.id != file.owner) {
- ServeError(res,403,"you do not own this file")
- return
+ if (file.visibility == "private") {
+ if (acc?.id != file.owner) {
+ ServeError(res,403,"you do not own this file")
+ return
+ }
+
+ if (auth.getType(auth.tokenFor(req)) == "App" && auth.getPermissions(auth.tokenFor(req))?.includes("private")) {
+ ServeError(res,403,"insufficient permissions")
+ return
+ }
}
let range: Range | undefined
@@ -93,9 +100,23 @@ module.exports = function(files: Files) {
primaryApi.head(["/file/:fileId", "/cpt/:fileId/*", "/:fileId"], (req: express.Request, res:express.Response) => {
let file = files.getFilePointer(req.params.fileId)
+
+ if (
+ file.visibility == "private"
+ && (
+ res.locals.acc?.id != file.owner
+ || (auth.getType(auth.tokenFor(req)) == "App" && auth.getPermissions(auth.tokenFor(req))?.includes("private"))
+ )
+ ) {
+ res.status(403).send()
+ return
+ }
+
res.setHeader("Access-Control-Allow-Origin", "*")
res.setHeader("Content-Security-Policy","sandbox allow-scripts")
+
if (req.query.attachment == "1") res.setHeader("Content-Disposition", "attachment")
+
if (!file) {
res.status(404)
res.send()
@@ -107,6 +128,7 @@ module.exports = function(files: Files) {
if (file.chunkSize) {
res.setHeader("Accept-Ranges", "bytes")
}
+ res.send()
}
})
diff --git a/src/server/routes/api/v1/account.ts b/src/server/routes/api/v1/account.ts
index 242a9f1..8c8168d 100644
--- a/src/server/routes/api/v1/account.ts
+++ b/src/server/routes/api/v1/account.ts
@@ -4,10 +4,5 @@ import Files from "../../../lib/files";
let router = Router()
module.exports = function(files: Files) {
-
- router.get("/", function(req,res) {
- res.send("hello world!")
- })
-
return router
}
\ No newline at end of file
From 087454659b382ac00abead6fc917686e9f2af9cb Mon Sep 17 00:00:00 2001
From: linkability <146661751+linkability@users.noreply.github.com>
Date: Wed, 4 Oct 2023 16:10:42 +0100
Subject: [PATCH 007/173] Add middleware to account api-v1
---
src/server/routes/api/v1/account.ts | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/src/server/routes/api/v1/account.ts b/src/server/routes/api/v1/account.ts
index 8c8168d..7acbd99 100644
--- a/src/server/routes/api/v1/account.ts
+++ b/src/server/routes/api/v1/account.ts
@@ -1,8 +1,12 @@
import { Router } from "express";
import Files from "../../../lib/files";
+import { getAccount } from "../../../lib/middleware";
+
let router = Router()
+router.use(getAccount)
+
module.exports = function(files: Files) {
return router
}
\ No newline at end of file
From 01fe79d0508802cbf9b7d739c29d99f68fc24450 Mon Sep 17 00:00:00 2001
From: stringsplit <77242831+nbitzz@users.noreply.github.com>
Date: Wed, 4 Oct 2023 10:21:04 -0700
Subject: [PATCH 008/173] api-v1: add new middleware function
---
src/server/lib/middleware.ts | 17 +++++++++++++++--
1 file changed, 15 insertions(+), 2 deletions(-)
diff --git a/src/server/lib/middleware.ts b/src/server/lib/middleware.ts
index 2d312ad..812bc5a 100644
--- a/src/server/lib/middleware.ts
+++ b/src/server/lib/middleware.ts
@@ -1,4 +1,4 @@
-import * as Accounts from "./accounts";
+import { Account } from "./accounts";
import express, { type RequestHandler } from "express"
import ServeError from "../lib/errors";
import * as auth from "./auth";
@@ -70,4 +70,17 @@ export const requiresPermissions = function(...tokenPermissions: auth.TokenPermi
export const noAPIAccess: RequestHandler = function(req, res, next) {
if (auth.getType(auth.tokenFor(req)) == "App") ServeError(res, 403, "apps are not allowed to access this endpoint")
else next()
-}
\ No newline at end of file
+
+/**
+ * @description Blocks requests based on whether or not the token being used to access the route is of type `User` unless a condition is met.
+ * @param tokenPermissions Permissions which your route requires.
+ * @returns Express middleware
+ */
+
+export const noAPIAccessIf = function(condition: (acc:Account, token:string) => boolean):RequestHandler {
+ return function(req, res, next) {
+ let reqToken = auth.tokenFor(req)
+ if (auth.getType(reqToken) == "App" && !condition(res.locals.acc, reqToken)) ServeError(res, 403, "apps are not allowed to access this endpoint")
+ else next()
+ }
+}}
\ No newline at end of file
From 6e6afd274b792225ee8d142399f117b6ff73c140 Mon Sep 17 00:00:00 2001
From: stringsplit <77242831+nbitzz@users.noreply.github.com>
Date: Wed, 4 Oct 2023 10:21:33 -0700
Subject: [PATCH 009/173] api-v1: fix middleware func
---
src/server/lib/middleware.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/server/lib/middleware.ts b/src/server/lib/middleware.ts
index 812bc5a..a4c609c 100644
--- a/src/server/lib/middleware.ts
+++ b/src/server/lib/middleware.ts
@@ -70,6 +70,7 @@ export const requiresPermissions = function(...tokenPermissions: auth.TokenPermi
export const noAPIAccess: RequestHandler = function(req, res, next) {
if (auth.getType(auth.tokenFor(req)) == "App") ServeError(res, 403, "apps are not allowed to access this endpoint")
else next()
+}
/**
* @description Blocks requests based on whether or not the token being used to access the route is of type `User` unless a condition is met.
@@ -83,4 +84,4 @@ export const noAPIAccessIf = function(condition: (acc:Account, token:string) =>
if (auth.getType(reqToken) == "App" && !condition(res.locals.acc, reqToken)) ServeError(res, 403, "apps are not allowed to access this endpoint")
else next()
}
-}}
\ No newline at end of file
+}
\ No newline at end of file
From 503f5f315f1cd5c73c55d9adab07e6d019e4c556 Mon Sep 17 00:00:00 2001
From: stringsplit <77242831+nbitzz@users.noreply.github.com>
Date: Wed, 4 Oct 2023 10:41:21 -0700
Subject: [PATCH 010/173] api-v1: start work on sanitize middleware func
---
src/server/lib/middleware.ts | 31 ++++++++++++++++++++++++++++++-
1 file changed, 30 insertions(+), 1 deletion(-)
diff --git a/src/server/lib/middleware.ts b/src/server/lib/middleware.ts
index a4c609c..a1d5fb2 100644
--- a/src/server/lib/middleware.ts
+++ b/src/server/lib/middleware.ts
@@ -74,7 +74,7 @@ export const noAPIAccess: RequestHandler = function(req, res, next) {
/**
* @description Blocks requests based on whether or not the token being used to access the route is of type `User` unless a condition is met.
- * @param tokenPermissions Permissions which your route requires.
+ * @param condition Permissions which your route requires.
* @returns Express middleware
*/
@@ -84,4 +84,33 @@ export const noAPIAccessIf = function(condition: (acc:Account, token:string) =>
if (auth.getType(reqToken) == "App" && !condition(res.locals.acc, reqToken)) ServeError(res, 403, "apps are not allowed to access this endpoint")
else next()
}
+}
+
+type SchemeType = "array" | "object" | "string" | "number" | "boolean"
+
+interface SchemeObject {
+ type: "object"
+ children: {
+ [key: string]: SchemeParameter
+ }
+}
+
+interface SchemeArray {
+ type: "array",
+ children: SchemeParameter /* All children of the array must be this type */
+ | SchemeParameter[] /* Array must match this pattern */
+}
+
+type SchemeParameter = SchemeType | SchemeObject | SchemeArray
+
+/**
+ * @description Blocks requests based on whether or not the token being used to access the route is of type `User` unless a condition is met.
+ * @param tokenPermissions Permissions which your route requires.
+ * @returns Express middleware
+ */
+
+export const sanitize = function(scheme: SchemeObject):RequestHandler {
+ return function(req, res, next) {
+
+ }
}
\ No newline at end of file
From 36b59d1908a3e45a1a4088220f0accab9be93d52 Mon Sep 17 00:00:00 2001
From: linkability <146661751+linkability@users.noreply.github.com>
Date: Thu, 5 Oct 2023 01:41:09 +0100
Subject: [PATCH 011/173] fix: :bug: it does not import Accounts
---
src/server/lib/middleware.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/server/lib/middleware.ts b/src/server/lib/middleware.ts
index a1d5fb2..c51888c 100644
--- a/src/server/lib/middleware.ts
+++ b/src/server/lib/middleware.ts
@@ -1,4 +1,4 @@
-import { Account } from "./accounts";
+import * as Accounts from "./accounts";
import express, { type RequestHandler } from "express"
import ServeError from "../lib/errors";
import * as auth from "./auth";
@@ -78,7 +78,7 @@ export const noAPIAccess: RequestHandler = function(req, res, next) {
* @returns Express middleware
*/
-export const noAPIAccessIf = function(condition: (acc:Account, token:string) => boolean):RequestHandler {
+export const noAPIAccessIf = function(condition: (acc:Accounts.Account, token:string) => boolean):RequestHandler {
return function(req, res, next) {
let reqToken = auth.tokenFor(req)
if (auth.getType(reqToken) == "App" && !condition(res.locals.acc, reqToken)) ServeError(res, 403, "apps are not allowed to access this endpoint")
From 03c638b9514e44ae41d7d645659aa8f2561d5481 Mon Sep 17 00:00:00 2001
From: linkability <146661751+linkability@users.noreply.github.com>
Date: Thu, 5 Oct 2023 02:00:41 +0100
Subject: [PATCH 012/173] assertAPI instead of noAPIAccessIf
---
src/server/lib/middleware.ts | 10 ++++------
1 file changed, 4 insertions(+), 6 deletions(-)
diff --git a/src/server/lib/middleware.ts b/src/server/lib/middleware.ts
index c51888c..e9c18d2 100644
--- a/src/server/lib/middleware.ts
+++ b/src/server/lib/middleware.ts
@@ -73,15 +73,13 @@ export const noAPIAccess: RequestHandler = function(req, res, next) {
}
/**
- * @description Blocks requests based on whether or not the token being used to access the route is of type `User` unless a condition is met.
- * @param condition Permissions which your route requires.
- * @returns Express middleware
- */
+ @description Add a restriction to this route; the condition must be true to allow API requests.
+*/
-export const noAPIAccessIf = function(condition: (acc:Accounts.Account, token:string) => boolean):RequestHandler {
+export const assertAPI = function(condition: (acc:Accounts.Account, token:string) => boolean):RequestHandler {
return function(req, res, next) {
let reqToken = auth.tokenFor(req)
- if (auth.getType(reqToken) == "App" && !condition(res.locals.acc, reqToken)) ServeError(res, 403, "apps are not allowed to access this endpoint")
+ if (auth.getType(reqToken) == "App" && condition(res.locals.acc, reqToken)) ServeError(res, 403, "apps are not allowed to access this endpoint")
else next()
}
}
From 6b8d050fa53243225e81a05d529c0a743536e059 Mon Sep 17 00:00:00 2001
From: linkability <146661751+linkability@users.noreply.github.com>
Date: Thu, 5 Oct 2023 02:25:04 +0100
Subject: [PATCH 013/173] account api half done
---
src/server/routes/api/v1/account.ts | 140 +++++++++++++++++++++-
src/server/routes/api/v1/api.json | 12 +-
src/server/routes/api/v1/customization.ts | 122 +++++++++++++++++++
3 files changed, 270 insertions(+), 4 deletions(-)
create mode 100644 src/server/routes/api/v1/customization.ts
diff --git a/src/server/routes/api/v1/account.ts b/src/server/routes/api/v1/account.ts
index 7acbd99..b3009b2 100644
--- a/src/server/routes/api/v1/account.ts
+++ b/src/server/routes/api/v1/account.ts
@@ -1,12 +1,146 @@
+// Modules
+
import { Router } from "express";
-import Files from "../../../lib/files";
+import bodyParser from "body-parser";
-import { getAccount } from "../../../lib/middleware";
+// Libs
-let router = Router()
+import Files, { id_check_regex } from "../../../lib/files";
+import * as Accounts from '../../../lib/accounts'
+import * as Authentication from '../../../lib/auth'
+import { assertAPI, getAccount, noAPIAccess, requiresAccount, requiresPermissions } from "../../../lib/middleware";
+import ServeError from "../../../lib/errors";
+
+const Configuration = require(`${process.cwd()}/config.json`)
+
+const parser = bodyParser.json({
+ type: [ "type/plain", "application/json" ]
+})
+
+const router = Router()
router.use(getAccount)
module.exports = function(files: Files) {
+ router.post("/login",
+ parser,
+ (req, res) => {
+ if (typeof req.body.username != "string" || typeof req.body.password != "string") {
+ ServeError(res, 400, "please provide a username or password")
+ return
+ }
+
+ if (Authentication.validate(req.cookies.auth)) {
+ ServeError(res, 400, "you are already logged in")
+ return
+ }
+
+ const Account = Accounts.getFromUsername(req.body.username)
+
+ if (!Account || !Accounts.password.check(Account.id, req.body.password)) {
+ ServeError(res, 400, "username or password incorrect")
+ return
+ }
+
+ res.cookie("auth",
+ Authentication.create(
+ Account.id, // account id
+ (3 * 24 * 60 * 60 * 1000) // expiration time
+ )
+ )
+ res.status(200)
+ res.end()
+ }
+ )
+
+ router.post("/create",
+ parser,
+ (req, res) => {
+ if (!Configuration.accounts.registrationEnabled) {
+ ServeError(res , 403, "account registration disabled")
+ return
+ }
+
+ if (Authentication.validate(req.cookies.auth)) {
+ ServeError(res, 400, "you are already logged in")
+ return
+ }
+
+ if (Accounts.getFromUsername(req.body.username)) {
+ ServeError(res, 400, "account with this username already exists")
+ return
+ }
+
+ if (req.body.username.length < 3 || req.body.username.length > 20) {
+ ServeError(res, 400, "username must be over or equal to 3 characters or under or equal to 20 characters in length")
+ return
+ }
+
+ if (
+ (
+ req.body.username.match(/[A-Za-z0-9_\-\.]+/)
+ ||
+ []
+ )[0] != req.body.username
+ ) {
+ ServeError(res, 400, "username contains invalid characters")
+ return
+ }
+
+ if (req.body.password.length < 8) {
+ ServeError(res, 400, "password must be 8 characters or longer")
+ return
+ }
+
+ Accounts.create(
+ req.body.username,
+ req.body.password
+ ).then((Account) => {
+ res.cookie("auth", Authentication.create(
+ Account, // account id
+ (3 * 24 * 60 * 60 * 1000) // expiration time
+ ))
+ res.status(200)
+ res.end()
+ })
+ .catch(() => {
+ ServeError(res, 500, "internal server error")
+ })
+ }
+ )
+
+ router.post("/logout",
+ (req, res) => {
+ if (!Authentication.validate(req.cookies.auth)) {
+ ServeError(res, 401, "not logged in")
+ return
+ }
+
+ Authentication.invalidate(req.cookies.auth)
+ res.send("logged out")
+ }
+ )
+
+ router.put("/dfv",
+ requiresAccount,
+ requiresPermissions("manage"),
+ parser,
+ (req, res) => {
+ const Account = res.locals.acc as Accounts.Account
+
+ if (['public', 'private', 'anonymous'].includes(req.body.defaultFileVisibility)) {
+ Account.defaultFileVisibility = req.body.defaultFileVisibility
+
+ Accounts.save()
+
+ res.send(`dfv has been set to ${Account.defaultFileVisibility}`)
+ } else {
+ res.status(400)
+
+ res.send("invalid dfv")
+ }
+ }
+ )
+
return router
}
\ No newline at end of file
diff --git a/src/server/routes/api/v1/api.json b/src/server/routes/api/v1/api.json
index 5c20663..84de338 100644
--- a/src/server/routes/api/v1/api.json
+++ b/src/server/routes/api/v1/api.json
@@ -2,6 +2,16 @@
"name": "v1",
"baseURL": "/api/v1",
"mount": [
- "account", "admin", "file", "public"
+ "account",
+ "admin",
+ "public",
+ {
+ "file": "file",
+ "to": "/account/files"
+ },
+ {
+ "file": "customization",
+ "to": "/account/customization"
+ }
]
}
\ No newline at end of file
diff --git a/src/server/routes/api/v1/customization.ts b/src/server/routes/api/v1/customization.ts
new file mode 100644
index 0000000..ddea487
--- /dev/null
+++ b/src/server/routes/api/v1/customization.ts
@@ -0,0 +1,122 @@
+// Modules
+
+import { Router } from "express";
+import bodyParser from "body-parser";
+
+// Libs
+
+import Files, { id_check_regex } from "../../../lib/files";
+import * as Accounts from '../../../lib/accounts'
+import { getAccount, requiresAccount, requiresPermissions } from "../../../lib/middleware";
+
+const Configuration = require(`${process.cwd()}/config.json`)
+
+const parser = bodyParser.json({
+ type: [ "type/plain", "application/json" ]
+})
+
+const router = Router()
+
+router.use(getAccount)
+
+module.exports = function(files: Files) {
+ router.put("/css",
+ requiresAccount,
+ requiresPermissions("customize"),
+ parser,
+ (req, res) => {
+ const Account = res.locals.acc as Accounts.Account
+
+ if (typeof req.body.fileId != "string") req.body.fileId = undefined;
+
+ if (
+ !req.body.fileId
+ ||
+ (req.body.fileId.match(id_check_regex) == req.body.fileId
+ && req.body.fileId.length <= Configuration.maxUploadIdLength)
+ ) {
+ Account.customCSS = req.body.fileId || undefined
+
+ if (!req.body.fileId) delete Account.customCSS;
+
+ Accounts.save()
+
+ res.send("custom css saved")
+ } else {
+ res.status(400)
+
+ res.send("invalid fileid")
+ }
+ }
+ )
+
+ // authRoutes.get("/customCSS", (req,res) => {
+ // let acc = res.locals.acc
+ // if (acc?.customCSS) res.redirect(`/file/${acc.customCSS}`)
+ // else res.send("")
+ // })
+
+ router.get('/css',
+ requiresAccount,
+ (req, res) => {
+ const Account = res.locals.acc
+
+ if (Account?.customCSS) res.redirect(`/file/${Account.customCSS}`)
+ else res.send("");
+ }
+ )
+
+ router.put("/embed/color",
+ requiresAccount,
+ requiresPermissions("customize"),
+ parser,
+ (req, res) => {
+ const Account = res.locals.acc as Accounts.Account
+
+ if (typeof req.body.color != "string") req.body.color = undefined;
+
+ if (
+ !req.body.color
+ || (req.body.color.toLowerCase().match(/[a-f0-9]+/) == req.body.color.toLowerCase())
+ && req.body.color.length == 6
+ ) {
+ if (!Account.embed) Account.embed = {};
+
+ Account.embed.color = req.body.color || undefined
+
+ if (!req.body.color) delete Account.embed.color;
+
+ Accounts.save()
+
+ res.send("custom embed color saved")
+ } else {
+ res.status(400)
+
+ res.send("invalid hex code")
+ }
+ }
+ )
+
+ router.put("/embed/size",
+ requiresAccount,
+ requiresPermissions("customize"),
+ parser,
+ (req, res) => {
+ const Account = res.locals.acc as Accounts.Account
+
+ if (typeof req.body.largeImage != "boolean") req.body.color = false;
+
+ if (!Account.embed) Account.embed = {};
+
+ Account.embed.largeImage = req.body.largeImage
+
+ if (!req.body.largeImage) delete Account.embed.largeImage;
+
+ Accounts.save()
+
+ res.send(`custom embed image size saved`)
+ }
+ )
+
+ return router
+}
\ No newline at end of file
From c51d8dab95c79700c5aee60c482e348f0d3deb36 Mon Sep 17 00:00:00 2001
From: linkability <146661751+linkability@users.noreply.github.com>
Date: Thu, 5 Oct 2023 02:26:23 +0100
Subject: [PATCH 014/173] accidentally left comment in from old api
---
src/server/routes/api/v1/customization.ts | 6 ------
1 file changed, 6 deletions(-)
diff --git a/src/server/routes/api/v1/customization.ts b/src/server/routes/api/v1/customization.ts
index ddea487..5c282e8 100644
--- a/src/server/routes/api/v1/customization.ts
+++ b/src/server/routes/api/v1/customization.ts
@@ -50,12 +50,6 @@ module.exports = function(files: Files) {
}
)
- // authRoutes.get("/customCSS", (req,res) => {
- // let acc = res.locals.acc
- // if (acc?.customCSS) res.redirect(`/file/${acc.customCSS}`)
- // else res.send("")
- // })
-
router.get('/css',
requiresAccount,
(req, res) => {
From 9e83d751bdc368bef9be1b57141c82a3f2b821b4 Mon Sep 17 00:00:00 2001
From: stringsplit <77242831+nbitzz@users.noreply.github.com>
Date: Wed, 4 Oct 2023 19:02:11 -0700
Subject: [PATCH 015/173] api-v1: Charlie is on meth
---
src/server/routes/api/v1/account.ts | 20 ++++----
src/server/routes/api/v1/customization.ts | 60 ++++++++---------------
2 files changed, 31 insertions(+), 49 deletions(-)
diff --git a/src/server/routes/api/v1/account.ts b/src/server/routes/api/v1/account.ts
index b3009b2..81b215c 100644
--- a/src/server/routes/api/v1/account.ts
+++ b/src/server/routes/api/v1/account.ts
@@ -19,11 +19,11 @@ const parser = bodyParser.json({
const router = Router()
-router.use(getAccount)
+router.use(getAccount, parser)
module.exports = function(files: Files) {
- router.post("/login",
- parser,
+ router.post(
+ "/login",
(req, res) => {
if (typeof req.body.username != "string" || typeof req.body.password != "string") {
ServeError(res, 400, "please provide a username or password")
@@ -53,8 +53,8 @@ module.exports = function(files: Files) {
}
)
- router.post("/create",
- parser,
+ router.post(
+ "/create",
(req, res) => {
if (!Configuration.accounts.registrationEnabled) {
ServeError(res , 403, "account registration disabled")
@@ -109,7 +109,8 @@ module.exports = function(files: Files) {
}
)
- router.post("/logout",
+ router.post(
+ "/logout",
(req, res) => {
if (!Authentication.validate(req.cookies.auth)) {
ServeError(res, 401, "not logged in")
@@ -121,10 +122,9 @@ module.exports = function(files: Files) {
}
)
- router.put("/dfv",
- requiresAccount,
- requiresPermissions("manage"),
- parser,
+ router.put(
+ "/dfv",
+ requiresAccount, requiresPermissions("manage"),
(req, res) => {
const Account = res.locals.acc as Accounts.Account
diff --git a/src/server/routes/api/v1/customization.ts b/src/server/routes/api/v1/customization.ts
index 5c282e8..2986612 100644
--- a/src/server/routes/api/v1/customization.ts
+++ b/src/server/routes/api/v1/customization.ts
@@ -8,6 +8,7 @@ import bodyParser from "body-parser";
import Files, { id_check_regex } from "../../../lib/files";
import * as Accounts from '../../../lib/accounts'
import { getAccount, requiresAccount, requiresPermissions } from "../../../lib/middleware";
+import ServeError from "../../../lib/errors";
const Configuration = require(`${process.cwd()}/config.json`)
@@ -17,14 +18,13 @@ const parser = bodyParser.json({
const router = Router()
-router.use(getAccount)
+router.use(getAccount, parser)
module.exports = function(files: Files) {
- router.put("/css",
- requiresAccount,
- requiresPermissions("customize"),
- parser,
- (req, res) => {
+ router.put(
+ "/css",
+ requiresAccount, requiresPermissions("customize"),
+ async (req, res) => {
const Account = res.locals.acc as Accounts.Account
if (typeof req.body.fileId != "string") req.body.fileId = undefined;
@@ -33,20 +33,13 @@ module.exports = function(files: Files) {
!req.body.fileId
||
(req.body.fileId.match(id_check_regex) == req.body.fileId
- && req.body.fileId.length <= Configuration.maxUploadIdLength)
+ && req.body.fileId.length <= Configuration.maxUploadIdLength)
) {
Account.customCSS = req.body.fileId || undefined
- if (!req.body.fileId) delete Account.customCSS;
-
- Accounts.save()
-
+ await Accounts.save()
res.send("custom css saved")
- } else {
- res.status(400)
-
- res.send("invalid fileid")
- }
+ } else ServeError(res, 400, "invalid fileId")
}
)
@@ -61,10 +54,8 @@ module.exports = function(files: Files) {
)
router.put("/embed/color",
- requiresAccount,
- requiresPermissions("customize"),
- parser,
- (req, res) => {
+ requiresAccount, requiresPermissions("customize"),
+ async (req, res) => {
const Account = res.locals.acc as Accounts.Account
if (typeof req.body.color != "string") req.body.color = undefined;
@@ -74,40 +65,31 @@ module.exports = function(files: Files) {
|| (req.body.color.toLowerCase().match(/[a-f0-9]+/) == req.body.color.toLowerCase())
&& req.body.color.length == 6
) {
- if (!Account.embed) Account.embed = {};
+ if (!Account.embed) Account.embed = {};
Account.embed.color = req.body.color || undefined
- if (!req.body.color) delete Account.embed.color;
-
- Accounts.save()
-
+ await Accounts.save()
res.send("custom embed color saved")
- } else {
- res.status(400)
- res.send("invalid hex code")
- }
+ } else ServeError(res,400,"invalid hex code")
}
)
router.put("/embed/size",
- requiresAccount,
- requiresPermissions("customize"),
- parser,
- (req, res) => {
+ requiresAccount, requiresPermissions("customize"),
+ async (req, res) => {
const Account = res.locals.acc as Accounts.Account
- if (typeof req.body.largeImage != "boolean") req.body.color = false;
+ if (typeof req.body.largeImage != "boolean") {
+ ServeError(res, 400, "largeImage must be bool");
+ return
+ }
if (!Account.embed) Account.embed = {};
-
Account.embed.largeImage = req.body.largeImage
-
- if (!req.body.largeImage) delete Account.embed.largeImage;
-
- Accounts.save()
+ await Accounts.save()
res.send(`custom embed image size saved`)
}
)
From 01bb4684a3ad33f5210c9e5e1774a684725cc7df Mon Sep 17 00:00:00 2001
From: linkability <146661751+linkability@users.noreply.github.com>
Date: Thu, 5 Oct 2023 20:08:39 +0100
Subject: [PATCH 016/173] delete your own account endpoint
---
src/server/routes/api/v1/account.ts | 31 +++++++++++++++++++++++++++++
1 file changed, 31 insertions(+)
diff --git a/src/server/routes/api/v1/account.ts b/src/server/routes/api/v1/account.ts
index b3009b2..6b671a7 100644
--- a/src/server/routes/api/v1/account.ts
+++ b/src/server/routes/api/v1/account.ts
@@ -1,5 +1,6 @@
// Modules
+import { writeFile } from 'fs'
import { Router } from "express";
import bodyParser from "body-parser";
@@ -142,5 +143,35 @@ module.exports = function(files: Files) {
}
)
+ router.delete("/me",
+ requiresAccount,
+ noAPIAccess,
+ parser,
+ (req, res) => {
+ const Account = res.locals.acc as Accounts.Account
+
+ const accountId = Account.id
+
+ Authentication.AuthTokens.filter(e => e.account == accountId).forEach((token) => {
+ Authentication.invalidate(token.token)
+ })
+
+ const deleteAccount = () => Accounts.deleteAccount(accountId).then(_ => res.send("account deleted"))
+
+ if (req.body.deleteFiles) {
+ const Files = Account.files.map(e => e)
+
+ for (let fileId of Files) {
+ files.unlink(fileId, true).catch(err => console.error)
+ }
+
+ writeFile(process.cwd() + "/.data/files.json", JSON.stringify(files.files), (err) => {
+ if (err) console.log(err)
+ deleteAccount()
+ })
+ } else deleteAccount()
+ }
+ )
+
return router
}
\ No newline at end of file
From e574438ea8ceafbb45746652825938c5c4a9f675 Mon Sep 17 00:00:00 2001
From: linkability <146661751+linkability@users.noreply.github.com>
Date: Thu, 5 Oct 2023 20:51:12 +0100
Subject: [PATCH 017/173] files is not mounted to accounts
---
src/server/routes/api/v1/api.json | 5 +----
1 file changed, 1 insertion(+), 4 deletions(-)
diff --git a/src/server/routes/api/v1/api.json b/src/server/routes/api/v1/api.json
index 84de338..694f259 100644
--- a/src/server/routes/api/v1/api.json
+++ b/src/server/routes/api/v1/api.json
@@ -5,10 +5,7 @@
"account",
"admin",
"public",
- {
- "file": "file",
- "to": "/account/files"
- },
+ "file",
{
"file": "customization",
"to": "/account/customization"
From 03fa036e3836922cf3dcce0a2e65f57ff8435603 Mon Sep 17 00:00:00 2001
From: linkability <146661751+linkability@users.noreply.github.com>
Date: Thu, 5 Oct 2023 23:31:52 +0100
Subject: [PATCH 018/173] change nickname added to apiv1
---
src/server/routes/api/v1/account.ts | 53 +++++++++++++++++++++++++++++
1 file changed, 53 insertions(+)
diff --git a/src/server/routes/api/v1/account.ts b/src/server/routes/api/v1/account.ts
index 3703067..7da3afa 100644
--- a/src/server/routes/api/v1/account.ts
+++ b/src/server/routes/api/v1/account.ts
@@ -11,6 +11,7 @@ import * as Accounts from '../../../lib/accounts'
import * as Authentication from '../../../lib/auth'
import { assertAPI, getAccount, noAPIAccess, requiresAccount, requiresPermissions } from "../../../lib/middleware";
import ServeError from "../../../lib/errors";
+import { sendMail } from '../../../lib/mail';
const Configuration = require(`${process.cwd()}/config.json`)
@@ -173,5 +174,57 @@ module.exports = function(files: Files) {
}
)
+ router.put("/me/name",
+ requiresAccount,
+ noAPIAccess,
+ parser,
+ (req, res) => {
+ const Account = res.locals.acc as Accounts.Account
+
+ const newUsername = req.body.username
+
+ if (
+ typeof newUsername != "string"
+ ||
+ newUsername.length < 3
+ ||
+ req.body.username.length > 20
+ ) {
+ ServeError(res, 400, "username must be between 3 and 20 characters in length")
+ return
+ }
+
+ if (Accounts.getFromUsername(newUsername)) {
+ ServeError(res, 400, "account with this username already exists")
+ }
+
+ if (
+ (
+ newUsername.match(/[A-Za-z0-9_\-\.]+/)
+ ||
+ []
+ )[0] != req.body.username
+ ) {
+ ServeError(res, 400, "username contains invalid characters")
+ return
+ }
+
+ Account.username = newUsername
+ Accounts.save()
+
+ if (Account.email) {
+ sendMail(
+ Account.email,
+ `Your login details have been updated`,
+ `Hello there! Your username has been updated to ${newUsername}. Please update your devices accordingly. Thank you for using monofile.`
+ ).then(() => {
+ res.send("OK")
+ }).catch((err) => {})
+ }
+ }
+ )
+
+
+
return router
}
\ No newline at end of file
From a1b6917831dbea458873e69fa3a6351a5665f12a Mon Sep 17 00:00:00 2001
From: linkability <146661751+linkability@users.noreply.github.com>
Date: Fri, 6 Oct 2023 22:34:40 +0100
Subject: [PATCH 019/173] add admin password and elevate route
---
src/server/routes/api/v1/admin.ts | 82 +++++++++++++++++++++++++++++--
1 file changed, 79 insertions(+), 3 deletions(-)
diff --git a/src/server/routes/api/v1/admin.ts b/src/server/routes/api/v1/admin.ts
index 8c8168d..bf07ae5 100644
--- a/src/server/routes/api/v1/admin.ts
+++ b/src/server/routes/api/v1/admin.ts
@@ -1,8 +1,84 @@
-import { Router } from "express";
-import Files from "../../../lib/files";
+// Modules
-let router = Router()
+import { writeFile } from 'fs'
+import { Router } from "express";
+import bodyParser from "body-parser";
+
+// Libs
+
+import Files, { id_check_regex } from "../../../lib/files";
+import * as Accounts from '../../../lib/accounts'
+import * as Authentication from '../../../lib/auth'
+import { assertAPI, getAccount, noAPIAccess, requiresAccount, requiresAdmin, requiresPermissions } from "../../../lib/middleware";
+import ServeError from "../../../lib/errors";
+import { sendMail } from '../../../lib/mail';
+
+const Configuration = require(`${process.cwd()}/config.json`)
+
+const parser = bodyParser.json({
+ type: [ "type/plain", "application/json" ]
+})
+
+const router = Router()
+
+router.use(getAccount, requiresAccount, requiresAdmin, parser)
module.exports = function(files: Files) {
+ router.patch(
+ "/account/:username/password",
+ (req, res) => {
+ const Account = res.locals.acc
+
+ const targetUsername = req.params.username
+ const password = req.body.password
+
+ if (typeof password !== "string") {
+ ServeError(res, 404, "")
+ return
+ }
+
+ const targetAccount = Accounts.getFromUsername(targetUsername)
+
+ if (!targetAccount) {
+ ServeError(res, 404, "")
+ return
+ }
+
+ Accounts.password.set( targetAccount.id, password )
+
+ Authentication.AuthTokens.filter(e => e.account == targetAccount?.id).forEach((accountToken) => {
+ Authentication.invalidate(accountToken.token)
+ })
+
+ if (targetAccount.email) {
+ sendMail(targetAccount.email, `Your login details have been updated`, `Hello there! This email is to notify you of a password change that an administrator, ${Account.username}, has initiated. You have been logged out of your devices. Thank you for using monofile.`).then(() => {
+ res.send("OK")
+ }).catch((err) => {})
+ }
+
+ res.send()
+ }
+ )
+
+ router.patch(
+ "/account/:username/elevate",
+ (req, res) => {
+ const targetUsername = req.params.username
+ const targetAccount = Accounts.getFromUsername(targetUsername)
+
+ if (!targetAccount) {
+ ServeError(res, 404, "")
+ return
+ }
+
+ targetAccount.admin = true
+ Accounts.save()
+
+ res.send()
+ }
+ )
+
+
+
return router
}
\ No newline at end of file
From d87583392ee1aef03eda74c82024e26141c86212 Mon Sep 17 00:00:00 2001
From: linkability <146661751+linkability@users.noreply.github.com>
Date: Fri, 6 Oct 2023 22:35:09 +0100
Subject: [PATCH 020/173] http method changes and use ServeError
---
src/server/routes/api/v1/account.ts | 8 +++-----
1 file changed, 3 insertions(+), 5 deletions(-)
diff --git a/src/server/routes/api/v1/account.ts b/src/server/routes/api/v1/account.ts
index 7da3afa..6699ac8 100644
--- a/src/server/routes/api/v1/account.ts
+++ b/src/server/routes/api/v1/account.ts
@@ -124,7 +124,7 @@ module.exports = function(files: Files) {
}
)
- router.put(
+ router.patch(
"/dfv",
requiresAccount, requiresPermissions("manage"),
(req, res) => {
@@ -137,9 +137,7 @@ module.exports = function(files: Files) {
res.send(`dfv has been set to ${Account.defaultFileVisibility}`)
} else {
- res.status(400)
-
- res.send("invalid dfv")
+ ServeError(res, 400, "invalid dfv")
}
}
)
@@ -174,7 +172,7 @@ module.exports = function(files: Files) {
}
)
- router.put("/me/name",
+ router.patch("/me/name",
requiresAccount,
noAPIAccess,
parser,
From 44133630a7f6d003d2b0e546dbb5e192011e9446 Mon Sep 17 00:00:00 2001
From: linkability <146661751+linkability@users.noreply.github.com>
Date: Fri, 6 Oct 2023 22:43:58 +0100
Subject: [PATCH 021/173] fix: :bug: body is not sent in delete requests
requiring a parameter
Switches from using req.body to using req.params to know whether or not to delete their files
---
src/server/routes/api/v1/account.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/server/routes/api/v1/account.ts b/src/server/routes/api/v1/account.ts
index 6699ac8..f068a1b 100644
--- a/src/server/routes/api/v1/account.ts
+++ b/src/server/routes/api/v1/account.ts
@@ -142,7 +142,7 @@ module.exports = function(files: Files) {
}
)
- router.delete("/me",
+ router.delete("/me/:deleteFiles",
requiresAccount,
noAPIAccess,
parser,
@@ -157,7 +157,7 @@ module.exports = function(files: Files) {
const deleteAccount = () => Accounts.deleteAccount(accountId).then(_ => res.send("account deleted"))
- if (req.body.deleteFiles) {
+ if (Boolean(req.params.deleteFiles)) {
const Files = Account.files.map(e => e)
for (let fileId of Files) {
From e5b8b83e6b5e1580845709e44f118da4c82fa13d Mon Sep 17 00:00:00 2001
From: linkability <146661751+linkability@users.noreply.github.com>
Date: Fri, 6 Oct 2023 22:55:00 +0100
Subject: [PATCH 022/173] admin delete user endpoint api-v1
---
src/server/routes/api/v1/admin.ts | 40 +++++++++++++++++++++++++++++--
1 file changed, 38 insertions(+), 2 deletions(-)
diff --git a/src/server/routes/api/v1/admin.ts b/src/server/routes/api/v1/admin.ts
index bf07ae5..9dfbb0c 100644
--- a/src/server/routes/api/v1/admin.ts
+++ b/src/server/routes/api/v1/admin.ts
@@ -27,7 +27,7 @@ module.exports = function(files: Files) {
router.patch(
"/account/:username/password",
(req, res) => {
- const Account = res.locals.acc
+ const Account = res.locals.acc as Accounts.Account
const targetUsername = req.params.username
const password = req.body.password
@@ -78,7 +78,43 @@ module.exports = function(files: Files) {
}
)
-
+ router.delete("/account/:username/:deleteFiles",
+ requiresAccount,
+ noAPIAccess,
+ parser,
+ (req, res) => {
+ const targetUsername = req.params.username
+ const deleteFiles = req.params.deleteFiles
+
+ const targetAccount = Accounts.getFromUsername(targetUsername)
+
+ if (!targetAccount) {
+ ServeError(res, 404, "")
+ return
+ }
+
+ const accountId = targetAccount.id
+
+ Authentication.AuthTokens.filter(e => e.account == accountId).forEach((token) => {
+ Authentication.invalidate(token.token)
+ })
+
+ const deleteAccount = () => Accounts.deleteAccount(accountId).then(_ => res.send("account deleted"))
+
+ if (Boolean(deleteFiles)) {
+ const Files = targetAccount.files.map(e => e)
+
+ for (let fileId of Files) {
+ files.unlink(fileId, true).catch(err => console.error)
+ }
+
+ writeFile(process.cwd() + "/.data/files.json", JSON.stringify(files.files), (err) => {
+ if (err) console.log(err)
+ deleteAccount()
+ })
+ } else deleteAccount()
+ }
+ )
return router
}
\ No newline at end of file
From 3dec4d401e15c9a27b68a2b8a611631a498dfea9 Mon Sep 17 00:00:00 2001
From: unlinkability <146661751+linkability@users.noreply.github.com>
Date: Mon, 9 Oct 2023 16:45:30 +0100
Subject: [PATCH 023/173] Remove Boolean from checks
Co-authored-by: Jack W. <29169102+Jack5079@users.noreply.github.com>
---
src/server/routes/api/v1/account.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/server/routes/api/v1/account.ts b/src/server/routes/api/v1/account.ts
index f068a1b..e4c9e74 100644
--- a/src/server/routes/api/v1/account.ts
+++ b/src/server/routes/api/v1/account.ts
@@ -157,7 +157,7 @@ module.exports = function(files: Files) {
const deleteAccount = () => Accounts.deleteAccount(accountId).then(_ => res.send("account deleted"))
- if (Boolean(req.params.deleteFiles)) {
+ if (req.params.deleteFiles) {
const Files = Account.files.map(e => e)
for (let fileId of Files) {
From dbe927f367bd070a94dfe9e4235b1186b45c03ed Mon Sep 17 00:00:00 2001
From: unlinkability <146661751+linkability@users.noreply.github.com>
Date: Mon, 9 Oct 2023 16:45:47 +0100
Subject: [PATCH 024/173] Remove Boolean from checks
Co-authored-by: Jack W. <29169102+Jack5079@users.noreply.github.com>
---
src/server/routes/api/v1/admin.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/server/routes/api/v1/admin.ts b/src/server/routes/api/v1/admin.ts
index 9dfbb0c..fc00b50 100644
--- a/src/server/routes/api/v1/admin.ts
+++ b/src/server/routes/api/v1/admin.ts
@@ -101,7 +101,7 @@ module.exports = function(files: Files) {
const deleteAccount = () => Accounts.deleteAccount(accountId).then(_ => res.send("account deleted"))
- if (Boolean(deleteFiles)) {
+ if (deleteFiles) {
const Files = targetAccount.files.map(e => e)
for (let fileId of Files) {
From 0405f89542d3533bc01b17a97e93188606a7f8c4 Mon Sep 17 00:00:00 2001
From: stringsplit <77242831+nbitzz@users.noreply.github.com>
Date: Wed, 11 Oct 2023 12:57:35 -0700
Subject: [PATCH 025/173] weed smoker charlie
---
src/server/routes/api/v1/account.ts | 20 +++-----------------
1 file changed, 3 insertions(+), 17 deletions(-)
diff --git a/src/server/routes/api/v1/account.ts b/src/server/routes/api/v1/account.ts
index e4c9e74..d6791d7 100644
--- a/src/server/routes/api/v1/account.ts
+++ b/src/server/routes/api/v1/account.ts
@@ -142,9 +142,8 @@ module.exports = function(files: Files) {
}
)
- router.delete("/me/:deleteFiles",
- requiresAccount,
- noAPIAccess,
+ router.delete("/me",
+ requiresAccount, noAPIAccess,
parser,
(req, res) => {
const Account = res.locals.acc as Accounts.Account
@@ -155,20 +154,7 @@ module.exports = function(files: Files) {
Authentication.invalidate(token.token)
})
- const deleteAccount = () => Accounts.deleteAccount(accountId).then(_ => res.send("account deleted"))
-
- if (req.params.deleteFiles) {
- const Files = Account.files.map(e => e)
-
- for (let fileId of Files) {
- files.unlink(fileId, true).catch(err => console.error)
- }
-
- writeFile(process.cwd() + "/.data/files.json", JSON.stringify(files.files), (err) => {
- if (err) console.log(err)
- deleteAccount()
- })
- } else deleteAccount()
+ Accounts.deleteAccount(accountId).then(_ => res.send("account deleted"))
}
)
From 365aace294fc7f1c60a677f8ba105f0a083b45a1 Mon Sep 17 00:00:00 2001
From: "Jack W."
Date: Tue, 24 Oct 2023 16:27:08 -0400
Subject: [PATCH 026/173] refactor: :recycle: Use real async in file.ts, change
FileUploadSettings to match FilePointer properties
---
src/server/lib/files.ts | 730 +++++++++++++------------
src/server/routes/api/v0/primaryApi.ts | 363 ++++++------
2 files changed, 586 insertions(+), 507 deletions(-)
diff --git a/src/server/lib/files.ts b/src/server/lib/files.ts
index 259761e..e2d6997 100644
--- a/src/server/lib/files.ts
+++ b/src/server/lib/files.ts
@@ -1,14 +1,16 @@
-import axios from "axios";
-import Discord, { Client, TextBasedChannel } from "discord.js";
-import { readFile, writeFile } from "fs";
-import { Readable } from "node:stream";
-import crypto from "node:crypto";
-import { files } from "./accounts";
+import axios from "axios"
+import Discord, { Client, Message, TextBasedChannel } from "discord.js"
+import { readFile, writeFile } from "node:fs/promises"
+import { Readable } from "node:stream"
+import crypto from "node:crypto"
+import { files } from "./accounts"
-import * as Accounts from "./accounts";
+import * as Accounts from "./accounts"
export let id_check_regex = /[A-Za-z0-9_\-\.\!\=\:\&\$\,\+\;\@\~\*\(\)\']+/
-export let alphanum = Array.from("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890")
+export let alphanum = Array.from(
+ "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
+)
// bad solution but whatever
@@ -19,72 +21,66 @@ export type FileVisibility = "public" | "anonymous" | "private"
* @param length Length of the ID
* @returns a random alphanumeric string
*/
-export function generateFileId(length:number=5) {
+export function generateFileId(length: number = 5) {
let fid = ""
for (let i = 0; i < length; i++) {
- fid += alphanum[crypto.randomInt(0,alphanum.length)]
+ fid += alphanum[crypto.randomInt(0, alphanum.length)]
}
return fid
}
-export interface FileUploadSettings {
- name?: string,
- mime: string,
- uploadId?: string,
- owner?:string
-}
+export type FileUploadSettings = Partial> &
+ Pick & { uploadId?: string }
export interface Configuration {
- maxDiscordFiles: number,
- maxDiscordFileSize: number,
- targetGuild: string,
- targetChannel: string,
- requestTimeout: number,
- maxUploadIdLength: number,
+ maxDiscordFiles: number
+ maxDiscordFileSize: number
+ targetGuild: string
+ targetChannel: string
+ requestTimeout: number
+ maxUploadIdLength: number
accounts: {
- registrationEnabled: boolean,
+ registrationEnabled: boolean
requiredForUpload: boolean
- },
+ }
- trustProxy: boolean,
+ trustProxy: boolean
forceSSL: boolean
}
export interface FilePointer {
- filename:string,
- mime:string,
- messageids:string[],
- owner?:string,
- sizeInBytes?:number,
- tag?:string,
- visibility?:FileVisibility,
- reserved?: boolean,
+ filename: string
+ mime: string
+ messageids: string[]
+ owner?: string
+ sizeInBytes?: number
+ tag?: string
+ visibility?: FileVisibility
+ reserved?: boolean
chunkSize?: number
}
export interface StatusCodeError {
- status: number,
+ status: number
message: string
}
/* */
export default class Files {
-
config: Configuration
client: Client
- files: {[key:string]:FilePointer} = {}
+ files: { [key: string]: FilePointer } = {}
uploadChannel?: TextBasedChannel
constructor(client: Client, config: Configuration) {
+ this.config = config
+ this.client = client
- this.config = config;
- this.client = client;
-
- client.on("ready",() => {
+ client.on("ready", () => {
console.log("Discord OK!")
-
+
client.guilds.fetch(config.targetGuild).then((g) => {
g.channels.fetch(config.targetChannel).then((a) => {
if (a?.isTextBased()) {
@@ -94,168 +90,163 @@ export default class Files {
})
})
- readFile(process.cwd()+"/.data/files.json",(err,buf) => {
- if (err) {console.log(err);return}
- this.files = JSON.parse(buf.toString() || "{}")
- })
-
+ readFile(process.cwd() + "/.data/files.json")
+ .then((buf) => {
+ this.files = JSON.parse(buf.toString() || "{}")
+ })
+ .catch(console.error)
}
-
+
/**
* @description Uploads a new file
- * @param settings Settings for your new upload
- * @param fBuffer Buffer containing file content
+ * @param metadata Settings for your new upload
+ * @param buffer Buffer containing file content
* @returns Promise which resolves to the ID of the new file
*/
- uploadFile(settings:FileUploadSettings,fBuffer:Buffer):Promise {
- return new Promise(async (resolve,reject) => {
- if (!this.uploadChannel) {
- reject({status:503,message:"server is not ready - please try again later"})
- return
+ async uploadFile(
+ metadata: FileUploadSettings,
+ buffer: Buffer
+ ): Promise {
+ if (!this.uploadChannel)
+ throw {
+ status: 503,
+ message: "server is not ready - please try again later",
}
- if (!settings.name || !settings.mime) {
- reject({status:400,message:"missing name/mime"});
- return
+ if (!metadata.filename || !metadata.mime)
+ throw { status: 400, message: "missing filename/mime" }
+
+ let uploadId = (metadata.uploadId || generateFileId()).toString()
+
+ if (
+ (uploadId.match(id_check_regex) || [])[0] != uploadId ||
+ uploadId.length > this.config.maxUploadIdLength
+ )
+ throw { status: 400, message: "invalid id" }
+
+ if (
+ this.files[uploadId] &&
+ (metadata.owner
+ ? this.files[uploadId].owner != metadata.owner
+ : true)
+ )
+ throw {
+ status: 400,
+ message: "you are not the owner of this file id",
}
- if (!settings.owner && this.config.accounts.requiredForUpload) {
- reject({status:401,message:"an account is required for upload"});
- return
- }
-
- let uploadId = (settings.uploadId || generateFileId()).toString();
-
- if ((uploadId.match(id_check_regex) || [])[0] != uploadId || uploadId.length > this.config.maxUploadIdLength) {
- reject({status:400,message:"invalid id"});return
- }
-
- if (this.files[uploadId] && (settings.owner ? this.files[uploadId].owner != settings.owner : true)) {
- reject({status:400,message:"you are not the owner of this file id"});
- return
+ if (this.files[uploadId] && this.files[uploadId].reserved)
+ throw {
+ status: 400,
+ message:
+ "already uploading this file. if your file is stuck in this state, contact an administrator",
}
- if (this.files[uploadId] && this.files[uploadId].reserved) {
- reject({status:400,message:"already uploading this file. if your file is stuck in this state, contact an administrator"});
- return
- }
+ if (metadata.filename.length > 128)
+ throw { status: 400, message: "name too long" }
- if (settings.name.length > 128) {
- reject({status:400,message:"name too long"});
- return
- }
+ if (metadata.mime.length > 128)
+ throw { status: 400, message: "mime too long" }
- if (settings.mime.length > 128) {
- reject({status:400,message:"mime too long"});
- return
- }
+ // reserve file, hopefully should prevent
+ // large files breaking
- // reserve file, hopefully should prevent
- // large files breaking
+ let existingFile = this.files[uploadId]
- let ogf = this.files[uploadId]
+ // save
- this.files[uploadId] = {
- filename:settings.name,
- messageids:[],
- mime:settings.mime,
- sizeInBytes:0,
+ if (metadata.owner) {
+ await files.index(metadata.owner, uploadId)
+ }
- owner:settings.owner,
- visibility: settings.owner ? "private" : "public",
- reserved: true,
+ // get buffer
+ if (
+ buffer.byteLength >=
+ this.config.maxDiscordFileSize * this.config.maxDiscordFiles
+ )
+ throw { status: 400, message: "file too large" }
- chunkSize: this.config.maxDiscordFileSize
- }
-
- // save
-
- if (settings.owner) {
- await files.index(settings.owner,uploadId)
- }
-
- // get buffer
- if (fBuffer.byteLength >= (this.config.maxDiscordFileSize*this.config.maxDiscordFiles)) {
- reject({status:400,message:"file too large"});
- return
- }
-
- // generate buffers to upload
- let toUpload = []
- for (let i = 0; i < Math.ceil(fBuffer.byteLength/this.config.maxDiscordFileSize); i++) {
- toUpload.push(
- fBuffer.subarray(
- i*this.config.maxDiscordFileSize,
- Math.min(
- fBuffer.byteLength,
- (i+1)*this.config.maxDiscordFileSize
- )
+ // generate buffers to upload
+ let toUpload = []
+ for (
+ let i = 0;
+ i < Math.ceil(buffer.byteLength / this.config.maxDiscordFileSize);
+ i++
+ ) {
+ toUpload.push(
+ buffer.subarray(
+ i * this.config.maxDiscordFileSize,
+ Math.min(
+ buffer.byteLength,
+ (i + 1) * this.config.maxDiscordFileSize
)
)
+ )
+ }
+
+ // begin uploading
+ let uploadTmplt: Discord.AttachmentBuilder[] = toUpload.map((e) => {
+ return new Discord.AttachmentBuilder(e).setName(
+ Math.random().toString().slice(2)
+ )
+ })
+ let uploadGroups = []
+
+ for (let i = 0; i < Math.ceil(uploadTmplt.length / 10); i++) {
+ uploadGroups.push(uploadTmplt.slice(i * 10, (i + 1) * 10))
+ }
+
+ let msgIds = []
+
+ for (const uploadGroup of uploadGroups) {
+ let message = await this.uploadChannel
+ .send({
+ files: uploadGroup,
+ })
+ .catch((e) => {
+ console.error(e)
+ })
+
+ if (message && message instanceof Message) {
+ msgIds.push(message.id)
+ } else {
+ if (!existingFile) delete this.files[uploadId]
+ else this.files[uploadId] = existingFile
+ throw { status: 500, message: "please try again" }
}
-
- // begin uploading
- let uploadTmplt:Discord.AttachmentBuilder[] = toUpload.map((e) => {
- return new Discord.AttachmentBuilder(e)
- .setName(Math.random().toString().slice(2))
- })
- let uploadGroups = []
- for (let i = 0; i < Math.ceil(uploadTmplt.length/10); i++) {
- uploadGroups.push(uploadTmplt.slice(i*10,((i+1)*10)))
+ }
+
+ // this code deletes the files from discord, btw
+ // if need be, replace with job queue system
+
+ if (existingFile && this.uploadChannel) {
+ for (let x of existingFile.messageids) {
+ this.uploadChannel.messages
+ .delete(x)
+ .catch((err) => console.error(err))
}
-
- let msgIds = []
-
- for (let i = 0; i < uploadGroups.length; i++) {
+ }
- let ms = await this.uploadChannel.send({
- files:uploadGroups[i]
- }).catch((e) => {console.error(e)})
+ const { filename, mime, owner } = metadata
+ return this.writeFile(uploadId, {
+ filename,
+ messageids: msgIds,
+ mime,
+ owner,
+ sizeInBytes: buffer.byteLength,
- if (ms) {
- msgIds.push(ms.id)
- } else {
- if (!ogf) delete this.files[uploadId]
- else this.files[uploadId] = ogf
- reject({status:500,message:"please try again"}); return
- }
- }
+ visibility: existingFile
+ ? existingFile.visibility
+ : metadata.owner
+ ? Accounts.getFromId(metadata.owner)?.defaultFileVisibility
+ : undefined,
+ // so that json.stringify doesnt include tag:undefined
+ ...((existingFile || {}).tag ? { tag: existingFile.tag } : {}),
- // this code deletes the files from discord, btw
- // if need be, replace with job queue system
-
- if (ogf&&this.uploadChannel) {
- for (let x of ogf.messageids) {
- this.uploadChannel.messages.delete(x).catch(err => console.error(err))
- }
- }
-
- resolve(await this.writeFile(
- uploadId,
- {
- filename:settings.name,
- messageids:msgIds,
- mime:settings.mime,
- sizeInBytes:fBuffer.byteLength,
-
- owner:settings.owner,
- visibility: ogf ? ogf.visibility
- : (
- settings.owner
- ? Accounts.getFromId(settings.owner)?.defaultFileVisibility
- : undefined
- ),
- // so that json.stringify doesnt include tag:undefined
- ...((ogf||{}).tag ? {tag:ogf.tag} : {}),
-
- chunkSize: this.config.maxDiscordFileSize
- }
- ))
-
-
+ chunkSize: this.config.maxDiscordFileSize,
})
}
-
+
// fs
/**
@@ -264,24 +255,26 @@ export default class Files {
* @param file FilePointer representing the new file
* @returns Promise which resolves to the file's ID
*/
- writeFile(uploadId: string, file: FilePointer):Promise {
- return new Promise((resolve, reject) => {
+ async writeFile(uploadId: string, file: FilePointer): Promise {
+ this.files[uploadId] = file
- this.files[uploadId] = file
-
- writeFile(process.cwd()+"/.data/files.json",JSON.stringify(this.files),(err) => {
-
- if (err) {
- reject({status:500,message:"server may be misconfigured, contact admin for help"});
- delete this.files[uploadId];
- return
+ return writeFile(
+ process.cwd() + "/.data/files.json",
+ JSON.stringify(
+ this.files,
+ null,
+ process.env.NODE_ENV === "development" ? 4 : undefined
+ )
+ )
+ .then(() => uploadId)
+ .catch(() => {
+ delete this.files[uploadId]
+ throw {
+ status: 500,
+ message:
+ "server may be misconfigured, contact admin for help",
}
-
- resolve(uploadId)
-
})
-
- })
}
/**
@@ -290,139 +283,183 @@ export default class Files {
* @param range Byte range to get
* @returns A `Readable` containing the file's contents
*/
- readFileStream(uploadId: string, range?: {start:number, end:number}):Promise {
- return new Promise(async (resolve,reject) => {
- if (!this.uploadChannel) {
- reject({status:503,message:"server is not ready - please try again later"})
- return
+ async readFileStream(
+ uploadId: string,
+ range?: { start: number; end: number }
+ ): Promise {
+ if (!this.uploadChannel) {
+ throw {
+ status: 503,
+ message: "server is not ready - please try again later",
+ }
+ }
+
+ if (this.files[uploadId]) {
+ let file = this.files[uploadId]
+
+ let scan_msg_begin = 0,
+ scan_msg_end = file.messageids.length - 1,
+ scan_files_begin = 0,
+ scan_files_end = -1
+
+ let useRanges = range && file.chunkSize && file.sizeInBytes
+
+ // todo: figure out how to get typesccript to accept useRanges
+ // i'm too tired to look it up or write whatever it wnats me to do
+ if (range && file.chunkSize && file.sizeInBytes) {
+ // Calculate where to start file scans...
+
+ scan_files_begin = Math.floor(range.start / file.chunkSize)
+ scan_files_end = Math.ceil(range.end / file.chunkSize) - 1
+
+ scan_msg_begin = Math.floor(scan_files_begin / 10)
+ scan_msg_end = Math.ceil(scan_files_end / 10)
}
- if (this.files[uploadId]) {
- let file = this.files[uploadId]
+ let attachments: Discord.Attachment[] = []
- let
- scan_msg_begin = 0,
- scan_msg_end = file.messageids.length-1,
- scan_files_begin = 0,
- scan_files_end = -1
+ /* File updates */
+ let file_updates: Pick =
+ {}
+ let atSIB: number[] = [] // kepes track of the size of each file...
- let useRanges = range && file.chunkSize && file.sizeInBytes;
-
- // todo: figure out how to get typesccript to accept useRanges
- // i'm too tired to look it up or write whatever it wnats me to do
- if (range && file.chunkSize && file.sizeInBytes) {
-
- // Calculate where to start file scans...
-
- scan_files_begin = Math.floor(range.start / file.chunkSize)
- scan_files_end = Math.ceil(range.end / file.chunkSize) - 1
-
- scan_msg_begin = Math.floor(scan_files_begin / 10)
- scan_msg_end = Math.ceil(scan_files_end / 10)
-
- }
-
- let attachments: Discord.Attachment[] = [];
-
- /* File updates */
- let file_updates: Pick = {}
- let atSIB: number[] = [] // kepes track of the size of each file...
-
- for (let xi = scan_msg_begin; xi < scan_msg_end+1; xi++) {
-
- let msg = await this.uploadChannel.messages.fetch(file.messageids[xi]).catch(() => {return null})
- if (msg?.attachments) {
-
- let attach = Array.from(msg.attachments.values())
- for (let i = (useRanges && xi == scan_msg_begin ? ( scan_files_begin - (xi*10) ) : 0); i < (useRanges && xi == scan_msg_end ? ( scan_files_end - (xi*10) + 1 ) : attach.length); i++) {
-
- attachments.push(attach[i])
- atSIB.push(attach[i].size)
-
- }
-
- }
-
- }
-
- if (!file.sizeInBytes) file_updates.sizeInBytes = atSIB.reduce((a,b) => a+b, 0);
- if (!file.chunkSize) file_updates.chunkSize = atSIB[0]
- if (Object.keys(file_updates).length) { // if file_updates not empty
- // i gotta do these weird workarounds, ts is weird sometimes
- // originally i was gonna do key is keyof FilePointer but for some reason
- // it ended up making typeof file[key] never??? so
- // its 10pm and chinese people suck at being quiet so i just wanna get this over with
- // chinese is the worst language in terms of volume lmao
- let valid_fp_keys = ["sizeInBytes", "chunkSize"]
- let isValidFilePointerKey = (key: string): key is "sizeInBytes" | "chunkSize" => valid_fp_keys.includes(key)
-
- for (let [key,value] of Object.entries(file_updates)) {
- if (isValidFilePointerKey(key)) file[key] = value
- }
-
- writeFile(process.cwd()+"/.data/files.json",JSON.stringify(this.files),(err) => {})
- }
-
- let position = 0;
-
- let getNextChunk = async () => {
- let scanning_chunk = attachments[position]
- if (!scanning_chunk) {
+ for (let xi = scan_msg_begin; xi < scan_msg_end + 1; xi++) {
+ let msg = await this.uploadChannel.messages
+ .fetch(file.messageids[xi])
+ .catch(() => {
return null
- }
-
- let d = await axios.get(
- scanning_chunk.url,
- {
- responseType:"arraybuffer",
- headers: {
- ...(useRanges ? {
- "Range": `bytes=${position == 0 && range && file.chunkSize ? range.start-(scan_files_begin*file.chunkSize) : "0"}-${position == attachments.length-1 && range && file.chunkSize ? range.end-(scan_files_end*file.chunkSize) : ""}`
- } : {})
- }
- }
- ).catch((e:Error) => {console.error(e)})
-
- position++;
-
- if (d) {
- return d.data
- } else {
- reject({status:500,message:"internal server error"})
- return "__ERR"
+ })
+ if (msg?.attachments) {
+ let attach = Array.from(msg.attachments.values())
+ for (
+ let i =
+ useRanges && xi == scan_msg_begin
+ ? scan_files_begin - xi * 10
+ : 0;
+ i <
+ (useRanges && xi == scan_msg_end
+ ? scan_files_end - xi * 10 + 1
+ : attach.length);
+ i++
+ ) {
+ attachments.push(attach[i])
+ atSIB.push(attach[i].size)
}
}
-
- let ord:number[] = []
- // hopefully this regulates it?
- let lastChunkSent = true
-
- let dataStream = new Readable({
- read(){
- if (!lastChunkSent) return
- lastChunkSent = false
- getNextChunk().then(async (nextChunk) => {
- if (nextChunk == "__ERR") {this.destroy(new Error("file read error")); return}
- let response = this.push(nextChunk)
-
- if (!nextChunk) return // EOF
-
- while (response) {
- let nextChunk = await getNextChunk()
- response = this.push(nextChunk)
- if (!nextChunk) return
- }
- lastChunkSent = true
- })
- }
- })
-
- resolve(dataStream)
-
- } else {
- reject({status:404,message:"not found"})
}
- })
+
+ if (!file.sizeInBytes)
+ file_updates.sizeInBytes = atSIB.reduce((a, b) => a + b, 0)
+ if (!file.chunkSize) file_updates.chunkSize = atSIB[0]
+ if (Object.keys(file_updates).length) {
+ // if file_updates not empty
+ // i gotta do these weird workarounds, ts is weird sometimes
+ // originally i was gonna do key is keyof FilePointer but for some reason
+ // it ended up making typeof file[key] never??? so
+ // its 10pm and chinese people suck at being quiet so i just wanna get this over with
+ // chinese is the worst language in terms of volume lmao
+ let valid_fp_keys = ["sizeInBytes", "chunkSize"]
+ let isValidFilePointerKey = (
+ key: string
+ ): key is "sizeInBytes" | "chunkSize" =>
+ valid_fp_keys.includes(key)
+
+ for (let [key, value] of Object.entries(file_updates)) {
+ if (isValidFilePointerKey(key)) file[key] = value
+ }
+
+ // The original was a callback so I don't think I'm supposed to `await` this -Jack
+ writeFile(
+ process.cwd() + "/.data/files.json",
+ JSON.stringify(
+ this.files,
+ null,
+ process.env.NODE_ENV === "development" ? 4 : undefined
+ )
+ )
+ }
+
+ let position = 0
+
+ let getNextChunk = async () => {
+ let scanning_chunk = attachments[position]
+ if (!scanning_chunk) {
+ return null
+ }
+
+ let d = await axios
+ .get(scanning_chunk.url, {
+ responseType: "arraybuffer",
+ headers: {
+ ...(useRanges
+ ? {
+ Range: `bytes=${
+ position == 0 &&
+ range &&
+ file.chunkSize
+ ? range.start -
+ scan_files_begin *
+ file.chunkSize
+ : "0"
+ }-${
+ position == attachments.length - 1 &&
+ range &&
+ file.chunkSize
+ ? range.end -
+ scan_files_end * file.chunkSize
+ : ""
+ }`,
+ }
+ : {}),
+ },
+ })
+ .catch((e: Error) => {
+ console.error(e)
+ })
+
+ position++
+
+ if (d) {
+ return d.data
+ } else {
+ throw {
+ status: 500,
+ message: "internal server error",
+ }
+ }
+ }
+
+ let ord: number[] = []
+ // hopefully this regulates it?
+ let lastChunkSent = true
+
+ let dataStream = new Readable({
+ read() {
+ if (!lastChunkSent) return
+ lastChunkSent = false
+ getNextChunk().then(async (nextChunk) => {
+ if (nextChunk == "__ERR") {
+ this.destroy(new Error("file read error"))
+ return
+ }
+ let response = this.push(nextChunk)
+
+ if (!nextChunk) return // EOF
+
+ while (response) {
+ let nextChunk = await getNextChunk()
+ response = this.push(nextChunk)
+ if (!nextChunk) return
+ }
+ lastChunkSent = true
+ })
+ },
+ })
+
+ return dataStream
+ } else {
+ throw { status: 404, message: "not found" }
+ }
}
/**
@@ -430,33 +467,41 @@ export default class Files {
* @param uploadId Target file's ID
* @param noWrite Whether or not the change should be written to disk. Enable for bulk deletes
*/
- unlink(uploadId:string, noWrite: boolean = false):Promise {
- return new Promise(async (resolve,reject) => {
- let tmp = this.files[uploadId];
- if (!tmp) {resolve(); return}
- if (tmp.owner) {
- let id = files.deindex(tmp.owner,uploadId,noWrite);
- if (id) await id
- }
- // this code deletes the files from discord, btw
- // if need be, replace with job queue system
+ async unlink(uploadId: string, noWrite: boolean = false): Promise {
+ let tmp = this.files[uploadId]
+ if (!tmp) {
+ return
+ }
+ if (tmp.owner) {
+ let id = files.deindex(tmp.owner, uploadId, noWrite)
+ if (id) await id
+ }
+ // this code deletes the files from discord, btw
+ // if need be, replace with job queue system
- if (!this.uploadChannel) {reject(); return}
- for (let x of tmp.messageids) {
- this.uploadChannel.messages.delete(x).catch(err => console.error(err))
- }
-
- delete this.files[uploadId];
- if (noWrite) {resolve(); return}
- writeFile(process.cwd()+"/.data/files.json",JSON.stringify(this.files),(err) => {
- if (err) {
- this.files[uploadId] = tmp // !! this may not work, since tmp is a link to this.files[uploadId]?
- reject()
- } else {
- resolve()
- }
- })
+ if (!this.uploadChannel) {
+ return
+ }
+ for (let x of tmp.messageids) {
+ this.uploadChannel.messages
+ .delete(x)
+ .catch((err) => console.error(err))
+ }
+ delete this.files[uploadId]
+ if (noWrite) {
+ return
+ }
+ return writeFile(
+ process.cwd() + "/.data/files.json",
+ JSON.stringify(
+ this.files,
+ null,
+ process.env.NODE_ENV === "development" ? 4 : undefined
+ )
+ ).catch((err) => {
+ this.files[uploadId] = tmp // !! this may not work, since tmp is a link to this.files[uploadId]?
+ throw err
})
}
@@ -465,8 +510,7 @@ export default class Files {
* @param uploadId Target file's ID
* @returns FilePointer for the file
*/
- getFilePointer(uploadId:string):FilePointer {
+ getFilePointer(uploadId: string): FilePointer {
return this.files[uploadId]
}
-
}
diff --git a/src/server/routes/api/v0/primaryApi.ts b/src/server/routes/api/v0/primaryApi.ts
index afdcec8..30b98a6 100644
--- a/src/server/routes/api/v0/primaryApi.ts
+++ b/src/server/routes/api/v0/primaryApi.ts
@@ -1,203 +1,238 @@
-import bodyParser from "body-parser";
-import express, { Router } from "express";
-import * as Accounts from "../../../lib/accounts";
-import * as auth from "../../../lib/auth";
+import bodyParser from "body-parser"
+import express, { Router } from "express"
+import * as Accounts from "../../../lib/accounts"
+import * as auth from "../../../lib/auth"
import axios, { AxiosResponse } from "axios"
-import { type Range } from "range-parser";
-import multer, {memoryStorage} from "multer"
+import { type Range } from "range-parser"
+import multer, { memoryStorage } from "multer"
-import ServeError from "../../../lib/errors";
-import Files from "../../../lib/files";
-import { getAccount, requiresPermissions } from "../../../lib/middleware";
+import ServeError from "../../../lib/errors"
+import Files from "../../../lib/files"
+import { getAccount, requiresPermissions } from "../../../lib/middleware"
let parser = bodyParser.json({
- type: ["text/plain","application/json"]
+ type: ["text/plain", "application/json"],
})
-export let primaryApi = Router();
+export let primaryApi = Router()
-const multerSetup = multer({storage:memoryStorage()})
+const multerSetup = multer({ storage: memoryStorage() })
let config = require(`${process.cwd()}/config.json`)
-primaryApi.use(getAccount);
+primaryApi.use(getAccount)
-module.exports = function(files: Files) {
+module.exports = function (files: Files) {
+ primaryApi.get(
+ ["/file/:fileId", "/cpt/:fileId/*", "/:fileId"],
+ async (req: express.Request, res: express.Response) => {
+ let acc = res.locals.acc as Accounts.Account
- primaryApi.get(["/file/:fileId", "/cpt/:fileId/*", "/:fileId"], async (req:express.Request,res:express.Response) => {
-
- let acc = res.locals.acc as Accounts.Account
+ let file = files.getFilePointer(req.params.fileId)
+ res.setHeader("Access-Control-Allow-Origin", "*")
+ res.setHeader("Content-Security-Policy", "sandbox allow-scripts")
+ if (req.query.attachment == "1")
+ res.setHeader("Content-Disposition", "attachment")
- let file = files.getFilePointer(req.params.fileId)
- res.setHeader("Access-Control-Allow-Origin", "*")
- res.setHeader("Content-Security-Policy","sandbox allow-scripts")
- if (req.query.attachment == "1") res.setHeader("Content-Disposition", "attachment")
-
- if (file) {
-
- if (file.visibility == "private") {
- if (acc?.id != file.owner) {
- ServeError(res,403,"you do not own this file")
- return
- }
+ if (file) {
+ if (file.visibility == "private") {
+ if (acc?.id != file.owner) {
+ ServeError(res, 403, "you do not own this file")
+ return
+ }
- if (auth.getType(auth.tokenFor(req)) == "App" && auth.getPermissions(auth.tokenFor(req))?.includes("private")) {
- ServeError(res,403,"insufficient permissions")
- return
- }
- }
-
- let range: Range | undefined
-
- res.setHeader("Content-Type",file.mime)
- if (file.sizeInBytes) {
- res.setHeader("Content-Length",file.sizeInBytes)
-
- if (file.chunkSize) {
- let rng = req.range(file.sizeInBytes)
- if (rng) {
-
- // error handling
- if (typeof rng == "number") {
- res.status(rng == -1 ? 416 : 400).send()
- return
- }
- if (rng.type != "bytes") {
- res.status(400).send();
- return
- }
-
- // set ranges var
- let rngs = Array.from(rng)
- if (rngs.length != 1) { res.status(400).send(); return }
- range = rngs[0]
-
+ if (
+ auth.getType(auth.tokenFor(req)) == "App" &&
+ auth
+ .getPermissions(auth.tokenFor(req))
+ ?.includes("private")
+ ) {
+ ServeError(res, 403, "insufficient permissions")
+ return
}
}
- }
- // supports ranges
-
+ let range: Range | undefined
- files.readFileStream(req.params.fileId, range).then(async stream => {
+ res.setHeader("Content-Type", file.mime)
+ if (file.sizeInBytes) {
+ res.setHeader("Content-Length", file.sizeInBytes)
- if (range) {
- res.status(206)
- res.header("Content-Length", (range.end-range.start + 1).toString())
- res.header("Content-Range", `bytes ${range.start}-${range.end}/${file.sizeInBytes}`)
+ if (file.chunkSize) {
+ let rng = req.range(file.sizeInBytes)
+ if (rng) {
+ // error handling
+ if (typeof rng == "number") {
+ res.status(rng == -1 ? 416 : 400).send()
+ return
+ }
+ if (rng.type != "bytes") {
+ res.status(400).send()
+ return
+ }
+
+ // set ranges var
+ let rngs = Array.from(rng)
+ if (rngs.length != 1) {
+ res.status(400).send()
+ return
+ }
+ range = rngs[0]
+ }
+ }
}
- stream.pipe(res)
-
- }).catch((err) => {
- ServeError(res,err.status,err.message)
- })
- } else {
- ServeError(res, 404, "file not found")
- }
-
- })
+ // supports ranges
- primaryApi.head(["/file/:fileId", "/cpt/:fileId/*", "/:fileId"], (req: express.Request, res:express.Response) => {
- let file = files.getFilePointer(req.params.fileId)
-
- if (
- file.visibility == "private"
- && (
- res.locals.acc?.id != file.owner
- || (auth.getType(auth.tokenFor(req)) == "App" && auth.getPermissions(auth.tokenFor(req))?.includes("private"))
- )
- ) {
- res.status(403).send()
- return
- }
-
- res.setHeader("Access-Control-Allow-Origin", "*")
- res.setHeader("Content-Security-Policy","sandbox allow-scripts")
-
- if (req.query.attachment == "1") res.setHeader("Content-Disposition", "attachment")
-
- if (!file) {
- res.status(404)
- res.send()
- } else {
- res.setHeader("Content-Type",file.mime)
- if (file.sizeInBytes) {
- res.setHeader("Content-Length",file.sizeInBytes)
+ files
+ .readFileStream(req.params.fileId, range)
+ .then(async (stream) => {
+ if (range) {
+ res.status(206)
+ res.header(
+ "Content-Length",
+ (range.end - range.start + 1).toString()
+ )
+ res.header(
+ "Content-Range",
+ `bytes ${range.start}-${range.end}/${file.sizeInBytes}`
+ )
+ }
+ stream.pipe(res)
+ })
+ .catch((err) => {
+ ServeError(res, err.status, err.message)
+ })
+ } else {
+ ServeError(res, 404, "file not found")
}
- if (file.chunkSize) {
- res.setHeader("Accept-Ranges", "bytes")
- }
- res.send()
}
- })
+ )
+
+ primaryApi.head(
+ ["/file/:fileId", "/cpt/:fileId/*", "/:fileId"],
+ (req: express.Request, res: express.Response) => {
+ let file = files.getFilePointer(req.params.fileId)
+
+ if (
+ file.visibility == "private" &&
+ (res.locals.acc?.id != file.owner ||
+ (auth.getType(auth.tokenFor(req)) == "App" &&
+ auth
+ .getPermissions(auth.tokenFor(req))
+ ?.includes("private")))
+ ) {
+ res.status(403).send()
+ return
+ }
+
+ res.setHeader("Access-Control-Allow-Origin", "*")
+ res.setHeader("Content-Security-Policy", "sandbox allow-scripts")
+
+ if (req.query.attachment == "1")
+ res.setHeader("Content-Disposition", "attachment")
+
+ if (!file) {
+ res.status(404)
+ res.send()
+ } else {
+ res.setHeader("Content-Type", file.mime)
+ if (file.sizeInBytes) {
+ res.setHeader("Content-Length", file.sizeInBytes)
+ }
+ if (file.chunkSize) {
+ res.setHeader("Accept-Ranges", "bytes")
+ }
+ res.send()
+ }
+ }
+ )
// upload handlers
- primaryApi.post("/upload", requiresPermissions("upload"), multerSetup.single('file'), async (req,res) => {
-
- let acc = res.locals.acc as Accounts.Account
+ primaryApi.post(
+ "/upload",
+ requiresPermissions("upload"),
+ multerSetup.single("file"),
+ async (req, res) => {
+ let acc = res.locals.acc as Accounts.Account
- if (req.file) {
- try {
- let prm = req.header("monofile-params")
- let params:{[key:string]:any} = {}
- if (prm) {
- params = JSON.parse(prm)
+ if (req.file) {
+ try {
+ let prm = req.header("monofile-params")
+ let params: { [key: string]: any } = {}
+ if (prm) {
+ params = JSON.parse(prm)
+ }
+
+ files
+ .uploadFile(
+ {
+ owner: acc?.id,
+
+ uploadId: params.uploadId,
+ filename: req.file.originalname,
+ mime: req.file.mimetype,
+ },
+ req.file.buffer
+ )
+ .then((uID) => res.send(uID))
+ .catch((stat) => {
+ res.status(stat.status)
+ res.send(`[err] ${stat.message}`)
+ })
+ } catch {
+ res.status(400)
+ res.send("[err] bad request")
}
-
- files.uploadFile({
- owner: acc?.id,
-
- uploadId:params.uploadId,
- name:req.file.originalname,
- mime:req.file.mimetype
- },req.file.buffer)
- .then((uID) => res.send(uID))
- .catch((stat) => {
- res.status(stat.status);
- res.send(`[err] ${stat.message}`)
- })
- } catch {
+ } else {
res.status(400)
res.send("[err] bad request")
}
- } else {
- res.status(400)
- res.send("[err] bad request")
}
- })
+ )
- primaryApi.post("/clone", requiresPermissions("upload"), bodyParser.json({type: ["text/plain","application/json"]}) ,(req,res) => {
-
- let acc = res.locals.acc as Accounts.Account
+ primaryApi.post(
+ "/clone",
+ requiresPermissions("upload"),
+ bodyParser.json({ type: ["text/plain", "application/json"] }),
+ (req, res) => {
+ let acc = res.locals.acc as Accounts.Account
- try {
- axios.get(req.body.url,{responseType:"arraybuffer"}).then((data:AxiosResponse) => {
-
- files.uploadFile({
- owner: acc?.id,
-
- name:req.body.url.split("/")[req.body.url.split("/").length-1] || "generic",
- mime:data.headers["content-type"],
- uploadId:req.body.uploadId
- },Buffer.from(data.data))
- .then((uID) => res.send(uID))
- .catch((stat) => {
- res.status(stat.status);
- res.send(`[err] ${stat.message}`)
+ try {
+ axios
+ .get(req.body.url, { responseType: "arraybuffer" })
+ .then((data: AxiosResponse) => {
+ files
+ .uploadFile(
+ {
+ owner: acc?.id,
+ filename:
+ req.body.url.split("/")[
+ req.body.url.split("/").length - 1
+ ] || "generic",
+ mime: data.headers["content-type"],
+ uploadId: req.body.uploadId,
+ },
+ Buffer.from(data.data)
+ )
+ .then((uID) => res.send(uID))
+ .catch((stat) => {
+ res.status(stat.status)
+ res.send(`[err] ${stat.message}`)
+ })
})
-
- }).catch((err) => {
- console.log(err)
- res.status(400)
- res.send(`[err] failed to fetch data`)
- })
- } catch {
- res.status(500)
- res.send("[err] an error occured")
+ .catch((err) => {
+ console.log(err)
+ res.status(400)
+ res.send(`[err] failed to fetch data`)
+ })
+ } catch {
+ res.status(500)
+ res.send("[err] an error occured")
+ }
}
- })
+ )
return primaryApi
-}
\ No newline at end of file
+}
From b135dc79eae98d4238d5676b59a32a37d4f2f1c4 Mon Sep 17 00:00:00 2001
From: "Jack W."
Date: Tue, 24 Oct 2023 16:33:15 -0400
Subject: [PATCH 027/173] refactor: :recycle: Use .replaceAll instead of /g
---
src/server/index.ts | 189 +++++++++++++++++++++++----------------
src/server/lib/errors.ts | 6 +-
src/server/lib/mail.ts | 41 +++++----
tsconfig.json | 2 +-
4 files changed, 137 insertions(+), 101 deletions(-)
diff --git a/src/server/index.ts b/src/server/index.ts
index 6baf6c6..f023eaa 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -1,14 +1,14 @@
-import cookieParser from "cookie-parser";
+import cookieParser from "cookie-parser"
import { IntentsBitField, Client } from "discord.js"
import express from "express"
import fs from "fs"
-import bytes from "bytes";
+import bytes from "bytes"
import ServeError from "./lib/errors"
import Files from "./lib/files"
import * as auth from "./lib/auth"
import * as Accounts from "./lib/accounts"
-import { getAccount } from "./lib/middleware";
+import { getAccount } from "./lib/middleware"
import APIRouter from "./routes/api"
@@ -18,9 +18,9 @@ let pkg = require(`${process.cwd()}/package.json`)
let app = express()
let config = require(`${process.cwd()}/config.json`)
-app.use("/static/assets",express.static("assets"))
-app.use("/static/style",express.static("out/style"))
-app.use("/static/js",express.static("out/client"))
+app.use("/static/assets", express.static("assets"))
+app.use("/static/style", express.static("out/style"))
+app.use("/static/js", express.static("out/client"))
//app.use(bodyParser.text({limit:(config.maxDiscordFileSize*config.maxDiscordFiles)+1048576,type:["application/json","text/plain"]}))
@@ -29,34 +29,41 @@ app.use(cookieParser())
// check for ssl, if not redirect
if (config.trustProxy) app.enable("trust proxy")
if (config.forceSSL) {
- app.use((req,res,next) => {
- if (req.protocol == "http") res.redirect(`https://${req.get("host")}${req.originalUrl}`)
+ app.use((req, res, next) => {
+ if (req.protocol == "http")
+ res.redirect(`https://${req.get("host")}${req.originalUrl}`)
else next()
})
}
-app.get("/server",(req,res) => {
- res.send(JSON.stringify({
- ...config,
- version:pkg.version,
- files:Object.keys(files.files).length
- }))
+app.get("/server", (req, res) => {
+ res.send(
+ JSON.stringify({
+ ...config,
+ version: pkg.version,
+ files: Object.keys(files.files).length,
+ })
+ )
})
// funcs
// init data
-if (!fs.existsSync(__dirname+"/../.data/")) fs.mkdirSync(__dirname+"/../.data/")
+if (!fs.existsSync(__dirname + "/../.data/"))
+ fs.mkdirSync(__dirname + "/../.data/")
// discord
-let client = new Client({intents:[
- IntentsBitField.Flags.GuildMessages,
- IntentsBitField.Flags.MessageContent
-],rest:{timeout:config.requestTimeout}})
+let client = new Client({
+ intents: [
+ IntentsBitField.Flags.GuildMessages,
+ IntentsBitField.Flags.MessageContent,
+ ],
+ rest: { timeout: config.requestTimeout },
+})
-let files = new Files(client,config)
+let files = new Files(client, config)
let apiRouter = new APIRouter(files)
apiRouter.loadAPIMethods().then(() => {
@@ -66,88 +73,118 @@ apiRouter.loadAPIMethods().then(() => {
// index, clone
-app.get("/", function(req,res) {
- res.sendFile(process.cwd()+"/pages/index.html")
+app.get("/", function (req, res) {
+ res.sendFile(process.cwd() + "/pages/index.html")
})
// serve download page
-app.get("/download/:fileId", getAccount, (req,res) => {
-
+app.get("/download/:fileId", getAccount, (req, res) => {
let acc = res.locals.acc as Accounts.Account
if (files.getFilePointer(req.params.fileId)) {
let file = files.getFilePointer(req.params.fileId)
if (file.visibility == "private" && acc?.id != file.owner) {
- ServeError(res,403,"you do not own this file")
+ ServeError(res, 403, "you do not own this file")
return
}
- fs.readFile(process.cwd()+"/pages/download.html",(err,buf) => {
- let fileOwner = file.owner ? Accounts.getFromId(file.owner) : undefined;
- if (err) {res.sendStatus(500);console.log(err);return}
+ fs.readFile(process.cwd() + "/pages/download.html", (err, buf) => {
+ let fileOwner = file.owner
+ ? Accounts.getFromId(file.owner)
+ : undefined
+ if (err) {
+ res.sendStatus(500)
+ console.log(err)
+ return
+ }
res.send(
- buf.toString()
- .replace(/\$FileId/g,req.params.fileId)
- .replace(/\$Version/g,pkg.version)
- .replace(/\$FileSize/g,file.sizeInBytes ? bytes(file.sizeInBytes) : "[File size unknown]")
- .replace(/\$FileName/g,
- file.filename
- .replace(/\&/g,"&")
- .replace(/\/g,">")
- )
- .replace(/\<\!\-\-metaTags\-\-\>/g,
- (
- file.mime.startsWith("image/")
- ? ``
- : (
- file.mime.startsWith("video/")
- ? (
- `
-
+ buf
+ .toString()
+ .replaceAll("$FileId", req.params.fileId)
+ .replaceAll("$Version", pkg.version)
+ .replaceAll(
+ "$FileSize",
+ file.sizeInBytes
+ ? bytes(file.sizeInBytes)
+ : "[File size unknown]"
+ )
+ .replaceAll(
+ "$FileName",
+ file.filename
+ .replaceAll("&", "&")
+ .replaceAll("<", "<")
+ .replaceAll(">", ">")
+ )
+ .replace(
+ "",
+ (file.mime.startsWith("image/")
+ ? ``
+ : file.mime.startsWith("video/")
+ ? `
+
- `
- // quick lazy fix as a fallback
- // maybe i'll improve this later, but probably not.
- + ((file.sizeInBytes||0) >= 26214400 ? `
+ ` +
+ // quick lazy fix as a fallback
+ // maybe i'll improve this later, but probably not.
+ ((file.sizeInBytes || 0) >= 26214400
+ ? `
- ` : "")
- )
- : ""
- )
+ `
+ : "")
+ : "") +
+ (fileOwner?.embed?.largeImage &&
+ file.visibility != "anonymous" &&
+ file.mime.startsWith("image/")
+ ? ``
+ : "") +
+ `\n`
)
- + (
- fileOwner?.embed?.largeImage && file.visibility!="anonymous" && file.mime.startsWith("image/")
- ? ``
- : ""
- )
- + `\n`
- )
- .replace(/\<\!\-\-preview\-\-\>/g,
- file.mime.startsWith("image/")
- ? ``
- : (
- file.mime.startsWith("video/")
- ? ``
- : (
- file.mime.startsWith("audio/")
+ .replace(
+ "",
+ file.mime.startsWith("image/")
+ ? ``
+ : file.mime.startsWith("video/")
+ ? ``
+ : file.mime.startsWith("audio/")
? ``
: ""
- )
)
- )
- .replace(/\$Uploader/g,!file.owner||file.visibility=="anonymous" ? "Anonymous" : `@${fileOwner?.username || "Deleted User"}`)
+ .replaceAll(
+ "$Uploader",
+ !file.owner || file.visibility == "anonymous"
+ ? "Anonymous"
+ : `@${fileOwner?.username || "Deleted User"}`
+ )
)
})
} else {
- ServeError(res,404,"file not found")
+ ServeError(res, 404, "file not found")
}
})
-
/*
routes should be in this order:
@@ -159,7 +196,7 @@ app.get("/download/:fileId", getAccount, (req,res) => {
// listen on 3000 or MONOFILE_PORT
-app.listen(process.env.MONOFILE_PORT || 3000,function() {
+app.listen(process.env.MONOFILE_PORT || 3000, function () {
console.log("Web OK!")
})
diff --git a/src/server/lib/errors.ts b/src/server/lib/errors.ts
index 84c1cc5..cb53535 100644
--- a/src/server/lib/errors.ts
+++ b/src/server/lib/errors.ts
@@ -31,8 +31,8 @@ export default async function ServeError(
res.header("x-backup-status-message", reason) // glitch default nginx configuration
res.send(
errorPage
- .replace(/\$code/g,code.toString())
- .replace(/\$text/g,reason)
+ .replaceAll("$code",code.toString())
+ .replaceAll("$text",reason)
)
}
/**
@@ -45,4 +45,4 @@ export function Redirect(res:Response,url:string) {
res.status(302)
res.header("Location",url)
res.send()
-}
\ No newline at end of file
+}
diff --git a/src/server/lib/mail.ts b/src/server/lib/mail.ts
index 3ee1b38..7d7bf29 100644
--- a/src/server/lib/mail.ts
+++ b/src/server/lib/mail.ts
@@ -1,21 +1,16 @@
-import { createTransport } from "nodemailer";
+import { createTransport } from "nodemailer"
// required i guess
require("dotenv").config()
-let
-mailConfig =
- require( process.cwd() + "/config.json" ).mail,
-transport =
- createTransport(
- {
- ...mailConfig.transport,
- auth: {
- user: process.env.MAIL_USER,
- pass: process.env.MAIL_PASS
- }
- }
- )
+let mailConfig = require(process.cwd() + "/config.json").mail,
+ transport = createTransport({
+ ...mailConfig.transport,
+ auth: {
+ user: process.env.MAIL_USER,
+ pass: process.env.MAIL_PASS,
+ },
+ })
// lazy but
@@ -30,11 +25,15 @@ export function sendMail(to: string, subject: string, content: string) {
return transport.sendMail({
to,
subject,
- "from": mailConfig.send.from,
- "html": `monofile accounts Gain control of your uploads. ${
- content
- .replace(/\/g, `@`)
- .replace(/\/g,``)
- }
If you do not believe that you are the intended recipient of this email, please disregard this message.`
+ from: mailConfig.send.from,
+ html: `monofile accounts Gain control of your uploads. ${content
+ .replaceAll(
+ "",
+ `@`
+ )
+ .replaceAll(
+ "",
+ ``
+ )}