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" "node": ">=v16.11"
}, },
"dependencies": { "dependencies": {
"@hono/node-server": "^1.2.0",
"@types/body-parser": "^1.19.2", "@types/body-parser": "^1.19.2",
"@types/express": "^4.17.14", "@types/express": "^4.17.14",
"@types/multer": "^1.4.7", "@types/multer": "^1.4.7",

View file

@ -5,6 +5,9 @@ settings:
excludeLinksFromLockfile: false excludeLinksFromLockfile: false
dependencies: dependencies:
'@hono/node-server':
specifier: ^1.2.0
version: 1.2.0
'@types/body-parser': '@types/body-parser':
specifier: ^1.19.2 specifier: ^1.19.2
version: 1.19.3 version: 1.19.3
@ -337,6 +340,11 @@ packages:
dev: true dev: true
optional: 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: /@jridgewell/sourcemap-codec@1.4.15:
resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==}
dev: true dev: true

View file

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

View file

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

View file

@ -1,48 +1,36 @@
import { Response } from "express";
import { readFile } from "fs/promises" 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 * @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 code Error code
* @param reason Error reason * @param reason Error reason
*/ */
export default async function ServeError( export default async function ServeError(
res:Response, ctx: Context,
code:number, code: number,
reason:string reason: string
) { ) {
// fetch error page if not cached // fetch error page if not cached
if (!errorPage) { if (!errorPage) {
errorPage = errorPage = (
( (await readFile(`${process.cwd()}/dist/error.html`).catch((err) =>
await readFile(`${process.cwd()}/dist/error.html`) console.error(err)
.catch((err) => console.error(err)) )) || "<pre>$code $text</pre>"
|| "<pre>$code $text</pre>" ).toString()
)
.toString()
} }
// serve error // serve error
res.statusMessage = reason return ctx.html(
res.status(code)
res.header("x-backup-status-message", reason) // glitch default nginx configuration
res.send(
errorPage errorPage
.replaceAll("$code",code.toString()) .replaceAll("$code", code.toString())
.replaceAll("$text",reason) .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 * as Accounts from "./accounts"
import express, { type RequestHandler } from "express" import { Handler as RequestHandler } from "hono"
import ServeError from "../lib/errors"; import ServeError from "../lib/errors"
import * as auth from "./auth"; 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) { export const getAccount: RequestHandler = function (ctx, next) {
res.locals.acc = Accounts.getFromToken(auth.tokenFor(req)) ctx.set("account", Accounts.getFromToken(auth.tokenFor(ctx)!))
next() 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) { export const requiresAccount: RequestHandler = function (ctx, next) {
if (!res.locals.acc) { if (!ctx.get("account")) {
ServeError(res, 401, "not logged in") return ServeError(ctx, 401, "not logged in")
return
} }
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) { export const requiresAdmin: RequestHandler = function (ctx, next) {
if (!res.locals.acc.admin) { if (!ctx.get("account").admin) {
ServeError(res, 403, "you are not an administrator") return ServeError(ctx, 403, "you are not an administrator")
return
} }
next() return next()
} }
/** /**
@ -39,48 +37,58 @@ export const requiresAdmin: RequestHandler = function(_req, res, next) {
* @returns Express middleware * @returns Express middleware
*/ */
export const requiresPermissions = function(...tokenPermissions: auth.TokenPermission[]): RequestHandler { export const requiresPermissions = function (
return function(req, res, next) { ...tokenPermissions: auth.TokenPermission[]
let token = auth.tokenFor(req) ): RequestHandler {
return function (ctx, next) {
let token = auth.tokenFor(ctx)!
let type = auth.getType(token) let type = auth.getType(token)
if (type == "App") { if (type == "App") {
let permissions = auth.getPermissions(token) 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) { for (let v of tokenPermissions) {
if (!permissions.includes(v as auth.TokenPermission)) { if (!permissions.includes(v as auth.TokenPermission)) {
ServeError(res,403,"insufficient permissions") return ServeError(ctx, 403, "insufficient permissions")
return
} }
} }
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) { export const noAPIAccess: RequestHandler = function (ctx, next) {
if (auth.getType(auth.tokenFor(req)) == "App") ServeError(res, 403, "apps are not allowed to access this endpoint") if (auth.getType(auth.tokenFor(ctx)!) == "App")
else next() 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. @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 { export const assertAPI = function (
return function(req, res, next) { condition: (acc: Accounts.Account, token: string) => boolean
let reqToken = auth.tokenFor(req) ): RequestHandler {
if (auth.getType(reqToken) == "App" && condition(res.locals.acc, reqToken)) ServeError(res, 403, "apps are not allowed to access this endpoint") return function (ctx, next) {
else 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 { interface SchemeArray {
type: "array", type: "array"
children: SchemeParameter /* All children of the array must be this type */ children:
| SchemeParameter[] /* Array must match this pattern */ | SchemeParameter /* All children of the array must be this type */
| SchemeParameter[] /* Array must match this pattern */
} }
type SchemeParameter = SchemeType | SchemeObject | SchemeArray 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 { Handler } from "hono"
import { type Account } from "./accounts"
import ServeError from "./errors" import ServeError from "./errors"
interface RatelimitSettings { interface RatelimitSettings {
requests: number requests: number
per: 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 * @param settings Ratelimit settings
* @returns Express middleware * @returns Express middleware
*/ */
export function accountRatelimit( settings: RatelimitSettings ): RequestHandler { export function accountRatelimit(settings: RatelimitSettings): Handler {
let activeLimits: { let activeLimits: {
[ key: string ]: { [key: string]: {
requests: number, requests: number
expirationHold: NodeJS.Timeout expirationHold: NodeJS.Timeout
} }
} = {} } = {}
return (req, res, next) => { return (ctx, next) => {
if (res.locals.acc) { if (ctx.get("account")) {
let accId = res.locals.acc.id let accId = ctx.get("account").id
let aL = activeLimits[accId] let aL = activeLimits[accId]
if (!aL) { if (!aL) {
activeLimits[accId] = { activeLimits[accId] = {
requests: 0, requests: 0,
expirationHold: setTimeout(() => delete activeLimits[accId], settings.per) expirationHold: setTimeout(
() => delete activeLimits[accId],
settings.per
),
} }
aL = activeLimits[accId] aL = activeLimits[accId]
} }
if (aL.requests < settings.requests) { if (aL.requests < settings.requests) {
res.locals.undoCount = () => { ctx.set("undoCount", () => {
if (activeLimits[accId]) { if (activeLimits[accId]) {
activeLimits[accId].requests-- activeLimits[accId].requests--
} }
} })
next() return next()
} else { } 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 bytes from "bytes"
import ServeError from "./lib/errors" import ServeError from "./lib/errors"
import * as Accounts from "./lib/accounts" import * as Accounts from "./lib/accounts"
import type { Handler } from "express" import type { Handler } from "hono"
import type Files from "./lib/files" import type Files from "./lib/files"
const pkg = require(`${process.cwd()}/package.json`) const pkg = require(`${process.cwd()}/package.json`)
export = (files: Files): Handler => export = (files: Files): Handler =>
async (req, res) => { async (ctx) => {
let acc = res.locals.acc as Accounts.Account let acc = ctx.get("account") as Accounts.Account
const file = files.getFilePointer(req.params.fileId) const fileId = ctx.req.param("fileId")
const host = ctx.req.header("Host")
const file = files.getFilePointer(fileId)
if (file) { if (file) {
if (file.visibility == "private" && acc?.id != file.owner) { if (file.visibility == "private" && acc?.id != file.owner) {
ServeError(res, 403, "you do not own this file") return ServeError(ctx, 403, "you do not own this file")
return
} }
const template = await fs const template = await fs
.readFile(process.cwd() + "/dist/download.html", "utf8") .readFile(process.cwd() + "/dist/download.html", "utf8")
.catch(() => { .catch(() => {
throw res.sendStatus(500) throw ctx.status(500)
}) })
let fileOwner = file.owner let fileOwner = file.owner
? Accounts.getFromId(file.owner) ? Accounts.getFromId(file.owner)
: undefined : undefined
res.send( return ctx.html(
template template
.replaceAll("$FileId", req.params.fileId) .replaceAll("$FileId", fileId)
.replaceAll("$Version", pkg.version) .replaceAll("$Version", pkg.version)
.replaceAll( .replaceAll(
"$FileSize", "$FileSize",
@ -44,18 +45,14 @@ export = (files: Files): Handler =>
.replace( .replace(
"<!--metaTags-->", "<!--metaTags-->",
(file.mime.startsWith("image/") (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/") : file.mime.startsWith("video/")
? `<meta property="og:video:url" content="https://${ ? `<meta property="og:video:url" content="https://${host}/cpt/${fileId}/video.${
req.headers.host
}/cpt/${req.params.fileId}/video.${
file.mime.split("/")[1] == "quicktime" file.mime.split("/")[1] == "quicktime"
? "mov" ? "mov"
: file.mime.split("/")[1] : file.mime.split("/")[1]
}" /> }" />
<meta property="og:video:secure_url" content="https://${ <meta property="og:video:secure_url" content="https://${host}/cpt/${fileId}/video.${
req.headers.host
}/cpt/${req.params.fileId}/video.${
file.mime.split("/")[1] == "quicktime" file.mime.split("/")[1] == "quicktime"
? "mov" ? "mov"
: file.mime.split("/")[1] : file.mime.split("/")[1]
@ -79,7 +76,7 @@ export = (files: Files): Handler =>
`\n<meta name="theme-color" content="${ `\n<meta name="theme-color" content="${
fileOwner?.embed?.color && fileOwner?.embed?.color &&
file.visibility != "anonymous" && file.visibility != "anonymous" &&
(req.headers["user-agent"] || "").includes( (ctx.req.header("user-agent") || "").includes(
"Discordbot" "Discordbot"
) )
? `#${fileOwner.embed.color}` ? `#${fileOwner.embed.color}`
@ -89,11 +86,11 @@ export = (files: Files): Handler =>
.replace( .replace(
"<!--preview-->", "<!--preview-->",
file.mime.startsWith("image/") 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/") : 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/") : 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( .replaceAll(
@ -104,6 +101,6 @@ export = (files: Files): Handler =>
) )
) )
} else { } else {
ServeError(res, 404, "file not found") ServeError(ctx, 404, "file not found")
} }
} }

View file

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

View file

@ -1,20 +1,21 @@
import bodyParser from "body-parser"; import { Hono } from "hono"
import { Router } from "express"; import * as Accounts from "../../../lib/accounts"
import * as Accounts from "../../../lib/accounts"; import * as auth from "../../../lib/auth"
import * as auth from "../../../lib/auth"; import { writeFile } from "fs/promises"
import bytes from "bytes" import { sendMail } from "../../../lib/mail"
import {writeFile} from "fs"; import {
import { sendMail } from "../../../lib/mail"; getAccount,
import { getAccount, requiresAccount, requiresAdmin, requiresPermissions } from "../../../lib/middleware" requiresAccount,
requiresAdmin,
requiresPermissions,
} from "../../../lib/middleware"
import Files from "../../../lib/files"
import ServeError from "../../../lib/errors"; export let adminRoutes = new Hono<{
import Files from "../../../lib/files"; Variables: {
account: Accounts.Account
let parser = bodyParser.json({ }
type: ["text/plain","application/json"] }>()
})
export let adminRoutes = Router();
adminRoutes adminRoutes
.use(getAccount) .use(getAccount)
.use(requiresAccount) .use(requiresAccount)
@ -23,214 +24,198 @@ adminRoutes
let config = require(`${process.cwd()}/config.json`) 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) => { if (
typeof body.target !== "string" ||
let acc = res.locals.acc as Accounts.Account typeof body.password !== "string"
) {
if (typeof req.body.target !== "string" || typeof req.body.password !== "string") { return ctx.status(404)
res.status(404)
res.send()
return
} }
let targetAccount = Accounts.getFromUsername(req.body.target) let targetAccount = Accounts.getFromUsername(body.target)
if (!targetAccount) { if (!targetAccount) {
res.status(404) return ctx.status(404)
res.send()
return
} }
Accounts.password.set ( targetAccount.id, req.body.password ) Accounts.password.set(targetAccount.id, body.password)
auth.AuthTokens.filter(e => e.account == targetAccount?.id).forEach((v) => { auth.AuthTokens.filter((e) => e.account == targetAccount?.id).forEach(
auth.invalidate(v.token) (v) => {
}) auth.invalidate(v.token)
}
)
if (targetAccount.email) { 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(() => { return sendMail(
res.send("OK") targetAccount.email,
}).catch((err) => {}) `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 body.target !== "string") {
return ctx.status(404)
if (typeof req.body.target !== "string") {
res.status(404)
res.send()
return
} }
let targetAccount = Accounts.getFromUsername(req.body.target) let targetAccount = Accounts.getFromUsername(body.target)
if (!targetAccount) { if (!targetAccount) {
res.status(404) return ctx.status(404)
res.send()
return
} }
targetAccount.admin = true;
Accounts.save() Accounts.save()
res.send() return ctx.text("OK")
}) })
adminRoutes.post("/delete", parser, (req,res) => { adminRoutes.post("/delete", async (ctx) => {
const body = await ctx.req.json()
if (typeof req.body.target !== "string") { if (typeof body.target !== "string") {
res.status(404) return ctx.status(404)
res.send()
return
} }
let targetFile = files.getFilePointer(req.body.target) let targetFile = files.getFilePointer(body.target)
if (!targetFile) { if (!targetFile) {
res.status(404) return ctx.status(404)
res.send()
return
} }
files.unlink(req.body.target).then(() => { return files
res.status(200) .unlink(body.target)
}).catch(() => { .then(() => ctx.status(200))
res.status(500) .catch(() => ctx.status(500))
}).finally(() => res.send()) .finally(() => ctx.status(200))
}) })
adminRoutes.post("/delete_account", parser, async (req,res) => { adminRoutes.post("/delete_account", async (ctx) => {
let acc = ctx.get("account") as Accounts.Account
let acc = res.locals.acc as Accounts.Account const body = await ctx.req.json()
if (typeof body.target !== "string") {
if (typeof req.body.target !== "string") { return ctx.status(404)
res.status(404)
res.send()
return
} }
let targetAccount = Accounts.getFromUsername(req.body.target) let targetAccount = Accounts.getFromUsername(body.target)
if (!targetAccount) { if (!targetAccount) {
res.status(404) return ctx.status(404)
res.send()
return
} }
let accId = targetAccount.id 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) auth.invalidate(v.token)
}) })
let cpl = () => Accounts.deleteAccount(accId).then(_ => { let cpl = () =>
if (targetAccount?.email) { Accounts.deleteAccount(accId).then((_) => {
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.`) if (targetAccount?.email) {
} sendMail(
res.send("account deleted") targetAccount.email,
}) "Notice of account deletion",
`Your account, <span username>${
if (req.body.deleteFiles) { targetAccount.username
let f = targetAccount.files.map(e=>e) // make shallow copy so that iterating over it doesnt Die }</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) { 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) => { return writeFile(
if (err) console.log(err) process.cwd() + "/.data/files.json",
cpl() JSON.stringify(files.files)
}) ).then(cpl)
} else cpl() } else return cpl()
}) })
adminRoutes.post("/transfer", parser, (req,res) => { adminRoutes.post("/transfer", async (ctx) => {
const body = await ctx.req.json()
if (typeof req.body.target !== "string" || typeof req.body.owner !== "string") { if (typeof body.target !== "string" || typeof body.owner !== "string") {
res.status(404) return ctx.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 || "") let targetFile = files.getFilePointer(body.target)
if (!targetFile) {
return ctx.status(404)
}
let newOwner = Accounts.getFromUsername(body.owner || "")
// clear old owner // clear old owner
if (targetFile.owner) { if (targetFile.owner) {
let oldOwner = Accounts.getFromId(targetFile.owner) let oldOwner = Accounts.getFromId(targetFile.owner)
if (oldOwner) { if (oldOwner) {
Accounts.files.deindex(oldOwner.id, req.body.target) Accounts.files.deindex(oldOwner.id, body.target)
} }
} }
if (newOwner) { if (newOwner) {
Accounts.files.index(newOwner.id, req.body.target) Accounts.files.index(newOwner.id, body.target)
} }
targetFile.owner = newOwner ? newOwner.id : undefined; 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
files
.writeFile(body.target, targetFile)
.then(() => ctx.status(200))
.catch(() => ctx.status(500))
}) })
adminRoutes.post("/idchange", parser, (req,res) => { adminRoutes.post("/idchange", async (ctx) => {
const body = await ctx.req.json()
if (typeof req.body.target !== "string" || typeof req.body.new !== "string") { if (typeof body.target !== "string" || typeof body.new !== "string") {
res.status(400) return ctx.status(400)
res.send()
return
} }
let targetFile = files.getFilePointer(req.body.target) let targetFile = files.getFilePointer(body.target)
if (!targetFile) { if (!targetFile) {
res.status(404) return ctx.status(404)
res.send()
return
} }
if (files.getFilePointer(req.body.new)) { if (files.getFilePointer(body.new)) {
res.status(400) return ctx.status(400)
res.send()
return
} }
if (targetFile.owner) { if (targetFile.owner) {
Accounts.files.deindex(targetFile.owner, req.body.target) Accounts.files.deindex(targetFile.owner, body.target)
Accounts.files.index(targetFile.owner, req.body.new) 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(() => { return files
res.send() .writeFile(body.new, targetFile)
}).catch(() => { .then(() => ctx.status(200))
files.files[req.body.target] = req.body.new .catch(() => {
files.files[body.target] = body.new
if (targetFile.owner) { if (targetFile.owner) {
Accounts.files.deindex(targetFile.owner, req.body.new) Accounts.files.deindex(targetFile.owner, body.new)
Accounts.files.index(targetFile.owner, req.body.target) Accounts.files.index(targetFile.owner, body.target)
} }
res.status(500)
res.send()
})
return ctx.status(500)
})
}) })
return adminRoutes return adminRoutes
} }

View file

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

View file

@ -1,98 +1,129 @@
import bodyParser from "body-parser"; import { Hono } from "hono"
import { Router } from "express"; import * as Accounts from "../../../lib/accounts"
import * as Accounts from "../../../lib/accounts"; import { writeFile } from "fs/promises"
import * as auth from "../../../lib/auth"; import Files from "../../../lib/files"
import bytes from "bytes" import {
import {writeFile} from "fs"; getAccount,
requiresAccount,
requiresPermissions,
} from "../../../lib/middleware"
import ServeError from "../../../lib/errors"; export let fileApiRoutes = new Hono<{
import Files from "../../../lib/files"; Variables: {
import { getAccount, requiresAccount, requiresPermissions } from "../../../lib/middleware"; account: Accounts.Account
}
let parser = bodyParser.json({ }>()
type: ["text/plain","application/json"]
})
export let fileApiRoutes = Router();
let config = require(`${process.cwd()}/config.json`) 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 ctx.json(
acc.files
if (!acc) return .map((e) => {
let accId = acc.id 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) => { fileApiRoutes.post(
let fp = files.getFilePointer(e) "/manage",
if (!fp) { Accounts.files.deindex(accId, e); return null } requiresPermissions("manage"),
return { async (ctx) => {
...fp, let acc = ctx.get("account") as Accounts.Account
messageids: null, const body = await ctx.req.json()
owner: null, if (!acc) return
id:e if (
} !body.target ||
}).filter(e=>e)) !(typeof body.target == "object") ||
body.target.length < 1
}) )
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 return
}
switch( req.body.action ) { let modified = 0
case "delete":
files.unlink(e, true)
modified++;
break;
case "changeFileVisibility": body.target.forEach((e: string) => {
if (!["public","anonymous","private"].includes(req.body.value)) return; if (!acc.files.includes(e)) return
files.files[e].visibility = req.body.value;
modified++;
break;
case "setTag": let fp = files.getFilePointer(e)
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(() => { if (fp.reserved) {
writeFile(process.cwd()+"/.data/files.json",JSON.stringify(files.files), (err) => { return
if (err) console.log(err) }
res.contentType("text/plain")
res.send(`modified ${modified} files`) 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 return fileApiRoutes
} }

View file

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

View file

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

View file

@ -1,120 +1,119 @@
// Modules // Modules
import { writeFile } from 'fs' import { writeFile } from "fs/promises"
import { Router } from "express"; import { Hono } from "hono"
import bodyParser from "body-parser";
// Libs // Libs
import Files, { id_check_regex } from "../../../lib/files"; import Files, { id_check_regex } from "../../../lib/files"
import * as Accounts from '../../../lib/accounts' import * as Accounts from "../../../lib/accounts"
import * as Authentication from '../../../lib/auth' import * as Authentication from "../../../lib/auth"
import { assertAPI, getAccount, noAPIAccess, requiresAccount, requiresAdmin, requiresPermissions } from "../../../lib/middleware"; import {
import ServeError from "../../../lib/errors"; getAccount,
import { sendMail } from '../../../lib/mail'; noAPIAccess,
requiresAccount,
requiresAdmin,
} from "../../../lib/middleware"
import ServeError from "../../../lib/errors"
import { sendMail } from "../../../lib/mail"
const Configuration = require(`${process.cwd()}/config.json`) const Configuration = require(`${process.cwd()}/config.json`)
const parser = bodyParser.json({ const router = new Hono<{
type: [ "type/plain", "application/json" ] 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) { const targetUsername = ctx.req.param("username")
router.patch( const password = body.password
"/account/:username/password",
(req, res) => {
const Account = res.locals.acc as Accounts.Account
const targetUsername = req.params.username if (typeof password !== "string") return ServeError(ctx, 404, "")
const password = req.body.password
if (typeof password !== "string") { const targetAccount = Accounts.getFromUsername(targetUsername)
ServeError(res, 404, "")
return
}
const targetAccount = Accounts.getFromUsername(targetUsername) if (!targetAccount) return ServeError(ctx, 404, "")
if (!targetAccount) { Accounts.password.set(targetAccount.id, password)
ServeError(res, 404, "")
return
}
Accounts.password.set( targetAccount.id, password ) Authentication.AuthTokens.filter(
(e) => e.account == targetAccount?.id
Authentication.AuthTokens.filter(e => e.account == targetAccount?.id).forEach((accountToken) => { ).forEach((accountToken) => {
Authentication.invalidate(accountToken.token) Authentication.invalidate(accountToken.token)
}) })
if (targetAccount.email) { 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(() => { await sendMail(
res.send("OK") targetAccount.email,
}).catch((err) => {}) `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()
res.send()
} }
)
router.patch( return ctx.text("")
"/account/:username/elevate", })
(req, res) => {
const targetUsername = req.params.username
const targetAccount = Accounts.getFromUsername(targetUsername)
if (!targetAccount) { router.patch("/account/:username/elevate", (ctx) => {
ServeError(res, 404, "") const targetUsername = ctx.req.param("username")
return const targetAccount = Accounts.getFromUsername(targetUsername)
}
targetAccount.admin = true if (!targetAccount) {
Accounts.save() return ServeError(ctx, 404, "")
res.send()
} }
)
router.delete("/account/:username/:deleteFiles", targetAccount.admin = true
Accounts.save()
return ctx.text("")
})
router.delete(
"/account/:username/:deleteFiles",
requiresAccount, requiresAccount,
noAPIAccess, noAPIAccess,
parser, async (ctx) => {
(req, res) => { const targetUsername = ctx.req.param("username")
const targetUsername = req.params.username const deleteFiles = ctx.req.param("deleteFiles")
const deleteFiles = req.params.deleteFiles
const targetAccount = Accounts.getFromUsername(targetUsername) const targetAccount = Accounts.getFromUsername(targetUsername)
if (!targetAccount) { if (!targetAccount) return ServeError(ctx, 404, "")
ServeError(res, 404, "")
return
}
const accountId = targetAccount.id 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) 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) { if (deleteFiles) {
const Files = targetAccount.files.map(e => e) const Files = targetAccount.files.map((e) => e)
for (let fileId of Files) { 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) => { await writeFile(
if (err) console.log(err) process.cwd() + "/.data/files.json",
deleteAccount() JSON.stringify(files.files)
}) )
} else deleteAccount() return deleteAccount()
} else return deleteAccount()
} }
) )
return router return router
} }

View file

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

View file

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

View file

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