api-v1: apihandler

This commit is contained in:
May 2023-10-03 20:10:08 -07:00
parent 70591c78e9
commit 1ed1acca1c
7 changed files with 1074 additions and 0 deletions

78
src/server/routes/api.ts Normal file
View file

@ -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)
}
}
}

View file

@ -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`, `<b>Hello there!</b> This email is to notify you of a password change that an administrator, <span username>${acc.username}</span>, 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, <span username>${targetAccount.username}</span>, has been deleted by <span username>${acc.username}</span> for the following reason: <br><br><span style="font-weight:600">${req.body.reason || "(no reason specified)"}</span><br><br> 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
}

View file

@ -0,0 +1,10 @@
{
"name": "v0",
"baseURL": "/",
"mount": [
{ "file": "primaryApi", "to": "/" },
{ "file": "adminRoutes", "to": "/admin" },
{ "file": "authRoutes", "to": "/auth" },
{ "file": "fileApiRoutes", "to": "/files" }
]
}

View file

@ -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`, `<b>Hello there!</b> Your username has been updated to <span username>${req.body.username}</span>. 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<string, {code: string, email: string, expiry: NodeJS.Timeout}>()
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`, `<b>Hello there!</b> You are recieving this message because you decided to link your email, <span code>${req.body.email.split("@")[0]}<span style="opacity:0.5">@${req.body.email.split("@")[1]}</span></span>, to your account, <span username>${acc.username}</span>. If you would like to continue, please <a href="https://${req.header("Host")}/auth/confirm_email/${code}"><span code>click here</span></a>, 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<string, {code: string, expiry: NodeJS.Timeout, requestedAt:number}>()
let prcIdx = new Map<string, string>()
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}`, `<b>Hello there!</b> You are recieving this message because you forgot your password to your monofile account, <span username>${acc.username}</span>. To log in, please <a href="https://${req.header("Host")}/auth/emergency_login/${code}"><span code>click here</span></a>, 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`, `<b>Hello there!</b> 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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -0,0 +1,7 @@
{
"name": "v1",
"baseURL": "/api/v1",
"mount": [
"account", "admin", "file", "public"
]
}