refactor: ♻️ Honofile.

This commit is contained in:
Jack W. 2023-10-24 19:59:00 -04:00
parent 6220cd8b0f
commit 0366c91f74
No known key found for this signature in database
18 changed files with 1531 additions and 1308 deletions

View file

@ -17,6 +17,7 @@
"node": ">=v16.11"
},
"dependencies": {
"@hono/node-server": "^1.2.0",
"@types/body-parser": "^1.19.2",
"@types/express": "^4.17.14",
"@types/multer": "^1.4.7",

View file

@ -5,6 +5,9 @@ settings:
excludeLinksFromLockfile: false
dependencies:
'@hono/node-server':
specifier: ^1.2.0
version: 1.2.0
'@types/body-parser':
specifier: ^1.19.2
version: 1.19.3
@ -337,6 +340,11 @@ packages:
dev: true
optional: true
/@hono/node-server@1.2.0:
resolution: {integrity: sha512-aHT8lDMLpd7ioXJ1/057+h+oE/k7rCOWmjklYDsE0jE4CoNB9XzG4f8dRHvw4s5HJFocaYDiGgYM/V0kYbQ0ww==}
engines: {node: '>=18.0.0'}
dev: false
/@jridgewell/sourcemap-codec@1.4.15:
resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==}
dev: true

View file

@ -1,45 +1,63 @@
import cookieParser from "cookie-parser"
import { IntentsBitField, Client } from "discord.js"
import express from "express"
import { serve } from "@hono/node-server"
import { serveStatic } from "@hono/node-server/serve-static"
import { Hono } from "hono"
import fs from "fs"
import Files from "./lib/files"
import { getAccount } from "./lib/middleware"
import APIRouter from "./routes/api"
import preview from "./preview"
require("dotenv").config()
const pkg = require(`${process.cwd()}/package.json`)
let app = express()
const app = new Hono()
let config = require(`${process.cwd()}/config.json`)
app.use("/static/assets", express.static("assets"))
app.use("/static/vite", express.static("dist/static/vite"))
app.get(
"/static/assets/*",
serveStatic({
rewriteRequestPath: (path) => {
return path.replace("/static/assets", "/assets")
},
})
)
app.get(
"/static/vite/*",
serveStatic({
rewriteRequestPath: (path) => {
return path.replace("/static/vite", "/dist/static/vite")
},
})
)
//app.use(bodyParser.text({limit:(config.maxDiscordFileSize*config.maxDiscordFiles)+1048576,type:["application/json","text/plain"]}))
app.use(cookieParser())
// check for ssl, if not redirect
if (config.trustProxy) app.enable("trust proxy")
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}`)
else next()
app.use(async (ctx, next) => {
if (new URL(ctx.req.url).protocol == "http") {
return ctx.redirect(
`https://${ctx.req.header("host")}${
new URL(ctx.req.url).pathname
}`
)
} else {
return next()
}
})
}
app.get("/server", (req, res) => {
res.send(
JSON.stringify({
...config,
version: pkg.version,
files: Object.keys(files.files).length,
})
)
})
app.get("/server", (ctx) =>
ctx.json({
...config,
version: pkg.version,
files: Object.keys(files.files).length,
})
)
// funcs
@ -60,17 +78,19 @@ let client = new Client({
let files = new Files(client, config)
let apiRouter = new APIRouter(files)
const apiRouter = new APIRouter(files)
apiRouter.loadAPIMethods().then(() => {
app.use(apiRouter.root)
app.route("/", apiRouter.root)
console.log("API OK!")
})
// index, clone
app.get("/", function (req, res) {
res.sendFile(process.cwd() + "/dist/index.html")
})
app.get("/", async (ctx) =>
ctx.html(
await fs.promises.readFile(process.cwd() + "/dist/index.html", "utf-8")
)
)
// serve download page
@ -87,8 +107,16 @@ app.get("/download/:fileId", getAccount, preview(files))
// listen on 3000 or MONOFILE_PORT
app.listen(process.env.MONOFILE_PORT || 3000, function () {
console.log("Web OK!")
})
serve(
{
fetch: app.fetch,
port: Number(process.env.MONOFILE_PORT || 3000),
},
(info) => {
console.log("Web OK!", info.port, info.address)
}
)
client.login(process.env.TOKEN)
export = app

View file

@ -1,48 +1,50 @@
import crypto from "crypto"
import express from "express"
import { getCookie } from "hono/cookie"
import type { Context } from "hono"
import { readFile, writeFile } from "fs/promises"
export let AuthTokens: AuthToken[] = []
export let AuthTokenTO:{[key:string]:NodeJS.Timeout} = {}
export let AuthTokenTO: { [key: string]: NodeJS.Timeout } = {}
export const ValidTokenPermissions = [
"user", // permissions to /auth/me, with email docked
"email", // adds email back to /auth/me
"private", // allows app to read private files
"upload", // allows an app to upload under an account
"manage", // allows an app to manage an account's files
"user", // permissions to /auth/me, with email docked
"email", // adds email back to /auth/me
"private", // allows app to read private files
"upload", // allows an app to upload under an account
"manage", // allows an app to manage an account's files
"customize", // allows an app to change customization settings
"admin" // only available for accounts with admin
// gives an app access to all admin tools
"admin", // only available for accounts with admin
// gives an app access to all admin tools
] as const
export type TokenType = "User" | "App"
export type TokenPermission = typeof ValidTokenPermissions[number]
export type TokenPermission = (typeof ValidTokenPermissions)[number]
export interface AuthToken {
account: string,
token: string,
expire: number,
account: string
token: string
expire: number
type?: TokenType, // if !type, assume User
type?: TokenType // if !type, assume User
tokenPermissions?: TokenPermission[] // default to user if type is App,
// give full permissions if type is User
// give full permissions if type is User
}
export function create(
id:string,
expire:number=(24*60*60*1000),
type:TokenType="User",
tokenPermissions?:TokenPermission[]
id: string,
expire: number = 24 * 60 * 60 * 1000,
type: TokenType = "User",
tokenPermissions?: TokenPermission[]
) {
let token = {
account:id,
token:crypto.randomBytes(36).toString('hex'),
expire: expire ? Date.now()+expire : 0,
account: id,
token: crypto.randomBytes(36).toString("hex"),
expire: expire ? Date.now() + expire : 0,
type,
tokenPermissions: type == "App" ? tokenPermissions || ["user"] : undefined
tokenPermissions:
type == "App" ? tokenPermissions || ["user"] : undefined,
}
AuthTokens.push(token)
tokenTimer(token)
@ -51,56 +53,68 @@ export function create(
return token.token
}
export function tokenFor(req: express.Request) {
return req.cookies.auth || (
req.header("authorization")?.startsWith("Bearer ")
? req.header("authorization")?.split(" ")[1]
: undefined
export function tokenFor(ctx: Context) {
return (
getCookie(ctx, "auth") ||
(ctx.req.header("authorization")?.startsWith("Bearer ")
? ctx.req.header("authorization")?.split(" ")[1]
: undefined)
)
}
function getToken(token:string) {
return AuthTokens.find(e => e.token == token && (e.expire == 0 || Date.now() < e.expire))
function getToken(token: string) {
return AuthTokens.find(
(e) => e.token == token && (e.expire == 0 || Date.now() < e.expire)
)
}
export function validate(token:string) {
export function validate(token: string) {
return getToken(token)?.account
}
export function getType(token:string): TokenType | undefined {
export function getType(token: string): TokenType | undefined {
return getToken(token)?.type
}
export function getPermissions(token:string): TokenPermission[] | undefined {
export function getPermissions(token: string): TokenPermission[] | undefined {
return getToken(token)?.tokenPermissions
}
export function tokenTimer(token:AuthToken) {
export function tokenTimer(token: AuthToken) {
if (!token.expire) return // justincase
if (Date.now() >= token.expire) {
invalidate(token.token)
return
}
AuthTokenTO[token.token] = setTimeout(() => invalidate(token.token),token.expire-Date.now())
AuthTokenTO[token.token] = setTimeout(
() => invalidate(token.token),
token.expire - Date.now()
)
}
export function invalidate(token:string) {
export function invalidate(token: string) {
if (AuthTokenTO[token]) {
clearTimeout(AuthTokenTO[token])
}
AuthTokens.splice(AuthTokens.findIndex(e => e.token == token),1)
AuthTokens.splice(
AuthTokens.findIndex((e) => e.token == token),
1
)
save()
}
export function save() {
writeFile(`${process.cwd()}/.data/tokens.json`,JSON.stringify(AuthTokens))
.catch((err) => console.error(err))
writeFile(
`${process.cwd()}/.data/tokens.json`,
JSON.stringify(AuthTokens)
).catch((err) => console.error(err))
}
readFile(`${process.cwd()}/.data/tokens.json`)
.then((buf) => {
AuthTokens = JSON.parse(buf.toString())
AuthTokens.forEach(e => tokenTimer(e))
}).catch(err => console.error(err))
AuthTokens.forEach((e) => tokenTimer(e))
})
.catch((err) => console.error(err))

View file

@ -1,48 +1,36 @@
import { Response } from "express";
import { readFile } from "fs/promises"
import type { Context } from "hono"
let errorPage:string
let errorPage: string
/**
* @description Serves an error as a response to a request with an error page attached
* @param res Express response object
* @param ctx Express response object
* @param code Error code
* @param reason Error reason
*/
export default async function ServeError(
res:Response,
code:number,
reason:string
ctx: Context,
code: number,
reason: string
) {
// fetch error page if not cached
if (!errorPage) {
errorPage =
(
await readFile(`${process.cwd()}/dist/error.html`)
.catch((err) => console.error(err))
|| "<pre>$code $text</pre>"
)
.toString()
errorPage = (
(await readFile(`${process.cwd()}/dist/error.html`).catch((err) =>
console.error(err)
)) || "<pre>$code $text</pre>"
).toString()
}
// serve error
res.statusMessage = reason
res.status(code)
res.header("x-backup-status-message", reason) // glitch default nginx configuration
res.send(
return ctx.html(
errorPage
.replaceAll("$code",code.toString())
.replaceAll("$text",reason)
.replaceAll("$code", code.toString())
.replaceAll("$text", reason),
code,
{
"x-backup-status-message": reason, // glitch default nginx configuration
}
)
}
/**
* @description Redirects a user to another page.
* @param res Express response object
* @param url Target URL
* @deprecated Use `res.redirect` instead.
*/
export function Redirect(res:Response,url:string) {
res.status(302)
res.header("Location",url)
res.send()
}

View file

@ -1,36 +1,34 @@
import * as Accounts from "./accounts";
import express, { type RequestHandler } from "express"
import ServeError from "../lib/errors";
import * as auth from "./auth";
import * as Accounts from "./accounts"
import { Handler as RequestHandler } from "hono"
import ServeError from "../lib/errors"
import * as auth from "./auth"
/**
* @description Middleware which adds an account, if any, to res.locals.acc
* @description Middleware which adds an account, if any, to ctx.get("account")
*/
export const getAccount: RequestHandler = function(req, res, next) {
res.locals.acc = Accounts.getFromToken(auth.tokenFor(req))
next()
export const getAccount: RequestHandler = function (ctx, next) {
ctx.set("account", Accounts.getFromToken(auth.tokenFor(ctx)!))
return next()
}
/**
* @description Middleware which blocks requests which do not have res.locals.acc set
* @description Middleware which blocks requests which do not have ctx.get("account") set
*/
export const requiresAccount: RequestHandler = function(_req, res, next) {
if (!res.locals.acc) {
ServeError(res, 401, "not logged in")
return
export const requiresAccount: RequestHandler = function (ctx, next) {
if (!ctx.get("account")) {
return ServeError(ctx, 401, "not logged in")
}
next()
return next()
}
/**
* @description Middleware which blocks requests that have res.locals.acc.admin set to a falsy value
* @description Middleware which blocks requests that have ctx.get("account").admin set to a falsy value
*/
export const requiresAdmin: RequestHandler = function(_req, res, next) {
if (!res.locals.acc.admin) {
ServeError(res, 403, "you are not an administrator")
return
export const requiresAdmin: RequestHandler = function (ctx, next) {
if (!ctx.get("account").admin) {
return ServeError(ctx, 403, "you are not an administrator")
}
next()
return next()
}
/**
@ -39,48 +37,58 @@ export const requiresAdmin: RequestHandler = function(_req, res, next) {
* @returns Express middleware
*/
export const requiresPermissions = function(...tokenPermissions: auth.TokenPermission[]): RequestHandler {
return function(req, res, next) {
let token = auth.tokenFor(req)
export const requiresPermissions = function (
...tokenPermissions: auth.TokenPermission[]
): RequestHandler {
return function (ctx, next) {
let token = auth.tokenFor(ctx)!
let type = auth.getType(token)
if (type == "App") {
let permissions = auth.getPermissions(token)
if (!permissions) ServeError(res, 403, "insufficient permissions")
else {
if (!permissions) return ServeError(ctx, 403, "insufficient permissions")
else {
for (let v of tokenPermissions) {
if (!permissions.includes(v as auth.TokenPermission)) {
ServeError(res,403,"insufficient permissions")
return
return ServeError(ctx, 403, "insufficient permissions")
}
}
next()
return next()
}
} else next()
} else return next()
}
}
/**
* @description Blocks requests based on whether or not the token being used to access the route is of type `User`.
* @description Blocks requests based on whether or not the token being used to access the route is of type `User`.
*/
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()
export const noAPIAccess: RequestHandler = function (ctx, next) {
if (auth.getType(auth.tokenFor(ctx)!) == "App")
return ServeError(ctx, 403, "apps are not allowed to access this endpoint")
else return next()
}
/**
@description Add a restriction to this route; the condition must be true to allow API requests.
*/
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")
else next()
export const assertAPI = function (
condition: (acc: Accounts.Account, token: string) => boolean
): RequestHandler {
return function (ctx, next) {
let reqToken = auth.tokenFor(ctx)!
if (
auth.getType(reqToken) == "App" &&
condition(ctx.get("account"), reqToken)
)
return ServeError(
ctx,
403,
"apps are not allowed to access this endpoint"
)
else return next()
}
}
@ -94,21 +102,10 @@ interface SchemeObject {
}
interface SchemeArray {
type: "array",
children: SchemeParameter /* All children of the array must be this type */
| SchemeParameter[] /* Array must match this pattern */
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) {
}
}

View file

@ -1,50 +1,50 @@
import { RequestHandler } from "express"
import { type Account } from "./accounts"
import type { Handler } from "hono"
import ServeError from "./errors"
interface RatelimitSettings {
requests: number
per: number
}
/**
* @description Ratelimits a route based on res.locals.acc
* @description Ratelimits a route based on ctx.get("account")
* @param settings Ratelimit settings
* @returns Express middleware
*/
export function accountRatelimit( settings: RatelimitSettings ): RequestHandler {
export function accountRatelimit(settings: RatelimitSettings): Handler {
let activeLimits: {
[ key: string ]: {
requests: number,
[key: string]: {
requests: number
expirationHold: NodeJS.Timeout
}
} = {}
return (req, res, next) => {
if (res.locals.acc) {
let accId = res.locals.acc.id
return (ctx, next) => {
if (ctx.get("account")) {
let accId = ctx.get("account").id
let aL = activeLimits[accId]
if (!aL) {
activeLimits[accId] = {
requests: 0,
expirationHold: setTimeout(() => delete activeLimits[accId], settings.per)
expirationHold: setTimeout(
() => delete activeLimits[accId],
settings.per
),
}
aL = activeLimits[accId]
}
if (aL.requests < settings.requests) {
res.locals.undoCount = () => {
ctx.set("undoCount", () => {
if (activeLimits[accId]) {
activeLimits[accId].requests--
}
}
next()
})
return next()
} else {
ServeError(res, 429, "too many requests")
return ServeError(ctx, 429, "too many requests")
}
}
}
}
}

View file

@ -2,31 +2,32 @@ import fs from "fs/promises"
import bytes from "bytes"
import ServeError from "./lib/errors"
import * as Accounts from "./lib/accounts"
import type { Handler } from "express"
import type { Handler } from "hono"
import type Files from "./lib/files"
const pkg = require(`${process.cwd()}/package.json`)
export = (files: Files): Handler =>
async (req, res) => {
let acc = res.locals.acc as Accounts.Account
const file = files.getFilePointer(req.params.fileId)
async (ctx) => {
let acc = ctx.get("account") as Accounts.Account
const fileId = ctx.req.param("fileId")
const host = ctx.req.header("Host")
const file = files.getFilePointer(fileId)
if (file) {
if (file.visibility == "private" && acc?.id != file.owner) {
ServeError(res, 403, "you do not own this file")
return
return ServeError(ctx, 403, "you do not own this file")
}
const template = await fs
.readFile(process.cwd() + "/dist/download.html", "utf8")
.catch(() => {
throw res.sendStatus(500)
throw ctx.status(500)
})
let fileOwner = file.owner
? Accounts.getFromId(file.owner)
: undefined
res.send(
return ctx.html(
template
.replaceAll("$FileId", req.params.fileId)
.replaceAll("$FileId", fileId)
.replaceAll("$Version", pkg.version)
.replaceAll(
"$FileSize",
@ -44,18 +45,14 @@ export = (files: Files): Handler =>
.replace(
"<!--metaTags-->",
(file.mime.startsWith("image/")
? `<meta name="og:image" content="https://${req.headers.host}/file/${req.params.fileId}" />`
? `<meta name="og:image" content="https://${host}/file/${fileId}" />`
: file.mime.startsWith("video/")
? `<meta property="og:video:url" content="https://${
req.headers.host
}/cpt/${req.params.fileId}/video.${
? `<meta property="og:video:url" content="https://${host}/cpt/${fileId}/video.${
file.mime.split("/")[1] == "quicktime"
? "mov"
: file.mime.split("/")[1]
}" />
<meta property="og:video:secure_url" content="https://${
req.headers.host
}/cpt/${req.params.fileId}/video.${
<meta property="og:video:secure_url" content="https://${host}/cpt/${fileId}/video.${
file.mime.split("/")[1] == "quicktime"
? "mov"
: file.mime.split("/")[1]
@ -79,7 +76,7 @@ export = (files: Files): Handler =>
`\n<meta name="theme-color" content="${
fileOwner?.embed?.color &&
file.visibility != "anonymous" &&
(req.headers["user-agent"] || "").includes(
(ctx.req.header("user-agent") || "").includes(
"Discordbot"
)
? `#${fileOwner.embed.color}`
@ -89,11 +86,11 @@ export = (files: Files): Handler =>
.replace(
"<!--preview-->",
file.mime.startsWith("image/")
? `<div style="min-height:10px"></div><img src="/file/${req.params.fileId}" />`
? `<div style="min-height:10px"></div><img src="/file/${fileId}" />`
: file.mime.startsWith("video/")
? `<div style="min-height:10px"></div><video src="/file/${req.params.fileId}" controls></video>`
? `<div style="min-height:10px"></div><video src="/file/${fileId}" controls></video>`
: file.mime.startsWith("audio/")
? `<div style="min-height:10px"></div><audio src="/file/${req.params.fileId}" controls></audio>`
? `<div style="min-height:10px"></div><audio src="/file/${fileId}" controls></audio>`
: ""
)
.replaceAll(
@ -104,6 +101,6 @@ export = (files: Files): Handler =>
)
)
} else {
ServeError(res, 404, "file not found")
ServeError(ctx, 404, "file not found")
}
}

View file

@ -1,8 +1,8 @@
import { Router } from "express";
import { readFile, readdir } from "fs/promises";
import Files from "../lib/files";
import { Hono } from "hono"
import { readFile, readdir } from "fs/promises"
import Files from "../lib/files"
const APIDirectory = __dirname+"/api"
const APIDirectory = __dirname + "/api"
interface APIMount {
file: string
@ -18,35 +18,35 @@ interface APIDefinition {
}
function resolveMount(mount: APIMountResolvable): APIMount {
return typeof mount == "string" ? { file: mount, to: "/"+mount } : mount
return typeof mount == "string" ? { file: mount, to: "/" + mount } : mount
}
class APIVersion {
readonly definition: APIDefinition;
readonly apiPath: string;
readonly root: Router = Router();
readonly definition: APIDefinition
readonly apiPath: string
readonly root: Hono = new Hono()
constructor(definition: APIDefinition, files: Files) {
this.definition = definition;
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))
let route = require(`${this.apiPath}/${mount.file}.js`) as (
files: Files
) => Hono
this.root.route(mount.to, route(files))
}
}
}
export default class APIRouter {
readonly files: Files
readonly root: Router = Router();
readonly root: Hono = new Hono()
constructor(files: Files) {
this.files = files;
this.files = files
}
/**
@ -55,24 +55,26 @@ export default class APIRouter {
*/
private mount(definition: APIDefinition) {
console.log(`mounting APIDefinition ${definition.name}`)
this.root.use(
definition.baseURL,
(new APIVersion(definition, this.files)).root
)
this.root.route(
definition.baseURL,
new APIVersion(definition, this.files).root
)
}
async loadAPIMethods() {
let files = await readdir(APIDirectory)
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
for (let version of files) {
/// temporary (hopefully). need to figure out something else for this
let def = JSON.parse(
(
await readFile(
`${process.cwd()}/src/server/routes/api/${version}/api.json`
)
).toString()
) as APIDefinition
this.mount(def)
}
}
}
}

View file

@ -1,20 +1,21 @@
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 { Hono } from "hono"
import * as Accounts from "../../../lib/accounts"
import * as auth from "../../../lib/auth"
import { writeFile } from "fs/promises"
import { sendMail } from "../../../lib/mail"
import {
getAccount,
requiresAccount,
requiresAdmin,
requiresPermissions,
} from "../../../lib/middleware"
import Files from "../../../lib/files"
import ServeError from "../../../lib/errors";
import Files from "../../../lib/files";
let parser = bodyParser.json({
type: ["text/plain","application/json"]
})
export let adminRoutes = Router();
export let adminRoutes = new Hono<{
Variables: {
account: Accounts.Account
}
}>()
adminRoutes
.use(getAccount)
.use(requiresAccount)
@ -23,214 +24,198 @@ adminRoutes
let config = require(`${process.cwd()}/config.json`)
module.exports = function(files: Files) {
module.exports = function (files: Files) {
adminRoutes.post("/reset", async (ctx) => {
let acc = ctx.get("account") as Accounts.Account
const body = await ctx.req.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
if (
typeof body.target !== "string" ||
typeof body.password !== "string"
) {
return ctx.status(404)
}
let targetAccount = Accounts.getFromUsername(req.body.target)
let targetAccount = Accounts.getFromUsername(body.target)
if (!targetAccount) {
res.status(404)
res.send()
return
return ctx.status(404)
}
Accounts.password.set ( targetAccount.id, req.body.password )
auth.AuthTokens.filter(e => e.account == targetAccount?.id).forEach((v) => {
auth.invalidate(v.token)
})
Accounts.password.set(targetAccount.id, 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) => {})
return 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(() => ctx.text("OK"))
.catch(() => ctx.status(500))
}
res.send()
})
adminRoutes.post("/elevate", parser, (req,res) => {
adminRoutes.post("/elevate", async (ctx) => {
const body = await ctx.req.json()
let acc = ctx.get("account") as Accounts.Account
let acc = res.locals.acc as Accounts.Account
if (typeof req.body.target !== "string") {
res.status(404)
res.send()
return
if (typeof body.target !== "string") {
return ctx.status(404)
}
let targetAccount = Accounts.getFromUsername(req.body.target)
let targetAccount = Accounts.getFromUsername(body.target)
if (!targetAccount) {
res.status(404)
res.send()
return
return ctx.status(404)
}
targetAccount.admin = true;
Accounts.save()
res.send()
return ctx.text("OK")
})
adminRoutes.post("/delete", parser, (req,res) => {
if (typeof req.body.target !== "string") {
res.status(404)
res.send()
return
adminRoutes.post("/delete", async (ctx) => {
const body = await ctx.req.json()
if (typeof body.target !== "string") {
return ctx.status(404)
}
let targetFile = files.getFilePointer(req.body.target)
let targetFile = files.getFilePointer(body.target)
if (!targetFile) {
res.status(404)
res.send()
return
return ctx.status(404)
}
files.unlink(req.body.target).then(() => {
res.status(200)
}).catch(() => {
res.status(500)
}).finally(() => res.send())
return files
.unlink(body.target)
.then(() => ctx.status(200))
.catch(() => ctx.status(500))
.finally(() => ctx.status(200))
})
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
adminRoutes.post("/delete_account", async (ctx) => {
let acc = ctx.get("account") as Accounts.Account
const body = await ctx.req.json()
if (typeof body.target !== "string") {
return ctx.status(404)
}
let targetAccount = Accounts.getFromUsername(req.body.target)
let targetAccount = Accounts.getFromUsername(body.target)
if (!targetAccount) {
res.status(404)
res.send()
return
return ctx.status(404)
}
let accId = targetAccount.id
auth.AuthTokens.filter(e => e.account == accId).forEach((v) => {
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
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">${
body.reason || "(no reason specified)"
}</span><br><br> Your files ${
body.deleteFiles
? "have been deleted"
: "have not been modified"
}. Thank you for using monofile.`
)
}
return ctx.text("account deleted")
})
if (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))
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()
return writeFile(
process.cwd() + "/.data/files.json",
JSON.stringify(files.files)
).then(cpl)
} else return 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
adminRoutes.post("/transfer", async (ctx) => {
const body = await ctx.req.json()
if (typeof body.target !== "string" || typeof body.owner !== "string") {
return ctx.status(404)
}
let newOwner = Accounts.getFromUsername(req.body.owner || "")
let targetFile = files.getFilePointer(body.target)
if (!targetFile) {
return ctx.status(404)
}
let newOwner = Accounts.getFromUsername(body.owner || "")
// clear old owner
if (targetFile.owner) {
let oldOwner = Accounts.getFromId(targetFile.owner)
if (oldOwner) {
Accounts.files.deindex(oldOwner.id, req.body.target)
}
Accounts.files.deindex(oldOwner.id, body.target)
}
}
if (newOwner) {
Accounts.files.index(newOwner.id, req.body.target)
Accounts.files.index(newOwner.id, 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
targetFile.owner = newOwner ? newOwner.id : undefined
files
.writeFile(body.target, targetFile)
.then(() => ctx.status(200))
.catch(() => ctx.status(500))
})
adminRoutes.post("/idchange", parser, (req,res) => {
if (typeof req.body.target !== "string" || typeof req.body.new !== "string") {
res.status(400)
res.send()
return
adminRoutes.post("/idchange", async (ctx) => {
const body = await ctx.req.json()
if (typeof body.target !== "string" || typeof body.new !== "string") {
return ctx.status(400)
}
let targetFile = files.getFilePointer(req.body.target)
let targetFile = files.getFilePointer(body.target)
if (!targetFile) {
res.status(404)
res.send()
return
return ctx.status(404)
}
if (files.getFilePointer(req.body.new)) {
res.status(400)
res.send()
return
if (files.getFilePointer(body.new)) {
return ctx.status(400)
}
if (targetFile.owner) {
Accounts.files.deindex(targetFile.owner, req.body.target)
Accounts.files.index(targetFile.owner, req.body.new)
Accounts.files.deindex(targetFile.owner, body.target)
Accounts.files.index(targetFile.owner, body.new)
}
delete files.files[req.body.target]
delete files.files[body.target]
files.writeFile(req.body.new, targetFile).then(() => {
res.send()
}).catch(() => {
files.files[req.body.target] = req.body.new
return files
.writeFile(body.new, targetFile)
.then(() => ctx.status(200))
.catch(() => {
files.files[body.target] = 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()
})
if (targetFile.owner) {
Accounts.files.deindex(targetFile.owner, body.new)
Accounts.files.index(targetFile.owner, body.target)
}
return ctx.status(500)
})
})
return adminRoutes
}
}

View file

@ -1,356 +1,483 @@
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 { Hono, Handler } from "hono"
import { getCookie, setCookie } from "hono/cookie"
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 ServeError from "../../../lib/errors"
import Files, {
FileVisibility,
generateFileId,
id_check_regex,
} from "../../../lib/files"
import { writeFile } from "fs";
import { writeFile } from "fs/promises"
let parser = bodyParser.json({
type: ["text/plain","application/json"]
})
export let authRoutes = Router();
authRoutes.use(getAccount)
export let authRoutes = new Hono<{
Variables: {
account: Accounts.Account
}
}>()
let config = require(`${process.cwd()}/config.json`)
authRoutes.all("*", getAccount)
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
module.exports = function (files: Files) {
authRoutes.post("/login", async (ctx) => {
console.log(ctx)
const body = await ctx.req.json()
if (
typeof body.username != "string" ||
typeof body.password != "string"
) {
return ServeError(ctx, 400, "please provide a username or password")
}
if (auth.validate(req.cookies.auth)) return
if (auth.validate(getCookie(ctx, "auth")!))
return ctx.text("You are already authed")
/*
check if account exists
*/
let acc = Accounts.getFromUsername(req.body.username)
let acc = Accounts.getFromUsername(body.username)
if (!acc) {
ServeError(res,401,"username or password incorrect")
return
return ServeError(ctx, 401, "username or password incorrect")
}
if (!Accounts.password.check(acc.id,req.body.password)) {
ServeError(res,401,"username or password incorrect")
return
if (!Accounts.password.check(acc.id, body.password)) {
return ServeError(ctx, 401, "username or password incorrect")
}
/*
assign token
*/
res.cookie("auth",auth.create(acc.id,(3*24*60*60*1000)))
res.status(200)
res.end()
setCookie(ctx, "auth", auth.create(acc.id, 3 * 24 * 60 * 60 * 1000))
return ctx.text("")
})
authRoutes.post("/create", parser, (req,res) => {
authRoutes.post("/create", async (ctx) => {
if (!config.accounts.registrationEnabled) {
ServeError(res,403,"account registration disabled")
return
return ServeError(ctx, 403, "account registration disabled")
}
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
if (auth.validate(getCookie(ctx, "auth")!)) return
const body = await ctx.req.json()
if (
typeof body.username != "string" ||
typeof body.password != "string"
) {
return ServeError(ctx, 400, "please provide a username or password")
}
/*
check if account exists
*/
let acc = Accounts.getFromUsername(req.body.username)
let acc = Accounts.getFromUsername(body.username)
if (acc) {
ServeError(res,400,"account with this username already exists")
ServeError(ctx, 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")
if (body.username.length < 3 || body.username.length > 20) {
return ServeError(
ctx,
400,
"username must be over or equal to 3 characters or under or equal to 20 characters in length"
)
}
if (
(body.username.match(/[A-Za-z0-9_\-\.]+/) || [])[0] != body.username
) {
return ServeError(ctx, 400, "username contains invalid characters")
}
if (body.password.length < 8) {
ServeError(ctx, 400, "password must be 8 characters or longer")
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)
return Accounts.create(body.username, 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")
setCookie(
ctx,
"auth",
auth.create(newAcc, 3 * 24 * 60 * 60 * 1000)
)
return ctx.text("")
})
.catch(() => ServeError(ctx, 500, "internal server error"))
})
authRoutes.post("/logout", (req,res) => {
if (!auth.validate(req.cookies.auth)) {
ServeError(res, 401, "not logged in")
return
authRoutes.post("/logout", async (ctx) => {
if (!auth.validate(getCookie(ctx, "auth")!)) {
return ServeError(ctx, 401, "not logged in")
}
auth.invalidate(req.cookies.auth)
res.send("logged out")
auth.invalidate(getCookie(ctx, "auth")!)
return ctx.text("logged out")
})
authRoutes.post("/dfv", requiresAccount, requiresPermissions("manage"), parser, (req,res) => {
let acc = res.locals.acc as Accounts.Account
authRoutes.post(
"/dfv",
requiresAccount,
requiresPermissions("manage"),
// Used body-parser
async (ctx) => {
const body = await ctx.req.json()
let acc = ctx.get("account") 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")
if (
["public", "private", "anonymous"].includes(
body.defaultFileVisibility
)
) {
acc.defaultFileVisibility = body.defaultFileVisibility
Accounts.save()
return ctx.text(
`dfv has been set to ${acc.defaultFileVisibility}`
)
} else {
return ctx.text("invalid dfv", 400)
}
}
})
)
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 (
authRoutes.post(
"/customcss",
requiresAccount,
requiresPermissions("customize"),
// Used body-parser
async (ctx) => {
const body = await ctx.req.json()
let acc = ctx.get("account") as Accounts.Account
!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")
if (typeof body.fileId != "string") body.fileId = undefined
if (
!body.fileId ||
(body.fileId.match(id_check_regex) == body.fileId &&
body.fileId.length <= config.maxUploadIdLength)
) {
acc.customCSS = body.fileId || undefined
if (!body.fileId) delete acc.customCSS
Accounts.save()
return ctx.text(`custom css saved`)
} else {
return ctx.text("invalid fileid", 400)
}
}
})
)
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 (
authRoutes.post(
"/embedcolor",
requiresAccount,
requiresPermissions("customize"),
// Used body-parser
async (ctx) => {
let acc = ctx.get("account") as Accounts.Account
const body = await ctx.req.json()
if (typeof body.color != "string") body.color = undefined
if (
!body.color ||
(body.color.toLowerCase().match(/[a-f0-9]+/) ==
body.color.toLowerCase() &&
body.color.length == 6)
) {
if (!acc.embed) acc.embed = {}
acc.embed.color = body.color || undefined
if (!body.color) delete acc.embed.color
Accounts.save()
return ctx.text(`custom embed color saved`)
} else {
return ctx.text("invalid hex code", 400)
}
}
)
authRoutes.post(
"/embedsize",
requiresAccount,
requiresPermissions("customize"),
// Used body-parser
async (ctx) => {
let acc = ctx.get("account") as Accounts.Account
const body = await ctx.req.json()
if (typeof body.largeImage != "boolean") body.color = false
!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
acc.embed.largeImage = body.largeImage
if (!body.largeImage) delete acc.embed.largeImage
Accounts.save()
res.send(`custom embed color saved`)
} else {
res.status(400)
res.send("invalid hex code")
return ctx.text(`custom embed image size saved`)
}
})
)
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;
authRoutes.post(
"/delete_account",
requiresAccount,
noAPIAccess,
// Used body-parser
async (ctx) => {
let acc = ctx.get("account") as Accounts.Account
const body = await ctx.req.json()
let accId = acc.id
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`)
})
auth.AuthTokens.filter((e) => e.account == accId).forEach((v) => {
auth.invalidate(v.token)
})
authRoutes.post("/delete_account", requiresAccount, noAPIAccess, parser, async (req,res) => {
let acc = res.locals.acc as Accounts.Account
let accId = acc.id
let cpl = () =>
Accounts.deleteAccount(accId).then((_) =>
ctx.text("account deleted")
)
auth.AuthTokens.filter(e => e.account == accId).forEach((v) => {
auth.invalidate(v.token)
})
if (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))
}
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))
return writeFile(
process.cwd() + "/.data/files.json",
JSON.stringify(files.files)
).then(cpl)
} else cpl()
}
)
authRoutes.post(
"/change_username",
requiresAccount,
noAPIAccess,
// Used body-parser
async (ctx) => {
let acc = ctx.get("account") as Accounts.Account
const body = await ctx.req.json()
if (
typeof body.username != "string" ||
body.username.length < 3 ||
body.username.length > 20
) {
return ServeError(
ctx,
400,
"username must be between 3 and 20 characters in length"
)
}
writeFile(process.cwd()+"/.data/files.json",JSON.stringify(files.files), (err) => {
if (err) console.log(err)
cpl()
})
} else cpl()
})
let _acc = Accounts.getFromUsername(body.username)
authRoutes.post("/change_username", requiresAccount, noAPIAccess, parser, (req,res) => {
let acc = res.locals.acc as Accounts.Account
if (_acc) {
return ServeError(
ctx,
400,
"account with this username already exists"
)
}
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
if (
(body.username.match(/[A-Za-z0-9_\-\.]+/) || [])[0] !=
body.username
) {
return ServeError(
ctx,
400,
"username contains invalid characters"
)
}
acc.username = body.username
Accounts.save()
if (acc.email) {
return sendMail(
acc.email,
`Your login details have been updated`,
`<b>Hello there!</b> Your username has been updated to <span username>${body.username}</span>. Please update your devices accordingly. Thank you for using monofile.`
)
.then(() => ctx.text("OK"))
.catch((err) => {})
}
return ctx.text("username changed")
}
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}>()
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
}
authRoutes.post(
"/request_email_change",
requiresAccount,
noAPIAccess,
accountRatelimit({ requests: 4, per: 60 * 60 * 1000 }),
// Used body-parser
async (ctx) => {
let acc = ctx.get("account") as Accounts.Account
const body = await ctx.req.json()
if (typeof body.email != "string" || !body.email) {
ServeError(ctx, 400, "supply an email")
return
}
let vcode = verificationCodes.get(acc.id)
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
// delete previous if any
let e = vcode?.expiry
if (e) clearTimeout(e)
verificationCodes.delete(acc?.id||"")
res.locals.undoCount();
ServeError(res, 500, err?.toString())
})
})
verificationCodes.delete(acc?.id || "")
authRoutes.get("/confirm_email/:code", requiresAccount, noAPIAccess, (req,res) => {
let acc = res.locals.acc as Accounts.Account
let code = generateFileId(12).toUpperCase()
let vcode = verificationCodes.get(acc.id)
// set
if (!vcode) { ServeError(res, 400, "nothing to confirm"); return }
verificationCodes.set(acc.id, {
code,
email: body.email,
expiry: setTimeout(
() => verificationCodes.delete(acc?.id || ""),
15 * 60 * 1000
),
})
if (typeof req.params.code == "string" && req.params.code.toUpperCase() == vcode.code) {
acc.email = vcode.email
Accounts.save();
// this is a mess but it's fine
let e = verificationCodes.get(acc?.id||"")?.expiry
if (e) clearTimeout(e)
verificationCodes.delete(acc?.id||"")
res.redirect("/")
} else {
ServeError(res, 400, "invalid code")
sendMail(
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>${
body.email.split("@")[0]
}<span style="opacity:0.5">@${
body.email.split("@")[1]
}</span></span>, to your account, <span username>${
acc.username
}</span>. If you would like to continue, please <a href="https://${ctx.req.header(
"Host"
)}/auth/confirm_email/${code}"><span code>click here</span></a>, or go to https://${ctx.req.header(
"Host"
)}/auth/confirm_email/${code}.`
)
.then(() => ctx.text("OK"))
.catch((err) => {
let e = verificationCodes.get(acc?.id || "")?.expiry
if (e) clearTimeout(e)
verificationCodes.delete(acc?.id || "")
;(ctx.get("undoCount" as never) as () => {})()
return ServeError(ctx, 500, err?.toString())
})
}
})
)
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")
})
authRoutes.get(
"/confirm_email/:code",
requiresAccount,
noAPIAccess,
async (ctx) => {
let acc = ctx.get("account") as Accounts.Account
let pwReset = new Map<string, {code: string, expiry: NodeJS.Timeout, requestedAt:number}>()
let vcode = verificationCodes.get(acc.id)
if (!vcode) {
ServeError(ctx, 400, "nothing to confirm")
return
}
if (
typeof ctx.req.param("code") == "string" &&
ctx.req.param("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 || "")
return ctx.redirect("/")
} else {
return ServeError(ctx, 400, "invalid code")
}
}
)
authRoutes.post(
"/remove_email",
requiresAccount,
noAPIAccess,
async (ctx) => {
let acc = ctx.get("account") as Accounts.Account
if (acc.email) {
delete acc.email
Accounts.save()
return ctx.text("email detached")
} else return ServeError(ctx, 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")
authRoutes.post("/request_emergency_login", async (ctx) => {
if (auth.validate(getCookie(ctx, "auth") || "")) return
const body = await ctx.req.json()
if (typeof body.account != "string" || !body.account) {
ServeError(ctx, 400, "supply a username")
return
}
let acc = Accounts.getFromUsername(req.body.account)
let acc = Accounts.getFromUsername(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
return ServeError(
ctx,
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"
)
}
let pResetCode = pwReset.get(acc.id)
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
if (
pResetCode &&
pResetCode.requestedAt + 15 * 60 * 1000 > Date.now()
) {
return ServeError(
ctx,
429,
`Please wait a few moments to request another emergency login.`
)
}
// delete previous if any
let e = pResetCode?.expiry
if (e) clearTimeout(e)
pwReset.delete(acc?.id||"")
prcIdx.delete(pResetCode?.code||"")
pwReset.delete(acc?.id || "")
prcIdx.delete(pResetCode?.code || "")
let code = generateFileId(12).toUpperCase()
@ -358,107 +485,146 @@ module.exports = function(files: Files) {
pwReset.set(acc.id, {
code,
expiry: setTimeout( () => { pwReset.delete(acc?.id||""); prcIdx.delete(pResetCode?.code||"") }, 15*60*1000),
requestedAt: Date.now()
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())
})
return 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://${ctx.req.header(
"Host"
)}/auth/emergency_login/${code}"><span code>click here</span></a>, or go to https://${ctx.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(() => ctx.text("OK"))
.catch((err) => {
let e = pwReset.get(acc?.id || "")?.expiry
if (e) clearTimeout(e)
pwReset.delete(acc?.id || "")
prcIdx.delete(code || "")
return ServeError(ctx, 500, err?.toString())
})
})
authRoutes.get("/emergency_login/:code", (req,res) => {
if (auth.validate(req.cookies.auth || "")) {
ServeError(res, 403, "already logged in")
return
authRoutes.get("/emergency_login/:code", async (ctx) => {
if (auth.validate(getCookie(ctx, "auth") || "")) {
return ServeError(ctx, 403, "already logged in")
}
let vcode = prcIdx.get(req.params.code)
let vcode = prcIdx.get(ctx.req.param("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("/")
if (!vcode) {
return ServeError(ctx, 400, "invalid emergency login code")
}
if (typeof ctx.req.param("code") == "string" && vcode) {
setCookie(ctx, "auth", auth.create(vcode, 3 * 24 * 60 * 60 * 1000))
let e = pwReset.get(vcode)?.expiry
if (e) clearTimeout(e)
pwReset.delete(vcode)
prcIdx.delete(req.params.code)
prcIdx.delete(ctx.req.param("code"))
return ctx.redirect("/")
} else {
ServeError(res, 400, "invalid code")
ServeError(ctx, 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
authRoutes.post(
"/change_password",
requiresAccount,
noAPIAccess,
// Used body-parser
async (ctx) => {
let acc = ctx.get("account") as Accounts.Account
const body = await ctx.req.json()
if (typeof body.password != "string" || body.password.length < 8) {
ServeError(ctx, 400, "password must be 8 characters or longer")
return
}
let accId = acc.id
Accounts.password.set(accId, body.password)
auth.AuthTokens.filter((e) => e.account == accId).forEach((v) => {
auth.invalidate(v.token)
})
if (acc.email) {
return 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(() => ctx.text("OK"))
.catch((err) => {})
}
return ctx.text("password changed - logged out all sessions")
}
)
let accId = acc.id
authRoutes.post(
"/logout_sessions",
requiresAccount,
noAPIAccess,
async (ctx) => {
let acc = ctx.get("account") as Accounts.Account
Accounts.password.set(accId,req.body.password)
let accId = acc.id
auth.AuthTokens.filter(e => e.account == accId).forEach((v) => {
auth.invalidate(v.token)
})
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) => {})
return ctx.text("logged out all sessions")
}
)
res.send("password changed - logged out all sessions")
authRoutes.get(
"/me",
requiresAccount,
requiresPermissions("user"),
async (ctx) => {
let acc = ctx.get("account") as Accounts.Account
let sessionToken = auth.tokenFor(ctx)!
let accId = acc.id
return ctx.json({
...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", async (ctx) => {
let acc = ctx.get("account")
if (acc?.customCSS) return ctx.redirect(`/file/${acc.customCSS}`)
else return ctx.text("")
})
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

@ -1,98 +1,129 @@
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 { Hono } from "hono"
import * as Accounts from "../../../lib/accounts"
import { writeFile } from "fs/promises"
import Files from "../../../lib/files"
import {
getAccount,
requiresAccount,
requiresPermissions,
} from "../../../lib/middleware"
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();
export let fileApiRoutes = new Hono<{
Variables: {
account: Accounts.Account
}
}>()
let config = require(`${process.cwd()}/config.json`)
fileApiRoutes.use("*", getAccount) // :warning: /list somehow crashes Hono with an internal error!
/*
/home/jack/Code/Web/monofile/node_modules/.pnpm/@hono+node-server@1.2.0/node_modules/@hono/node-server/dist/listener.js:55
const contentType = res.headers.get("content-type") || "";
^
module.exports = function(files: Files) {
TypeError: Cannot read properties of undefined (reading 'get')
at Server.<anonymous> (/home/jack/Code/Web/monofile/node_modules/.pnpm/@hono+node-server@1.2.0/node_modules/@hono/node-server/dist/listener.js:55:37)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
*/
fileApiRoutes.use(getAccount);
module.exports = function (files: Files) {
fileApiRoutes.get(
"/list",
requiresAccount,
requiresPermissions("user"),
async (ctx) => {
let acc = ctx.get("account") as Accounts.Account
fileApiRoutes.get("/list", requiresAccount, requiresPermissions("user"), (req,res) => {
if (!acc) return
let accId = acc.id
let acc = res.locals.acc as Accounts.Account
if (!acc) return
let accId = acc.id
ctx.json(
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)
)
}
)
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) {
fileApiRoutes.post(
"/manage",
requiresPermissions("manage"),
async (ctx) => {
let acc = ctx.get("account") as Accounts.Account
const body = await ctx.req.json()
if (!acc) return
if (
!body.target ||
!(typeof body.target == "object") ||
body.target.length < 1
)
return
}
switch( req.body.action ) {
case "delete":
files.unlink(e, true)
modified++;
break;
let modified = 0
case "changeFileVisibility":
if (!["public","anonymous","private"].includes(req.body.value)) return;
files.files[e].visibility = req.body.value;
modified++;
break;
body.target.forEach((e: string) => {
if (!acc.files.includes(e)) return
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;
}
})
let fp = files.getFilePointer(e)
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`)
if (fp.reserved) {
return
}
switch (body.action) {
case "delete":
files.unlink(e, true)
modified++
break
case "changeFileVisibility":
if (
!["public", "anonymous", "private"].includes(
body.value
)
)
return
files.files[e].visibility = body.value
modified++
break
case "setTag":
if (!body.value) delete files.files[e].tag
else {
if (body.value.toString().length > 30) return
files.files[e].tag = body.value
.toString()
.toLowerCase()
}
modified++
break
}
})
}).catch((err) => console.error(err))
})
return Accounts.save()
.then(() => {
writeFile(
process.cwd() + "/.data/files.json",
JSON.stringify(files.files)
)
})
.then(() => ctx.text(`modified ${modified} files`))
.catch((err) => console.error(err))
}
)
return fileApiRoutes
}
}

View file

@ -1,11 +1,12 @@
import bodyParser from "body-parser"
import express, { Router } from "express"
import { Hono } from "hono"
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 { Readable } from "stream"
import ServeError from "../../../lib/errors"
import Files from "../../../lib/files"
import { getAccount, requiresPermissions } from "../../../lib/middleware"
@ -14,7 +15,11 @@ let parser = bodyParser.json({
type: ["text/plain", "application/json"],
})
export let primaryApi = Router()
export let primaryApi = new Hono<{
Variables: {
account: Accounts.Account
}
}>()
const multerSetup = multer({ storage: memoryStorage() })
@ -23,216 +28,210 @@ 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
// primaryApi.get(
// ["/file/:fileId", "/cpt/:fileId/*", "/:fileId"],
// async (ctx) => {
// let acc = ctx.get("account") 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(ctx.req.param("fileId"))
// ctx.header("Access-Control-Allow-Origin", "*")
// ctx.header("Content-Security-Policy", "sandbox allow-scripts")
// if (ctx.req.query("attachment") == "1")
// ctx.header("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) {
// return ServeError(ctx, 403, "you do not own this file")
// }
if (
auth.getType(auth.tokenFor(req)) == "App" &&
auth
.getPermissions(auth.tokenFor(req))
?.includes("private")
) {
ServeError(res, 403, "insufficient permissions")
return
}
}
// if (
// auth.getType(auth.tokenFor(ctx)!) == "App" &&
// auth
// .getPermissions(auth.tokenFor(ctx)!)
// ?.includes("private")
// ) {
// ServeError(ctx, 403, "insufficient permissions")
// return
// }
// }
let range: Range | undefined
// let range: Range | undefined
res.setHeader("Content-Type", file.mime)
if (file.sizeInBytes) {
res.setHeader("Content-Length", file.sizeInBytes)
// ctx.header("Content-Type", file.mime)
// if (file.sizeInBytes) {
// ctx.header("Content-Length", file.sizeInBytes.toString())
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
}
// if (file.chunkSize) {
// let range = ctx.range(file.sizeInBytes)
// if (range) {
// // error handling
// if (typeof range == "number") {
// return ctx.status(range == -1 ? 416 : 400)
// }
// if (range.type != "bytes") {
// return ctx.status(400)
// }
// set ranges var
let rngs = Array.from(rng)
if (rngs.length != 1) {
res.status(400).send()
return
}
range = rngs[0]
}
}
}
// // set ranges var
// let rngs = Array.from(range)
// if (rngs.length != 1) {
// return ctx.status(400)
// }
// range = rngs[0]
// }
// }
// }
// supports ranges
// // 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")
}
}
)
// return files
// .readFileStream(ctx.req.param("fileId"), range)
// .then(async (stream) => {
// if (range) {
// ctx.status(206)
// ctx.header(
// "Content-Length",
// (range.end - range.start + 1).toString()
// )
// ctx.header(
// "Content-Range",
// `bytes ${range.start}-${range.end}/${file.sizeInBytes}`
// )
// }
primaryApi.head(
["/file/:fileId", "/cpt/:fileId/*", "/:fileId"],
(req: express.Request, res: express.Response) => {
let file = files.getFilePointer(req.params.fileId)
// return ctx.stream((stre) => {
// // Somehow return a stream?
// })
// })
// .catch((err) => {
// return ServeError(ctx, err.status, err.message)
// })
// } else {
// return ServeError(ctx, 404, "file not found")
// }
// }
// )
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
}
// // primaryApi.head(
// // ["/file/:fileId", "/cpt/:fileId/*", "/:fileId"],
// // async (ctx) => {
// // let file = files.getFilePointer(req.params.fileId)
res.setHeader("Access-Control-Allow-Origin", "*")
res.setHeader("Content-Security-Policy", "sandbox allow-scripts")
// // if (
// // file.visibility == "private" &&
// // (ctx.get("account")?.id != file.owner ||
// // (auth.getType(auth.tokenFor(ctx)!) == "App" &&
// // auth
// // .getPermissions(auth.tokenFor(ctx)!)
// // ?.includes("private")))
// // ) {
// // return ctx.status(403)
// // }
if (req.query.attachment == "1")
res.setHeader("Content-Disposition", "attachment")
// // ctx.header("Content-Security-Policy", "sandbox allow-scripts")
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()
}
}
)
// // if (ctx.req.query("attachment") == "1")
// // ctx.header("Content-Disposition", "attachment")
// upload handlers
// // if (!file) {
// // res.status(404)
// // res.send()
// // } else {
// // ctx.header("Content-Type", file.mime)
// // if (file.sizeInBytes) {
// // ctx.header("Content-Length", file.sizeInBytes)
// // }
// // if (file.chunkSize) {
// // ctx.header("Accept-Ranges", "bytes")
// // }
// // res.send()
// // }
// // }
// // )
primaryApi.post(
"/upload",
requiresPermissions("upload"),
multerSetup.single("file"),
async (req, res) => {
let acc = res.locals.acc as Accounts.Account
// // upload handlers
if (req.file) {
try {
let prm = req.header("monofile-params")
let params: { [key: string]: any } = {}
if (prm) {
params = JSON.parse(prm)
}
// primaryApi.post(
// "/upload",
// requiresPermissions("upload"),
// multerSetup.single("file"),
// async (ctx) => {
// let acc = ctx.get("account") as Accounts.Account
files
.uploadFile(
{
owner: acc?.id,
// if (req.file) {
// try {
// let prm = req.header("monofile-params")
// let params: { [key: string]: any } = {}
// if (prm) {
// params = JSON.parse(prm)
// }
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")
}
} else {
res.status(400)
res.send("[err] bad request")
}
}
)
// files
// .uploadFile(
// {
// owner: acc?.id,
primaryApi.post(
"/clone",
requiresPermissions("upload"),
bodyParser.json({ type: ["text/plain", "application/json"] }),
(req, res) => {
let acc = res.locals.acc as Accounts.Account
// 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")
// }
// } else {
// res.status(400)
// res.send("[err] bad request")
// }
// }
// )
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")
}
}
)
// primaryApi.post(
// "/clone",
// requiresPermissions("upload"),
// async ctx => {
// let acc = ctx.get("account") as Accounts.Account
// try {
// return 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)
// return res.text(`[err] failed to fetch data`, 400)
// })
// } catch {
// return ctx.text("[err] an error occured", 500)
// }
// }
// )
return primaryApi
}

View file

@ -1,214 +1,223 @@
// Modules
import { writeFile } from 'fs'
import { Router } from "express";
import bodyParser from "body-parser";
import { Hono } from "hono"
import { getCookie, setCookie } from "hono/cookie"
// 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, requiresPermissions } from "../../../lib/middleware";
import ServeError from "../../../lib/errors";
import { sendMail } from '../../../lib/mail';
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"
import { sendMail } from "../../../lib/mail"
const Configuration = require(`${process.cwd()}/config.json`)
const parser = bodyParser.json({
type: [ "type/plain", "application/json" ]
})
const router = new Hono<{
Variables: {
account: Accounts.Account
}
}>()
const router = Router()
router.use(getAccount)
router.use(getAccount, parser)
module.exports = function(files: Files) {
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")
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()
module.exports = function (files: Files) {
router.post("/login", async (ctx, res) => {
const body = await ctx.req.json()
if (
typeof body.username != "string" ||
typeof body.password != "string"
) {
ServeError(ctx, 400, "please provide a username or password")
return
}
)
router.post(
"/create",
(req, res) => {
if (!Configuration.accounts.registrationEnabled) {
ServeError(res , 403, "account registration disabled")
return
if (Authentication.validate(getCookie(ctx, "auth")!)) {
ServeError(ctx, 400, "you are already logged in")
return
}
const Account = Accounts.getFromUsername(body.username)
if (!Account || !Accounts.password.check(Account.id, body.password)) {
ServeError(ctx, 400, "username or password incorrect")
return
}
setCookie(
ctx,
"auth",
Authentication.create(
Account.id, // account id
3 * 24 * 60 * 60 * 1000 // expiration time
),
{
// expires:
}
)
ctx.status(200)
})
if (Authentication.validate(req.cookies.auth)) {
ServeError(res, 400, "you are already logged in")
return
}
router.post("/create", async (ctx) => {
const body = await ctx.req.json()
if (!Configuration.accounts.registrationEnabled) {
return ServeError(ctx, 403, "account registration disabled")
}
if (Accounts.getFromUsername(req.body.username)) {
ServeError(res, 400, "account with this username already exists")
return
}
if (Authentication.validate(getCookie(ctx, "auth")!)) {
return ServeError(ctx, 400, "you are already logged in")
}
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 (Accounts.getFromUsername(body.username)) {
return ServeError(
ctx,
400,
"account with this username already exists"
)
}
if (
(
req.body.username.match(/[A-Za-z0-9_\-\.]+/)
||
[]
)[0] != req.body.username
) {
ServeError(res, 400, "username contains invalid characters")
return
}
if (body.username.length < 3 || body.username.length > 20) {
return ServeError(
ctx,
400,
"username must be over or equal to 3 characters or under or equal to 20 characters in length"
)
}
if (req.body.password.length < 8) {
ServeError(res, 400, "password must be 8 characters or longer")
return
}
if (
(body.username.match(/[A-Za-z0-9_\-\.]+/) || [])[0] != body.username
) {
return ServeError(ctx, 400, "username contains invalid characters")
}
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()
if (body.password.length < 8) {
return ServeError(
ctx,
400,
"password must be 8 characters or longer"
)
}
return Accounts.create(body.username, body.password)
.then((Account) => {
setCookie(
ctx,
"auth",
Authentication.create(
Account, // account id
3 * 24 * 60 * 60 * 1000 // expiration time
),
{
// expires:
}
)
return ctx.status(200)
})
.catch(() => {
ServeError(res, 500, "internal server error")
return ServeError(ctx, 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.post("/logout", (ctx) => {
if (!Authentication.validate(getCookie(ctx, "auth")!)) {
return ServeError(ctx, 401, "not logged in")
}
)
Authentication.invalidate(getCookie(ctx, "auth")!)
return ctx.text("logged out")
})
router.patch(
"/dfv",
requiresAccount, requiresPermissions("manage"),
(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 {
ServeError(res, 400, "invalid dfv")
}
}
)
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)
})
Accounts.deleteAccount(accountId).then(_ => res.send("account deleted"))
}
)
router.patch("/me/name",
requiresAccount,
noAPIAccess,
parser,
(req, res) => {
const Account = res.locals.acc as Accounts.Account
const newUsername = req.body.username
requiresPermissions("manage"),
async (ctx) => {
const body = await ctx.req.json()
const Account = ctx.get("account")! as Accounts.Account
if (
typeof newUsername != "string"
||
newUsername.length < 3
||
req.body.username.length > 20
["public", "private", "anonymous"].includes(
body.defaultFileVisibility
)
) {
ServeError(res, 400, "username must be between 3 and 20 characters in length")
return
}
Account.defaultFileVisibility = body.defaultFileVisibility
if (Accounts.getFromUsername(newUsername)) {
ServeError(res, 400, "account with this username already exists")
}
Accounts.save()
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`,
`<b>Hello there!</b> Your username has been updated to <span username>${newUsername}</span>. Please update your devices accordingly. Thank you for using monofile.`
).then(() => {
res.send("OK")
}).catch((err) => {})
return ctx.text(
`dfv has been set to ${Account.defaultFileVisibility}`
)
} else {
return ServeError(ctx, 400, "invalid dfv")
}
}
)
router.delete("/me", requiresAccount, noAPIAccess, async (ctx) => {
const Account = ctx.get("account") as Accounts.Account
const accountId = Account.id
Authentication.AuthTokens.filter((e) => e.account == accountId).forEach(
(token) => {
Authentication.invalidate(token.token)
}
)
await Accounts.deleteAccount(accountId)
return ctx.text("account deleted")
})
router.patch("/me/name", requiresAccount, noAPIAccess, async (ctx) => {
const Account = ctx.get("account") as Accounts.Account
const body = await ctx.req.json()
const newUsername = body.username
if (
typeof newUsername != "string" ||
newUsername.length < 3 ||
newUsername.length > 20
) {
return ServeError(
ctx,
400,
"username must be between 3 and 20 characters in length"
)
}
if (Accounts.getFromUsername(newUsername)) {
return ServeError(
ctx,
400,
"account with this username already exists"
)
}
if (
(newUsername.match(/[A-Za-z0-9_\-\.]+/) || [])[0] != body.username
) {
ServeError(ctx, 400, "username contains invalid characters")
return
}
Account.username = newUsername
Accounts.save()
if (Account.email) {
await sendMail(
Account.email,
`Your login details have been updated`,
`<b>Hello there!</b> Your username has been updated to <span username>${newUsername}</span>. Please update your devices accordingly. Thank you for using monofile.`
).catch()
return ctx.text("OK")
}
})
return router
}
}

View file

@ -1,120 +1,119 @@
// Modules
import { writeFile } from 'fs'
import { Router } from "express";
import bodyParser from "body-parser";
import { writeFile } from "fs/promises"
import { Hono } from "hono"
// 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';
import Files, { id_check_regex } from "../../../lib/files"
import * as Accounts from "../../../lib/accounts"
import * as Authentication from "../../../lib/auth"
import {
getAccount,
noAPIAccess,
requiresAccount,
requiresAdmin,
} 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 = new Hono<{
Variables: {
account?: Accounts.Account
}
}>()
const router = Router()
router.use(getAccount, requiresAccount, requiresAdmin)
router.use(getAccount, requiresAccount, requiresAdmin, parser)
module.exports = function (files: Files) {
router.patch("/account/:username/password", async (ctx) => {
const Account = ctx.get("account") as Accounts.Account
const body = await ctx.req.json()
module.exports = function(files: Files) {
router.patch(
"/account/:username/password",
(req, res) => {
const Account = res.locals.acc as Accounts.Account
const targetUsername = ctx.req.param("username")
const password = body.password
const targetUsername = req.params.username
const password = req.body.password
if (typeof password !== "string") return ServeError(ctx, 404, "")
if (typeof password !== "string") {
ServeError(res, 404, "")
return
}
const targetAccount = Accounts.getFromUsername(targetUsername)
const targetAccount = Accounts.getFromUsername(targetUsername)
if (!targetAccount) return ServeError(ctx, 404, "")
if (!targetAccount) {
ServeError(res, 404, "")
return
}
Accounts.password.set(targetAccount.id, password)
Accounts.password.set( targetAccount.id, password )
Authentication.AuthTokens.filter(e => e.account == targetAccount?.id).forEach((accountToken) => {
Authentication.invalidate(accountToken.token)
})
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`, `<b>Hello there!</b> This email is to notify you of a password change that an administrator, <span username>${Account.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()
if (targetAccount.email) {
await 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>${Account.username}</span>, has initiated. You have been logged out of your devices. Thank you for using monofile.`
).catch()
}
)
router.patch(
"/account/:username/elevate",
(req, res) => {
const targetUsername = req.params.username
const targetAccount = Accounts.getFromUsername(targetUsername)
return ctx.text("")
})
if (!targetAccount) {
ServeError(res, 404, "")
return
}
router.patch("/account/:username/elevate", (ctx) => {
const targetUsername = ctx.req.param("username")
const targetAccount = Accounts.getFromUsername(targetUsername)
targetAccount.admin = true
Accounts.save()
res.send()
if (!targetAccount) {
return ServeError(ctx, 404, "")
}
)
router.delete("/account/:username/:deleteFiles",
targetAccount.admin = true
Accounts.save()
return ctx.text("")
})
router.delete(
"/account/:username/:deleteFiles",
requiresAccount,
noAPIAccess,
parser,
(req, res) => {
const targetUsername = req.params.username
const deleteFiles = req.params.deleteFiles
async (ctx) => {
const targetUsername = ctx.req.param("username")
const deleteFiles = ctx.req.param("deleteFiles")
const targetAccount = Accounts.getFromUsername(targetUsername)
if (!targetAccount) {
ServeError(res, 404, "")
return
}
if (!targetAccount) return ServeError(ctx, 404, "")
const accountId = targetAccount.id
Authentication.AuthTokens.filter(e => e.account == accountId).forEach((token) => {
Authentication.AuthTokens.filter(
(e) => e.account == accountId
).forEach((token) => {
Authentication.invalidate(token.token)
})
const deleteAccount = () => Accounts.deleteAccount(accountId).then(_ => res.send("account deleted"))
const deleteAccount = () =>
Accounts.deleteAccount(accountId).then((_) =>
ctx.text("account deleted")
)
if (deleteFiles) {
const Files = targetAccount.files.map(e => e)
const Files = targetAccount.files.map((e) => e)
for (let fileId of Files) {
files.unlink(fileId, true).catch(err => console.error)
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()
await writeFile(
process.cwd() + "/.data/files.json",
JSON.stringify(files.files)
)
return deleteAccount()
} else return deleteAccount()
}
)
return router
}
}

View file

@ -1,98 +1,97 @@
// 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";
import ServeError from "../../../lib/errors";
import { Hono } from "hono"
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`)
const parser = bodyParser.json({
type: [ "type/plain", "application/json" ]
})
const router = new Hono<{
Variables: {
account?: Accounts.Account
}
}>()
const router = Router()
router.use(getAccount)
router.use(getAccount, parser)
module.exports = function(files: Files) {
module.exports = function (files: Files) {
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;
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
await Accounts.save()
res.send("custom css saved")
} else ServeError(res, 400, "invalid fileId")
}
)
router.get('/css',
requiresAccount,
(req, res) => {
const Account = res.locals.acc
requiresPermissions("customize"),
async (ctx) => {
const Account = ctx.get("account") as Accounts.Account
const body = await ctx.req.json()
if (typeof body.fileId != "string") body.fileId = undefined
if (Account?.customCSS) res.redirect(`/file/${Account.customCSS}`)
else res.send("");
}
)
router.put("/embed/color",
requiresAccount, requiresPermissions("customize"),
async (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
!body.fileId ||
(body.fileId.match(id_check_regex) == body.fileId &&
body.fileId.length <= Configuration.maxUploadIdLength)
) {
if (!Account.embed) Account.embed = {};
Account.embed.color = req.body.color || undefined
Account.customCSS = body.fileId || undefined
await Accounts.save()
res.send("custom embed color saved")
} else ServeError(res,400,"invalid hex code")
return ctx.text("custom css saved")
} else return ServeError(ctx, 400, "invalid fileId")
}
)
router.put("/embed/size",
requiresAccount, requiresPermissions("customize"),
async (req, res) => {
const Account = res.locals.acc as Accounts.Account
router.get("/css", requiresAccount, async (ctx) => {
const Account = ctx.get("account")
if (typeof req.body.largeImage != "boolean") {
ServeError(res, 400, "largeImage must be bool");
if (Account?.customCSS)
return ctx.redirect(`/file/${Account.customCSS}`)
else return ctx.text("")
})
router.put(
"/embed/color",
requiresAccount,
requiresPermissions("customize"),
async (ctx) => {
const Account = ctx.get("account") as Accounts.Account
const body = await ctx.req.json()
if (typeof body.color != "string") body.color = undefined
if (
!body.color ||
(body.color.toLowerCase().match(/[a-f0-9]+/) ==
body.color.toLowerCase() &&
body.color.length == 6)
) {
if (!Account.embed) Account.embed = {}
Account.embed.color = body.color || undefined
await Accounts.save()
return ctx.text("custom embed color saved")
} else return ServeError(ctx, 400, "invalid hex code")
}
)
router.put(
"/embed/size",
requiresAccount,
requiresPermissions("customize"),
async (ctx) => {
const Account = ctx.get("account") as Accounts.Account
const body = await ctx.req.json()
if (typeof body.largeImage != "boolean") {
ServeError(ctx, 400, "largeImage must be bool")
return
}
if (!Account.embed) Account.embed = {};
Account.embed.largeImage = req.body.largeImage
if (!Account.embed) Account.embed = {}
Account.embed.largeImage = body.largeImage
await Accounts.save()
res.send(`custom embed image size saved`)
return ctx.text(`custom embed image size saved`)
}
)
return router
}
}

View file

@ -1,8 +1,8 @@
import { Router } from "express";
import { Hono } from "hono";
import Files from "../../../lib/files";
let router = Router()
const router = new Hono()
module.exports = function(files: Files) {
return router
}
}

View file

@ -1,8 +1,8 @@
import { Router } from "express";
import Files from "../../../lib/files";
import { Hono } from "hono"
import Files from "../../../lib/files"
let router = Router()
const router = new Hono()
module.exports = function(files: Files) {
module.exports = function (files: Files) {
return router
}
}