mirror of
https://github.com/mollersuite/monofile.git
synced 2024-11-28 16:36:27 -08:00
api-v1: apihandler
This commit is contained in:
parent
70591c78e9
commit
1ed1acca1c
78
src/server/routes/api.ts
Normal file
78
src/server/routes/api.ts
Normal 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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
236
src/server/routes/api/v0/adminRoutes.ts
Normal file
236
src/server/routes/api/v0/adminRoutes.ts
Normal 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
|
||||
}
|
10
src/server/routes/api/v0/api.json
Normal file
10
src/server/routes/api/v0/api.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"name": "v0",
|
||||
"baseURL": "/",
|
||||
"mount": [
|
||||
{ "file": "primaryApi", "to": "/" },
|
||||
{ "file": "adminRoutes", "to": "/admin" },
|
||||
{ "file": "authRoutes", "to": "/auth" },
|
||||
{ "file": "fileApiRoutes", "to": "/files" }
|
||||
]
|
||||
}
|
464
src/server/routes/api/v0/authRoutes.ts
Normal file
464
src/server/routes/api/v0/authRoutes.ts
Normal 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
|
||||
}
|
98
src/server/routes/api/v0/fileApiRoutes.ts
Normal file
98
src/server/routes/api/v0/fileApiRoutes.ts
Normal 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
|
||||
}
|
181
src/server/routes/api/v0/primaryApi.ts
Normal file
181
src/server/routes/api/v0/primaryApi.ts
Normal 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
|
||||
}
|
7
src/server/routes/api/v1/api.json
Normal file
7
src/server/routes/api/v1/api.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"name": "v1",
|
||||
"baseURL": "/api/v1",
|
||||
"mount": [
|
||||
"account", "admin", "file", "public"
|
||||
]
|
||||
}
|
Loading…
Reference in a new issue