From 0366c91f74c5287bb9fedc2c6279fee524f180e2 Mon Sep 17 00:00:00 2001 From: "Jack W." Date: Tue, 24 Oct 2023 19:59:00 -0400 Subject: [PATCH] refactor: :recycle: Honofile. --- package.json | 1 + pnpm-lock.yaml | 8 + src/server/index.ts | 88 ++- src/server/lib/auth.ts | 98 +-- src/server/lib/errors.ts | 48 +- src/server/lib/middleware.ts | 109 ++- src/server/lib/ratelimit.ts | 36 +- src/server/preview.ts | 39 +- src/server/routes/api.ts | 56 +- src/server/routes/api/v0/adminRoutes.ts | 297 ++++---- src/server/routes/api/v0/authRoutes.ts | 816 +++++++++++++--------- src/server/routes/api/v0/fileApiRoutes.ts | 191 ++--- src/server/routes/api/v0/primaryApi.ts | 383 +++++----- src/server/routes/api/v1/account.ts | 359 +++++----- src/server/routes/api/v1/admin.ts | 151 ++-- src/server/routes/api/v1/customization.ts | 143 ++-- src/server/routes/api/v1/file.ts | 6 +- src/server/routes/api/v1/public.ts | 10 +- 18 files changed, 1531 insertions(+), 1308 deletions(-) diff --git a/package.json b/package.json index fd6ebee..af58dc0 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "node": ">=v16.11" }, "dependencies": { + "@hono/node-server": "^1.2.0", "@types/body-parser": "^1.19.2", "@types/express": "^4.17.14", "@types/multer": "^1.4.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d7134fe..8ffa49c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,6 +5,9 @@ settings: excludeLinksFromLockfile: false dependencies: + '@hono/node-server': + specifier: ^1.2.0 + version: 1.2.0 '@types/body-parser': specifier: ^1.19.2 version: 1.19.3 @@ -337,6 +340,11 @@ packages: dev: true optional: true + /@hono/node-server@1.2.0: + resolution: {integrity: sha512-aHT8lDMLpd7ioXJ1/057+h+oE/k7rCOWmjklYDsE0jE4CoNB9XzG4f8dRHvw4s5HJFocaYDiGgYM/V0kYbQ0ww==} + engines: {node: '>=18.0.0'} + dev: false + /@jridgewell/sourcemap-codec@1.4.15: resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} dev: true diff --git a/src/server/index.ts b/src/server/index.ts index ee0e4fe..ba4251c 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -1,45 +1,63 @@ -import cookieParser from "cookie-parser" import { IntentsBitField, Client } from "discord.js" -import express from "express" +import { serve } from "@hono/node-server" +import { serveStatic } from "@hono/node-server/serve-static" +import { Hono } from "hono" import fs from "fs" import Files from "./lib/files" import { getAccount } from "./lib/middleware" - import APIRouter from "./routes/api" import preview from "./preview" require("dotenv").config() const pkg = require(`${process.cwd()}/package.json`) -let app = express() +const app = new Hono() let config = require(`${process.cwd()}/config.json`) -app.use("/static/assets", express.static("assets")) -app.use("/static/vite", express.static("dist/static/vite")) +app.get( + "/static/assets/*", + serveStatic({ + rewriteRequestPath: (path) => { + return path.replace("/static/assets", "/assets") + }, + }) +) +app.get( + "/static/vite/*", + serveStatic({ + rewriteRequestPath: (path) => { + return path.replace("/static/vite", "/dist/static/vite") + }, + }) +) //app.use(bodyParser.text({limit:(config.maxDiscordFileSize*config.maxDiscordFiles)+1048576,type:["application/json","text/plain"]})) -app.use(cookieParser()) - // check for ssl, if not redirect -if (config.trustProxy) app.enable("trust proxy") +if (config.trustProxy) { + // app.enable("trust proxy") +} if (config.forceSSL) { - app.use((req, res, next) => { - if (req.protocol == "http") - res.redirect(`https://${req.get("host")}${req.originalUrl}`) - else next() + app.use(async (ctx, next) => { + if (new URL(ctx.req.url).protocol == "http") { + return ctx.redirect( + `https://${ctx.req.header("host")}${ + new URL(ctx.req.url).pathname + }` + ) + } else { + return next() + } }) } -app.get("/server", (req, res) => { - res.send( - JSON.stringify({ - ...config, - version: pkg.version, - files: Object.keys(files.files).length, - }) - ) -}) +app.get("/server", (ctx) => + ctx.json({ + ...config, + version: pkg.version, + files: Object.keys(files.files).length, + }) +) // funcs @@ -60,17 +78,19 @@ let client = new Client({ let files = new Files(client, config) -let apiRouter = new APIRouter(files) +const apiRouter = new APIRouter(files) apiRouter.loadAPIMethods().then(() => { - app.use(apiRouter.root) + app.route("/", apiRouter.root) console.log("API OK!") }) // index, clone -app.get("/", function (req, res) { - res.sendFile(process.cwd() + "/dist/index.html") -}) +app.get("/", async (ctx) => + ctx.html( + await fs.promises.readFile(process.cwd() + "/dist/index.html", "utf-8") + ) +) // serve download page @@ -87,8 +107,16 @@ app.get("/download/:fileId", getAccount, preview(files)) // listen on 3000 or MONOFILE_PORT -app.listen(process.env.MONOFILE_PORT || 3000, function () { - console.log("Web OK!") -}) +serve( + { + fetch: app.fetch, + port: Number(process.env.MONOFILE_PORT || 3000), + }, + (info) => { + console.log("Web OK!", info.port, info.address) + } +) client.login(process.env.TOKEN) + +export = app diff --git a/src/server/lib/auth.ts b/src/server/lib/auth.ts index 75a4fd8..783144e 100644 --- a/src/server/lib/auth.ts +++ b/src/server/lib/auth.ts @@ -1,48 +1,50 @@ import crypto from "crypto" -import express from "express" +import { getCookie } from "hono/cookie" +import type { Context } from "hono" import { readFile, writeFile } from "fs/promises" export let AuthTokens: AuthToken[] = [] -export let AuthTokenTO:{[key:string]:NodeJS.Timeout} = {} +export let AuthTokenTO: { [key: string]: NodeJS.Timeout } = {} export const ValidTokenPermissions = [ - "user", // permissions to /auth/me, with email docked - "email", // adds email back to /auth/me - "private", // allows app to read private files - "upload", // allows an app to upload under an account - "manage", // allows an app to manage an account's files + "user", // permissions to /auth/me, with email docked + "email", // adds email back to /auth/me + "private", // allows app to read private files + "upload", // allows an app to upload under an account + "manage", // allows an app to manage an account's files "customize", // allows an app to change customization settings - "admin" // only available for accounts with admin - // gives an app access to all admin tools + "admin", // only available for accounts with admin + // gives an app access to all admin tools ] as const export type TokenType = "User" | "App" -export type TokenPermission = typeof ValidTokenPermissions[number] +export type TokenPermission = (typeof ValidTokenPermissions)[number] export interface AuthToken { - account: string, - token: string, - expire: number, + account: string + token: string + expire: number - type?: TokenType, // if !type, assume User + type?: TokenType // if !type, assume User tokenPermissions?: TokenPermission[] // default to user if type is App, - // give full permissions if type is User + // give full permissions if type is User } export function create( - id:string, - expire:number=(24*60*60*1000), - type:TokenType="User", - tokenPermissions?:TokenPermission[] + id: string, + expire: number = 24 * 60 * 60 * 1000, + type: TokenType = "User", + tokenPermissions?: TokenPermission[] ) { let token = { - account:id, - token:crypto.randomBytes(36).toString('hex'), - expire: expire ? Date.now()+expire : 0, + account: id, + token: crypto.randomBytes(36).toString("hex"), + expire: expire ? Date.now() + expire : 0, type, - tokenPermissions: type == "App" ? tokenPermissions || ["user"] : undefined + tokenPermissions: + type == "App" ? tokenPermissions || ["user"] : undefined, } - + AuthTokens.push(token) tokenTimer(token) @@ -51,56 +53,68 @@ export function create( return token.token } -export function tokenFor(req: express.Request) { - return req.cookies.auth || ( - req.header("authorization")?.startsWith("Bearer ") - ? req.header("authorization")?.split(" ")[1] - : undefined +export function tokenFor(ctx: Context) { + return ( + getCookie(ctx, "auth") || + (ctx.req.header("authorization")?.startsWith("Bearer ") + ? ctx.req.header("authorization")?.split(" ")[1] + : undefined) ) } -function getToken(token:string) { - return AuthTokens.find(e => e.token == token && (e.expire == 0 || Date.now() < e.expire)) +function getToken(token: string) { + return AuthTokens.find( + (e) => e.token == token && (e.expire == 0 || Date.now() < e.expire) + ) } -export function validate(token:string) { +export function validate(token: string) { return getToken(token)?.account } -export function getType(token:string): TokenType | undefined { +export function getType(token: string): TokenType | undefined { return getToken(token)?.type } -export function getPermissions(token:string): TokenPermission[] | undefined { +export function getPermissions(token: string): TokenPermission[] | undefined { return getToken(token)?.tokenPermissions } -export function tokenTimer(token:AuthToken) { +export function tokenTimer(token: AuthToken) { if (!token.expire) return // justincase if (Date.now() >= token.expire) { invalidate(token.token) return } - AuthTokenTO[token.token] = setTimeout(() => invalidate(token.token),token.expire-Date.now()) + AuthTokenTO[token.token] = setTimeout( + () => invalidate(token.token), + token.expire - Date.now() + ) } -export function invalidate(token:string) { +export function invalidate(token: string) { if (AuthTokenTO[token]) { clearTimeout(AuthTokenTO[token]) } - AuthTokens.splice(AuthTokens.findIndex(e => e.token == token),1) + AuthTokens.splice( + AuthTokens.findIndex((e) => e.token == token), + 1 + ) save() } export function save() { - writeFile(`${process.cwd()}/.data/tokens.json`,JSON.stringify(AuthTokens)) - .catch((err) => console.error(err)) + writeFile( + `${process.cwd()}/.data/tokens.json`, + JSON.stringify(AuthTokens) + ).catch((err) => console.error(err)) } readFile(`${process.cwd()}/.data/tokens.json`) .then((buf) => { AuthTokens = JSON.parse(buf.toString()) - AuthTokens.forEach(e => tokenTimer(e)) - }).catch(err => console.error(err)) \ No newline at end of file + AuthTokens.forEach((e) => tokenTimer(e)) + }) + .catch((err) => console.error(err)) diff --git a/src/server/lib/errors.ts b/src/server/lib/errors.ts index c8eddd3..261d4d4 100644 --- a/src/server/lib/errors.ts +++ b/src/server/lib/errors.ts @@ -1,48 +1,36 @@ -import { Response } from "express"; import { readFile } from "fs/promises" +import type { Context } from "hono" -let errorPage:string +let errorPage: string /** * @description Serves an error as a response to a request with an error page attached - * @param res Express response object + * @param ctx Express response object * @param code Error code * @param reason Error reason */ export default async function ServeError( - res:Response, - code:number, - reason:string + ctx: Context, + code: number, + reason: string ) { // fetch error page if not cached if (!errorPage) { - errorPage = - ( - await readFile(`${process.cwd()}/dist/error.html`) - .catch((err) => console.error(err)) - || "
$code $text
" - ) - .toString() + errorPage = ( + (await readFile(`${process.cwd()}/dist/error.html`).catch((err) => + console.error(err) + )) || "
$code $text
" + ).toString() } // serve error - res.statusMessage = reason - res.status(code) - res.header("x-backup-status-message", reason) // glitch default nginx configuration - res.send( + return ctx.html( errorPage - .replaceAll("$code",code.toString()) - .replaceAll("$text",reason) + .replaceAll("$code", code.toString()) + .replaceAll("$text", reason), + code, + { + "x-backup-status-message": reason, // glitch default nginx configuration + } ) } -/** - * @description Redirects a user to another page. - * @param res Express response object - * @param url Target URL - * @deprecated Use `res.redirect` instead. - */ -export function Redirect(res:Response,url:string) { - res.status(302) - res.header("Location",url) - res.send() -} diff --git a/src/server/lib/middleware.ts b/src/server/lib/middleware.ts index e9c18d2..d666410 100644 --- a/src/server/lib/middleware.ts +++ b/src/server/lib/middleware.ts @@ -1,36 +1,34 @@ -import * as Accounts from "./accounts"; -import express, { type RequestHandler } from "express" -import ServeError from "../lib/errors"; -import * as auth from "./auth"; +import * as Accounts from "./accounts" +import { Handler as RequestHandler } from "hono" +import ServeError from "../lib/errors" +import * as auth from "./auth" /** - * @description Middleware which adds an account, if any, to res.locals.acc + * @description Middleware which adds an account, if any, to ctx.get("account") */ -export const getAccount: RequestHandler = function(req, res, next) { - res.locals.acc = Accounts.getFromToken(auth.tokenFor(req)) - next() +export const getAccount: RequestHandler = function (ctx, next) { + ctx.set("account", Accounts.getFromToken(auth.tokenFor(ctx)!)) + return next() } /** - * @description Middleware which blocks requests which do not have res.locals.acc set + * @description Middleware which blocks requests which do not have ctx.get("account") set */ -export const requiresAccount: RequestHandler = function(_req, res, next) { - if (!res.locals.acc) { - ServeError(res, 401, "not logged in") - return +export const requiresAccount: RequestHandler = function (ctx, next) { + if (!ctx.get("account")) { + return ServeError(ctx, 401, "not logged in") } - next() + return next() } /** - * @description Middleware which blocks requests that have res.locals.acc.admin set to a falsy value + * @description Middleware which blocks requests that have ctx.get("account").admin set to a falsy value */ -export const requiresAdmin: RequestHandler = function(_req, res, next) { - if (!res.locals.acc.admin) { - ServeError(res, 403, "you are not an administrator") - return +export const requiresAdmin: RequestHandler = function (ctx, next) { + if (!ctx.get("account").admin) { + return ServeError(ctx, 403, "you are not an administrator") } - next() + return next() } /** @@ -39,48 +37,58 @@ export const requiresAdmin: RequestHandler = function(_req, res, next) { * @returns Express middleware */ -export const requiresPermissions = function(...tokenPermissions: auth.TokenPermission[]): RequestHandler { - return function(req, res, next) { - let token = auth.tokenFor(req) +export const requiresPermissions = function ( + ...tokenPermissions: auth.TokenPermission[] +): RequestHandler { + return function (ctx, next) { + let token = auth.tokenFor(ctx)! let type = auth.getType(token) - + if (type == "App") { let permissions = auth.getPermissions(token) - - if (!permissions) ServeError(res, 403, "insufficient permissions") - else { + if (!permissions) return ServeError(ctx, 403, "insufficient permissions") + else { for (let v of tokenPermissions) { if (!permissions.includes(v as auth.TokenPermission)) { - ServeError(res,403,"insufficient permissions") - return + return ServeError(ctx, 403, "insufficient permissions") } } - next() - + return next() } - } else next() + } else return next() } } /** - * @description Blocks requests based on whether or not the token being used to access the route is of type `User`. + * @description Blocks requests based on whether or not the token being used to access the route is of type `User`. */ -export const noAPIAccess: RequestHandler = function(req, res, next) { - if (auth.getType(auth.tokenFor(req)) == "App") ServeError(res, 403, "apps are not allowed to access this endpoint") - else next() +export const noAPIAccess: RequestHandler = function (ctx, next) { + if (auth.getType(auth.tokenFor(ctx)!) == "App") + return ServeError(ctx, 403, "apps are not allowed to access this endpoint") + else return next() } /** @description Add a restriction to this route; the condition must be true to allow API requests. */ -export const assertAPI = function(condition: (acc:Accounts.Account, token:string) => boolean):RequestHandler { - return function(req, res, next) { - let reqToken = auth.tokenFor(req) - if (auth.getType(reqToken) == "App" && condition(res.locals.acc, reqToken)) ServeError(res, 403, "apps are not allowed to access this endpoint") - else next() +export const assertAPI = function ( + condition: (acc: Accounts.Account, token: string) => boolean +): RequestHandler { + return function (ctx, next) { + let reqToken = auth.tokenFor(ctx)! + if ( + auth.getType(reqToken) == "App" && + condition(ctx.get("account"), reqToken) + ) + return ServeError( + ctx, + 403, + "apps are not allowed to access this endpoint" + ) + else return next() } } @@ -94,21 +102,10 @@ interface SchemeObject { } interface SchemeArray { - type: "array", - children: SchemeParameter /* All children of the array must be this type */ - | SchemeParameter[] /* Array must match this pattern */ + type: "array" + children: + | SchemeParameter /* All children of the array must be this type */ + | SchemeParameter[] /* Array must match this pattern */ } type SchemeParameter = SchemeType | SchemeObject | SchemeArray - -/** - * @description Blocks requests based on whether or not the token being used to access the route is of type `User` unless a condition is met. - * @param tokenPermissions Permissions which your route requires. - * @returns Express middleware - */ - -export const sanitize = function(scheme: SchemeObject):RequestHandler { - return function(req, res, next) { - - } -} \ No newline at end of file diff --git a/src/server/lib/ratelimit.ts b/src/server/lib/ratelimit.ts index 94d9d32..afc1344 100644 --- a/src/server/lib/ratelimit.ts +++ b/src/server/lib/ratelimit.ts @@ -1,50 +1,50 @@ -import { RequestHandler } from "express" -import { type Account } from "./accounts" +import type { Handler } from "hono" import ServeError from "./errors" interface RatelimitSettings { - requests: number per: number - } /** - * @description Ratelimits a route based on res.locals.acc + * @description Ratelimits a route based on ctx.get("account") * @param settings Ratelimit settings * @returns Express middleware */ -export function accountRatelimit( settings: RatelimitSettings ): RequestHandler { +export function accountRatelimit(settings: RatelimitSettings): Handler { let activeLimits: { - [ key: string ]: { - requests: number, + [key: string]: { + requests: number expirationHold: NodeJS.Timeout } } = {} - return (req, res, next) => { - if (res.locals.acc) { - let accId = res.locals.acc.id + return (ctx, next) => { + if (ctx.get("account")) { + let accId = ctx.get("account").id let aL = activeLimits[accId] - + if (!aL) { activeLimits[accId] = { requests: 0, - expirationHold: setTimeout(() => delete activeLimits[accId], settings.per) + expirationHold: setTimeout( + () => delete activeLimits[accId], + settings.per + ), } aL = activeLimits[accId] } if (aL.requests < settings.requests) { - res.locals.undoCount = () => { + ctx.set("undoCount", () => { if (activeLimits[accId]) { activeLimits[accId].requests-- } - } - next() + }) + return next() } else { - ServeError(res, 429, "too many requests") + return ServeError(ctx, 429, "too many requests") } } } -} \ No newline at end of file +} diff --git a/src/server/preview.ts b/src/server/preview.ts index 31829c6..90c3cad 100644 --- a/src/server/preview.ts +++ b/src/server/preview.ts @@ -2,31 +2,32 @@ import fs from "fs/promises" import bytes from "bytes" import ServeError from "./lib/errors" import * as Accounts from "./lib/accounts" -import type { Handler } from "express" +import type { Handler } from "hono" import type Files from "./lib/files" const pkg = require(`${process.cwd()}/package.json`) export = (files: Files): Handler => - async (req, res) => { - let acc = res.locals.acc as Accounts.Account - const file = files.getFilePointer(req.params.fileId) + async (ctx) => { + let acc = ctx.get("account") as Accounts.Account + const fileId = ctx.req.param("fileId") + const host = ctx.req.header("Host") + const file = files.getFilePointer(fileId) if (file) { if (file.visibility == "private" && acc?.id != file.owner) { - ServeError(res, 403, "you do not own this file") - return + return ServeError(ctx, 403, "you do not own this file") } const template = await fs .readFile(process.cwd() + "/dist/download.html", "utf8") .catch(() => { - throw res.sendStatus(500) + throw ctx.status(500) }) let fileOwner = file.owner ? Accounts.getFromId(file.owner) : undefined - res.send( + return ctx.html( template - .replaceAll("$FileId", req.params.fileId) + .replaceAll("$FileId", fileId) .replaceAll("$Version", pkg.version) .replaceAll( "$FileSize", @@ -44,18 +45,14 @@ export = (files: Files): Handler => .replace( "", (file.mime.startsWith("image/") - ? `` + ? `` : file.mime.startsWith("video/") - ? ` - `\n .replace( "", file.mime.startsWith("image/") - ? `
` + ? `
` : file.mime.startsWith("video/") - ? `
` + ? `
` : file.mime.startsWith("audio/") - ? `
` + ? `
` : "" ) .replaceAll( @@ -104,6 +101,6 @@ export = (files: Files): Handler => ) ) } else { - ServeError(res, 404, "file not found") + ServeError(ctx, 404, "file not found") } } diff --git a/src/server/routes/api.ts b/src/server/routes/api.ts index 1374d7d..9ce21ee 100644 --- a/src/server/routes/api.ts +++ b/src/server/routes/api.ts @@ -1,8 +1,8 @@ -import { Router } from "express"; -import { readFile, readdir } from "fs/promises"; -import Files from "../lib/files"; +import { Hono } from "hono" +import { readFile, readdir } from "fs/promises" +import Files from "../lib/files" -const APIDirectory = __dirname+"/api" +const APIDirectory = __dirname + "/api" interface APIMount { file: string @@ -18,35 +18,35 @@ interface APIDefinition { } function resolveMount(mount: APIMountResolvable): APIMount { - return typeof mount == "string" ? { file: mount, to: "/"+mount } : mount + return typeof mount == "string" ? { file: mount, to: "/" + mount } : mount } class APIVersion { - readonly definition: APIDefinition; - readonly apiPath: string; - readonly root: Router = Router(); + readonly definition: APIDefinition + readonly apiPath: string + readonly root: Hono = new Hono() constructor(definition: APIDefinition, files: Files) { - - this.definition = definition; + this.definition = definition this.apiPath = APIDirectory + "/" + definition.name for (let _mount of definition.mount) { let mount = resolveMount(_mount) // no idea if there's a better way to do this but this is all i can think of - let route = require(`${this.apiPath}/${mount.file}.js`) as (files:Files)=>Router - this.root.use(mount.to, route(files)) + let route = require(`${this.apiPath}/${mount.file}.js`) as ( + files: Files + ) => Hono + this.root.route(mount.to, route(files)) } } } export default class APIRouter { - readonly files: Files - readonly root: Router = Router(); + readonly root: Hono = new Hono() constructor(files: Files) { - this.files = files; + this.files = files } /** @@ -55,24 +55,26 @@ export default class APIRouter { */ private mount(definition: APIDefinition) { - console.log(`mounting APIDefinition ${definition.name}`) - - this.root.use( - definition.baseURL, - (new APIVersion(definition, this.files)).root - ) + this.root.route( + definition.baseURL, + new APIVersion(definition, this.files).root + ) } async loadAPIMethods() { - let files = await readdir(APIDirectory) - for (let v of files) { /// temporary (hopefully). need to figure out something else for this - let def = JSON.parse((await readFile(`${process.cwd()}/src/server/routes/api/${v}/api.json`)).toString()) as APIDefinition + for (let version of files) { + /// temporary (hopefully). need to figure out something else for this + let def = JSON.parse( + ( + await readFile( + `${process.cwd()}/src/server/routes/api/${version}/api.json` + ) + ).toString() + ) as APIDefinition this.mount(def) } - } - -} \ No newline at end of file +} diff --git a/src/server/routes/api/v0/adminRoutes.ts b/src/server/routes/api/v0/adminRoutes.ts index 243b719..e91c060 100644 --- a/src/server/routes/api/v0/adminRoutes.ts +++ b/src/server/routes/api/v0/adminRoutes.ts @@ -1,20 +1,21 @@ -import bodyParser from "body-parser"; -import { Router } from "express"; -import * as Accounts from "../../../lib/accounts"; -import * as auth from "../../../lib/auth"; -import bytes from "bytes" -import {writeFile} from "fs"; -import { sendMail } from "../../../lib/mail"; -import { getAccount, requiresAccount, requiresAdmin, requiresPermissions } from "../../../lib/middleware" +import { Hono } from "hono" +import * as Accounts from "../../../lib/accounts" +import * as auth from "../../../lib/auth" +import { writeFile } from "fs/promises" +import { sendMail } from "../../../lib/mail" +import { + getAccount, + requiresAccount, + requiresAdmin, + requiresPermissions, +} from "../../../lib/middleware" +import Files from "../../../lib/files" -import ServeError from "../../../lib/errors"; -import Files from "../../../lib/files"; - -let parser = bodyParser.json({ - type: ["text/plain","application/json"] -}) - -export let adminRoutes = Router(); +export let adminRoutes = new Hono<{ + Variables: { + account: Accounts.Account + } +}>() adminRoutes .use(getAccount) .use(requiresAccount) @@ -23,214 +24,198 @@ adminRoutes let config = require(`${process.cwd()}/config.json`) -module.exports = function(files: Files) { - +module.exports = function (files: Files) { + adminRoutes.post("/reset", async (ctx) => { + let acc = ctx.get("account") as Accounts.Account + const body = await ctx.req.json() - adminRoutes.post("/reset", parser, (req,res) => { - - let acc = res.locals.acc as Accounts.Account - - if (typeof req.body.target !== "string" || typeof req.body.password !== "string") { - res.status(404) - res.send() - return + if ( + typeof body.target !== "string" || + typeof body.password !== "string" + ) { + return ctx.status(404) } - let targetAccount = Accounts.getFromUsername(req.body.target) + let targetAccount = Accounts.getFromUsername(body.target) if (!targetAccount) { - res.status(404) - res.send() - return + return ctx.status(404) } - Accounts.password.set ( targetAccount.id, req.body.password ) - auth.AuthTokens.filter(e => e.account == targetAccount?.id).forEach((v) => { - auth.invalidate(v.token) - }) + Accounts.password.set(targetAccount.id, body.password) + auth.AuthTokens.filter((e) => e.account == targetAccount?.id).forEach( + (v) => { + auth.invalidate(v.token) + } + ) if (targetAccount.email) { - sendMail(targetAccount.email, `Your login details have been updated`, `Hello there! This email is to notify you of a password change that an administrator, ${acc.username}, has initiated. You have been logged out of your devices. Thank you for using monofile.`).then(() => { - res.send("OK") - }).catch((err) => {}) + return sendMail( + targetAccount.email, + `Your login details have been updated`, + `Hello there! This email is to notify you of a password change that an administrator, ${acc.username}, has initiated. You have been logged out of your devices. Thank you for using monofile.` + ) + .then(() => ctx.text("OK")) + .catch(() => ctx.status(500)) } - - - res.send() - }) - adminRoutes.post("/elevate", parser, (req,res) => { + adminRoutes.post("/elevate", async (ctx) => { + const body = await ctx.req.json() + let acc = ctx.get("account") as Accounts.Account - let acc = res.locals.acc as Accounts.Account - - if (typeof req.body.target !== "string") { - res.status(404) - res.send() - return + if (typeof body.target !== "string") { + return ctx.status(404) } - let targetAccount = Accounts.getFromUsername(req.body.target) + let targetAccount = Accounts.getFromUsername(body.target) if (!targetAccount) { - res.status(404) - res.send() - return + return ctx.status(404) } - targetAccount.admin = true; Accounts.save() - res.send() - + return ctx.text("OK") }) - adminRoutes.post("/delete", parser, (req,res) => { - - if (typeof req.body.target !== "string") { - res.status(404) - res.send() - return + adminRoutes.post("/delete", async (ctx) => { + const body = await ctx.req.json() + if (typeof body.target !== "string") { + return ctx.status(404) } - let targetFile = files.getFilePointer(req.body.target) + let targetFile = files.getFilePointer(body.target) if (!targetFile) { - res.status(404) - res.send() - return + return ctx.status(404) } - files.unlink(req.body.target).then(() => { - res.status(200) - }).catch(() => { - res.status(500) - }).finally(() => res.send()) - + return files + .unlink(body.target) + .then(() => ctx.status(200)) + .catch(() => ctx.status(500)) + .finally(() => ctx.status(200)) }) - adminRoutes.post("/delete_account", parser, async (req,res) => { - - let acc = res.locals.acc as Accounts.Account - - if (typeof req.body.target !== "string") { - res.status(404) - res.send() - return + adminRoutes.post("/delete_account", async (ctx) => { + let acc = ctx.get("account") as Accounts.Account + const body = await ctx.req.json() + if (typeof body.target !== "string") { + return ctx.status(404) } - let targetAccount = Accounts.getFromUsername(req.body.target) + let targetAccount = Accounts.getFromUsername(body.target) if (!targetAccount) { - res.status(404) - res.send() - return + return ctx.status(404) } let accId = targetAccount.id - auth.AuthTokens.filter(e => e.account == accId).forEach((v) => { + auth.AuthTokens.filter((e) => e.account == accId).forEach((v) => { auth.invalidate(v.token) }) - let cpl = () => Accounts.deleteAccount(accId).then(_ => { - if (targetAccount?.email) { - sendMail(targetAccount.email, "Notice of account deletion", `Your account, ${targetAccount.username}, has been deleted by ${acc.username} for the following reason:

${req.body.reason || "(no reason specified)"}

Your files ${req.body.deleteFiles ? "have been deleted" : "have not been modified"}. Thank you for using monofile.`) - } - res.send("account deleted") - }) - - if (req.body.deleteFiles) { - let f = targetAccount.files.map(e=>e) // make shallow copy so that iterating over it doesnt Die + let cpl = () => + Accounts.deleteAccount(accId).then((_) => { + if (targetAccount?.email) { + sendMail( + targetAccount.email, + "Notice of account deletion", + `Your account, ${ + targetAccount.username + }, has been deleted by ${ + acc.username + } for the following reason:

${ + body.reason || "(no reason specified)" + }

Your files ${ + body.deleteFiles + ? "have been deleted" + : "have not been modified" + }. Thank you for using monofile.` + ) + } + return ctx.text("account deleted") + }) + + if (body.deleteFiles) { + let f = targetAccount.files.map((e) => e) // make shallow copy so that iterating over it doesnt Die for (let v of f) { - files.unlink(v,true).catch(err => console.error(err)) + files.unlink(v, true).catch((err) => console.error(err)) } - writeFile(process.cwd()+"/.data/files.json",JSON.stringify(files.files), (err) => { - if (err) console.log(err) - cpl() - }) - } else cpl() + return writeFile( + process.cwd() + "/.data/files.json", + JSON.stringify(files.files) + ).then(cpl) + } else return cpl() }) - adminRoutes.post("/transfer", parser, (req,res) => { - - if (typeof req.body.target !== "string" || typeof req.body.owner !== "string") { - res.status(404) - res.send() - return - } - - let targetFile = files.getFilePointer(req.body.target) - if (!targetFile) { - res.status(404) - res.send() - return + adminRoutes.post("/transfer", async (ctx) => { + const body = await ctx.req.json() + if (typeof body.target !== "string" || typeof body.owner !== "string") { + return ctx.status(404) } - let newOwner = Accounts.getFromUsername(req.body.owner || "") + let targetFile = files.getFilePointer(body.target) + if (!targetFile) { + return ctx.status(404) + } + + let newOwner = Accounts.getFromUsername(body.owner || "") // clear old owner if (targetFile.owner) { let oldOwner = Accounts.getFromId(targetFile.owner) if (oldOwner) { - Accounts.files.deindex(oldOwner.id, req.body.target) - } + Accounts.files.deindex(oldOwner.id, body.target) + } } if (newOwner) { - Accounts.files.index(newOwner.id, req.body.target) + Accounts.files.index(newOwner.id, body.target) } - targetFile.owner = newOwner ? newOwner.id : undefined; - - files.writeFile(req.body.target, targetFile).then(() => { - res.send() - }).catch(() => { - res.status(500) - res.send() - }) // wasting a reassignment but whatee + targetFile.owner = newOwner ? newOwner.id : undefined + files + .writeFile(body.target, targetFile) + .then(() => ctx.status(200)) + .catch(() => ctx.status(500)) }) - adminRoutes.post("/idchange", parser, (req,res) => { - - if (typeof req.body.target !== "string" || typeof req.body.new !== "string") { - res.status(400) - res.send() - return + adminRoutes.post("/idchange", async (ctx) => { + const body = await ctx.req.json() + if (typeof body.target !== "string" || typeof body.new !== "string") { + return ctx.status(400) } - - let targetFile = files.getFilePointer(req.body.target) + + let targetFile = files.getFilePointer(body.target) if (!targetFile) { - res.status(404) - res.send() - return + return ctx.status(404) } - - if (files.getFilePointer(req.body.new)) { - res.status(400) - res.send() - return + + if (files.getFilePointer(body.new)) { + return ctx.status(400) } if (targetFile.owner) { - Accounts.files.deindex(targetFile.owner, req.body.target) - Accounts.files.index(targetFile.owner, req.body.new) + Accounts.files.deindex(targetFile.owner, body.target) + Accounts.files.index(targetFile.owner, body.new) } - delete files.files[req.body.target] + delete files.files[body.target] - files.writeFile(req.body.new, targetFile).then(() => { - res.send() - }).catch(() => { - files.files[req.body.target] = req.body.new + return files + .writeFile(body.new, targetFile) + .then(() => ctx.status(200)) + .catch(() => { + files.files[body.target] = body.new - if (targetFile.owner) { - Accounts.files.deindex(targetFile.owner, req.body.new) - Accounts.files.index(targetFile.owner, req.body.target) - } - - res.status(500) - res.send() - }) + if (targetFile.owner) { + Accounts.files.deindex(targetFile.owner, body.new) + Accounts.files.index(targetFile.owner, body.target) + } + return ctx.status(500) + }) }) return adminRoutes -} \ No newline at end of file +} diff --git a/src/server/routes/api/v0/authRoutes.ts b/src/server/routes/api/v0/authRoutes.ts index b33684f..c075ef6 100644 --- a/src/server/routes/api/v0/authRoutes.ts +++ b/src/server/routes/api/v0/authRoutes.ts @@ -1,356 +1,483 @@ -import bodyParser from "body-parser"; -import { Router } from "express"; -import * as Accounts from "../../../lib/accounts"; -import * as auth from "../../../lib/auth"; -import { sendMail } from "../../../lib/mail"; -import { getAccount, noAPIAccess, requiresAccount, requiresPermissions } from "../../../lib/middleware" +import { Hono, Handler } from "hono" +import { getCookie, setCookie } from "hono/cookie" +import * as Accounts from "../../../lib/accounts" +import * as auth from "../../../lib/auth" +import { sendMail } from "../../../lib/mail" +import { + getAccount, + noAPIAccess, + requiresAccount, + requiresPermissions, +} from "../../../lib/middleware" import { accountRatelimit } from "../../../lib/ratelimit" -import ServeError from "../../../lib/errors"; -import Files, { FileVisibility, generateFileId, id_check_regex } from "../../../lib/files"; +import ServeError from "../../../lib/errors" +import Files, { + FileVisibility, + generateFileId, + id_check_regex, +} from "../../../lib/files" -import { writeFile } from "fs"; +import { writeFile } from "fs/promises" -let parser = bodyParser.json({ - type: ["text/plain","application/json"] -}) - -export let authRoutes = Router(); -authRoutes.use(getAccount) +export let authRoutes = new Hono<{ + Variables: { + account: Accounts.Account + } +}>() let config = require(`${process.cwd()}/config.json`) +authRoutes.all("*", getAccount) -module.exports = function(files: Files) { - - authRoutes.post("/login", parser, (req,res) => { - if (typeof req.body.username != "string" || typeof req.body.password != "string") { - ServeError(res,400,"please provide a username or password") - return +module.exports = function (files: Files) { + authRoutes.post("/login", async (ctx) => { + console.log(ctx) + const body = await ctx.req.json() + if ( + typeof body.username != "string" || + typeof body.password != "string" + ) { + return ServeError(ctx, 400, "please provide a username or password") } - if (auth.validate(req.cookies.auth)) return + if (auth.validate(getCookie(ctx, "auth")!)) + return ctx.text("You are already authed") /* check if account exists */ - let acc = Accounts.getFromUsername(req.body.username) + let acc = Accounts.getFromUsername(body.username) if (!acc) { - ServeError(res,401,"username or password incorrect") - return + return ServeError(ctx, 401, "username or password incorrect") } - if (!Accounts.password.check(acc.id,req.body.password)) { - ServeError(res,401,"username or password incorrect") - return + if (!Accounts.password.check(acc.id, body.password)) { + return ServeError(ctx, 401, "username or password incorrect") } /* assign token */ - res.cookie("auth",auth.create(acc.id,(3*24*60*60*1000))) - res.status(200) - res.end() + setCookie(ctx, "auth", auth.create(acc.id, 3 * 24 * 60 * 60 * 1000)) + return ctx.text("") }) - authRoutes.post("/create", parser, (req,res) => { + authRoutes.post("/create", async (ctx) => { if (!config.accounts.registrationEnabled) { - ServeError(res,403,"account registration disabled") - return + return ServeError(ctx, 403, "account registration disabled") } - if (auth.validate(req.cookies.auth)) return - - if (typeof req.body.username != "string" || typeof req.body.password != "string") { - ServeError(res,400,"please provide a username or password") - return + if (auth.validate(getCookie(ctx, "auth")!)) return + const body = await ctx.req.json() + if ( + typeof body.username != "string" || + typeof body.password != "string" + ) { + return ServeError(ctx, 400, "please provide a username or password") } /* check if account exists */ - let acc = Accounts.getFromUsername(req.body.username) + let acc = Accounts.getFromUsername(body.username) if (acc) { - ServeError(res,400,"account with this username already exists") + ServeError(ctx, 400, "account with this username already exists") return } - if (req.body.username.length < 3 || req.body.username.length > 20) { - ServeError(res,400,"username must be over or equal to 3 characters or under or equal to 20 characters in length") + if (body.username.length < 3 || body.username.length > 20) { + return ServeError( + ctx, + 400, + "username must be over or equal to 3 characters or under or equal to 20 characters in length" + ) + } + + if ( + (body.username.match(/[A-Za-z0-9_\-\.]+/) || [])[0] != body.username + ) { + return ServeError(ctx, 400, "username contains invalid characters") + } + + if (body.password.length < 8) { + ServeError(ctx, 400, "password must be 8 characters or longer") return } - if ((req.body.username.match(/[A-Za-z0-9_\-\.]+/) || [])[0] != req.body.username) { - ServeError(res,400,"username contains invalid characters") - return - } - - if (req.body.password.length < 8) { - ServeError(res,400,"password must be 8 characters or longer") - return - } - - Accounts.create(req.body.username,req.body.password) + return Accounts.create(body.username, body.password) .then((newAcc) => { /* assign token */ - res.cookie("auth",auth.create(newAcc,(3*24*60*60*1000))) - res.status(200) - res.end() - }) - .catch(() => { - ServeError(res,500,"internal server error") + setCookie( + ctx, + "auth", + auth.create(newAcc, 3 * 24 * 60 * 60 * 1000) + ) + return ctx.text("") }) + .catch(() => ServeError(ctx, 500, "internal server error")) }) - authRoutes.post("/logout", (req,res) => { - if (!auth.validate(req.cookies.auth)) { - ServeError(res, 401, "not logged in") - return + authRoutes.post("/logout", async (ctx) => { + if (!auth.validate(getCookie(ctx, "auth")!)) { + return ServeError(ctx, 401, "not logged in") } - auth.invalidate(req.cookies.auth) - res.send("logged out") + auth.invalidate(getCookie(ctx, "auth")!) + return ctx.text("logged out") }) - authRoutes.post("/dfv", requiresAccount, requiresPermissions("manage"), parser, (req,res) => { - let acc = res.locals.acc as Accounts.Account + authRoutes.post( + "/dfv", + requiresAccount, + requiresPermissions("manage"), + // Used body-parser + async (ctx) => { + const body = await ctx.req.json() + let acc = ctx.get("account") as Accounts.Account - if (['public','private','anonymous'].includes(req.body.defaultFileVisibility)) { - acc.defaultFileVisibility = req.body.defaultFileVisibility - Accounts.save() - res.send(`dfv has been set to ${acc.defaultFileVisibility}`) - } else { - res.status(400) - res.send("invalid dfv") + if ( + ["public", "private", "anonymous"].includes( + body.defaultFileVisibility + ) + ) { + acc.defaultFileVisibility = body.defaultFileVisibility + Accounts.save() + return ctx.text( + `dfv has been set to ${acc.defaultFileVisibility}` + ) + } else { + return ctx.text("invalid dfv", 400) + } } - }) + ) - authRoutes.post("/customcss", requiresAccount, requiresPermissions("customize"), parser, (req,res) => { - let acc = res.locals.acc as Accounts.Account - - if (typeof req.body.fileId != "string") req.body.fileId = undefined; - - if ( + authRoutes.post( + "/customcss", + requiresAccount, + requiresPermissions("customize"), + // Used body-parser + async (ctx) => { + const body = await ctx.req.json() + let acc = ctx.get("account") as Accounts.Account - !req.body.fileId - || (req.body.fileId.match(id_check_regex) == req.body.fileId - && req.body.fileId.length <= config.maxUploadIdLength) - - ) { - acc.customCSS = req.body.fileId || undefined - if (!req.body.fileId) delete acc.customCSS - Accounts.save() - res.send(`custom css saved`) - } else { - res.status(400) - res.send("invalid fileid") + if (typeof body.fileId != "string") body.fileId = undefined + + if ( + !body.fileId || + (body.fileId.match(id_check_regex) == body.fileId && + body.fileId.length <= config.maxUploadIdLength) + ) { + acc.customCSS = body.fileId || undefined + if (!body.fileId) delete acc.customCSS + Accounts.save() + return ctx.text(`custom css saved`) + } else { + return ctx.text("invalid fileid", 400) + } } - }) + ) - authRoutes.post("/embedcolor", requiresAccount, requiresPermissions("customize"), parser, (req,res) => { - let acc = res.locals.acc as Accounts.Account - - if (typeof req.body.color != "string") req.body.color = undefined; - - if ( + authRoutes.post( + "/embedcolor", + requiresAccount, + requiresPermissions("customize"), + // Used body-parser + async (ctx) => { + let acc = ctx.get("account") as Accounts.Account + const body = await ctx.req.json() + if (typeof body.color != "string") body.color = undefined + + if ( + !body.color || + (body.color.toLowerCase().match(/[a-f0-9]+/) == + body.color.toLowerCase() && + body.color.length == 6) + ) { + if (!acc.embed) acc.embed = {} + acc.embed.color = body.color || undefined + if (!body.color) delete acc.embed.color + Accounts.save() + return ctx.text(`custom embed color saved`) + } else { + return ctx.text("invalid hex code", 400) + } + } + ) + + authRoutes.post( + "/embedsize", + requiresAccount, + requiresPermissions("customize"), + // Used body-parser + async (ctx) => { + let acc = ctx.get("account") as Accounts.Account + const body = await ctx.req.json() + if (typeof body.largeImage != "boolean") body.color = false - !req.body.color - || (req.body.color.toLowerCase().match(/[a-f0-9]+/) == req.body.color.toLowerCase()) - && req.body.color.length == 6 - - ) { if (!acc.embed) acc.embed = {} - acc.embed.color = req.body.color || undefined - if (!req.body.color) delete acc.embed.color + acc.embed.largeImage = body.largeImage + if (!body.largeImage) delete acc.embed.largeImage Accounts.save() - res.send(`custom embed color saved`) - } else { - res.status(400) - res.send("invalid hex code") + return ctx.text(`custom embed image size saved`) } - }) + ) - authRoutes.post("/embedsize", requiresAccount, requiresPermissions("customize"), parser, (req,res) => { - let acc = res.locals.acc as Accounts.Account - - if (typeof req.body.largeImage != "boolean") req.body.color = false; + authRoutes.post( + "/delete_account", + requiresAccount, + noAPIAccess, + // Used body-parser + async (ctx) => { + let acc = ctx.get("account") as Accounts.Account + const body = await ctx.req.json() + let accId = acc.id - if (!acc.embed) acc.embed = {} - acc.embed.largeImage = req.body.largeImage - if (!req.body.largeImage) delete acc.embed.largeImage - Accounts.save() - res.send(`custom embed image size saved`) - }) + auth.AuthTokens.filter((e) => e.account == accId).forEach((v) => { + auth.invalidate(v.token) + }) - authRoutes.post("/delete_account", requiresAccount, noAPIAccess, parser, async (req,res) => { - let acc = res.locals.acc as Accounts.Account - - let accId = acc.id + let cpl = () => + Accounts.deleteAccount(accId).then((_) => + ctx.text("account deleted") + ) - auth.AuthTokens.filter(e => e.account == accId).forEach((v) => { - auth.invalidate(v.token) - }) + if (body.deleteFiles) { + let f = acc.files.map((e) => e) // make shallow copy so that iterating over it doesnt Die + for (let v of f) { + files.unlink(v, true).catch((err) => console.error(err)) + } - let cpl = () => Accounts.deleteAccount(accId).then(_ => res.send("account deleted")) - - if (req.body.deleteFiles) { - let f = acc.files.map(e=>e) // make shallow copy so that iterating over it doesnt Die - for (let v of f) { - files.unlink(v,true).catch(err => console.error(err)) + return writeFile( + process.cwd() + "/.data/files.json", + JSON.stringify(files.files) + ).then(cpl) + } else cpl() + } + ) + + authRoutes.post( + "/change_username", + requiresAccount, + noAPIAccess, + // Used body-parser + async (ctx) => { + let acc = ctx.get("account") as Accounts.Account + const body = await ctx.req.json() + if ( + typeof body.username != "string" || + body.username.length < 3 || + body.username.length > 20 + ) { + return ServeError( + ctx, + 400, + "username must be between 3 and 20 characters in length" + ) } - writeFile(process.cwd()+"/.data/files.json",JSON.stringify(files.files), (err) => { - if (err) console.log(err) - cpl() - }) - } else cpl() - }) + let _acc = Accounts.getFromUsername(body.username) - authRoutes.post("/change_username", requiresAccount, noAPIAccess, parser, (req,res) => { - let acc = res.locals.acc as Accounts.Account + if (_acc) { + return ServeError( + ctx, + 400, + "account with this username already exists" + ) + } - if (typeof req.body.username != "string" || req.body.username.length < 3 || req.body.username.length > 20) { - ServeError(res,400,"username must be between 3 and 20 characters in length") - return + if ( + (body.username.match(/[A-Za-z0-9_\-\.]+/) || [])[0] != + body.username + ) { + return ServeError( + ctx, + 400, + "username contains invalid characters" + ) + } + + acc.username = body.username + Accounts.save() + + if (acc.email) { + return sendMail( + acc.email, + `Your login details have been updated`, + `Hello there! Your username has been updated to ${body.username}. 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`, `Hello there! Your username has been updated to ${req.body.username}. Please update your devices accordingly. Thank you for using monofile.`).then(() => { - res.send("OK") - }).catch((err) => {}) - } - - res.send("username changed") - }) + ) // shit way to do this but... - let verificationCodes = new Map() + let verificationCodes = new Map< + string, + { code: string; email: string; expiry: NodeJS.Timeout } + >() - authRoutes.post("/request_email_change", requiresAccount, noAPIAccess, accountRatelimit({ requests: 4, per: 60*60*1000 }), parser, (req,res) => { - let acc = res.locals.acc as Accounts.Account - - - if (typeof req.body.email != "string" || !req.body.email) { - ServeError(res,400, "supply an email") - return - } + authRoutes.post( + "/request_email_change", + requiresAccount, + noAPIAccess, + accountRatelimit({ requests: 4, per: 60 * 60 * 1000 }), + // Used body-parser + async (ctx) => { + let acc = ctx.get("account") as Accounts.Account + const body = await ctx.req.json() + if (typeof body.email != "string" || !body.email) { + ServeError(ctx, 400, "supply an email") + return + } - let vcode = verificationCodes.get(acc.id) + let vcode = verificationCodes.get(acc.id) - // delete previous if any - let e = vcode?.expiry - if (e) clearTimeout(e) - verificationCodes.delete(acc?.id||"") - - let code = generateFileId(12).toUpperCase() - - // set - - verificationCodes.set(acc.id, { - code, - email: req.body.email, - expiry: setTimeout( () => verificationCodes.delete(acc?.id||""), 15*60*1000) - }) - - // this is a mess but it's fine - - sendMail(req.body.email, `Hey there, ${acc.username} - let's connect your email`, `Hello there! You are recieving this message because you decided to link your email, ${req.body.email.split("@")[0]}@${req.body.email.split("@")[1]}, to your account, ${acc.username}. If you would like to continue, please click here, or go to https://${req.header("Host")}/auth/confirm_email/${code}.`).then(() => { - res.send("OK") - }).catch((err) => { - let e = verificationCodes.get(acc?.id||"")?.expiry + // delete previous if any + let e = vcode?.expiry if (e) clearTimeout(e) - verificationCodes.delete(acc?.id||"") - res.locals.undoCount(); - ServeError(res, 500, err?.toString()) - }) - }) + verificationCodes.delete(acc?.id || "") - authRoutes.get("/confirm_email/:code", requiresAccount, noAPIAccess, (req,res) => { - let acc = res.locals.acc as Accounts.Account - + let code = generateFileId(12).toUpperCase() - let vcode = verificationCodes.get(acc.id) + // set - if (!vcode) { ServeError(res, 400, "nothing to confirm"); return } + verificationCodes.set(acc.id, { + code, + email: body.email, + expiry: setTimeout( + () => verificationCodes.delete(acc?.id || ""), + 15 * 60 * 1000 + ), + }) - if (typeof req.params.code == "string" && req.params.code.toUpperCase() == vcode.code) { - acc.email = vcode.email - Accounts.save(); + // this is a mess but it's fine - let e = verificationCodes.get(acc?.id||"")?.expiry - if (e) clearTimeout(e) - verificationCodes.delete(acc?.id||"") - - res.redirect("/") - } else { - ServeError(res, 400, "invalid code") + sendMail( + body.email, + `Hey there, ${acc.username} - let's connect your email`, + `Hello there! You are recieving this message because you decided to link your email, ${ + body.email.split("@")[0] + }@${ + body.email.split("@")[1] + }, to your account, ${ + acc.username + }. If you would like to continue, please click here, or go to https://${ctx.req.header( + "Host" + )}/auth/confirm_email/${code}.` + ) + .then(() => ctx.text("OK")) + .catch((err) => { + let e = verificationCodes.get(acc?.id || "")?.expiry + if (e) clearTimeout(e) + verificationCodes.delete(acc?.id || "") + ;(ctx.get("undoCount" as never) as () => {})() + return ServeError(ctx, 500, err?.toString()) + }) } - }) + ) - authRoutes.post("/remove_email", requiresAccount, noAPIAccess, (req,res) => { - let acc = res.locals.acc as Accounts.Account - - if (acc.email) { - delete acc.email; - Accounts.save() - res.send("email detached") - } - else ServeError(res, 400, "email not attached") - }) + authRoutes.get( + "/confirm_email/:code", + requiresAccount, + noAPIAccess, + async (ctx) => { + let acc = ctx.get("account") as Accounts.Account - let pwReset = new Map() + 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() - authRoutes.post("/request_emergency_login", parser, (req,res) => { - if (auth.validate(req.cookies.auth || "")) return - - if (typeof req.body.account != "string" || !req.body.account) { - ServeError(res,400, "supply a username") + authRoutes.post("/request_emergency_login", async (ctx) => { + if (auth.validate(getCookie(ctx, "auth") || "")) return + const body = await ctx.req.json() + if (typeof body.account != "string" || !body.account) { + ServeError(ctx, 400, "supply a username") return } - let acc = Accounts.getFromUsername(req.body.account) + let acc = Accounts.getFromUsername(body.account) if (!acc || !acc.email) { - ServeError(res, 400, "this account either does not exist or does not have an email attached; please contact the server's admin for a reset if you would still like to access it") - return + return ServeError( + ctx, + 400, + "this account either does not exist or does not have an email attached; please contact the server's admin for a reset if you would still like to access it" + ) } - let pResetCode = pwReset.get(acc.id) + let pResetCode = pwReset.get(acc.id) - if (pResetCode && pResetCode.requestedAt+(15*60*1000) > Date.now()) { - ServeError(res, 429, `Please wait a few moments to request another emergency login.`) - return + if ( + pResetCode && + pResetCode.requestedAt + 15 * 60 * 1000 > Date.now() + ) { + return ServeError( + ctx, + 429, + `Please wait a few moments to request another emergency login.` + ) } - // delete previous if any let e = pResetCode?.expiry if (e) clearTimeout(e) - pwReset.delete(acc?.id||"") - prcIdx.delete(pResetCode?.code||"") + pwReset.delete(acc?.id || "") + prcIdx.delete(pResetCode?.code || "") let code = generateFileId(12).toUpperCase() @@ -358,107 +485,146 @@ module.exports = function(files: Files) { pwReset.set(acc.id, { code, - expiry: setTimeout( () => { pwReset.delete(acc?.id||""); prcIdx.delete(pResetCode?.code||"") }, 15*60*1000), - requestedAt: Date.now() + expiry: setTimeout(() => { + pwReset.delete(acc?.id || "") + prcIdx.delete(pResetCode?.code || "") + }, 15 * 60 * 1000), + requestedAt: Date.now(), }) prcIdx.set(code, acc.id) // this is a mess but it's fine - sendMail(acc.email, `Emergency login requested for ${acc.username}`, `Hello there! You are recieving this message because you forgot your password to your monofile account, ${acc.username}. To log in, please click here, or go to https://${req.header("Host")}/auth/emergency_login/${code}. If it doesn't appear that you are logged in after visiting this link, please try refreshing. Once you have successfully logged in, you may reset your password.`).then(() => { - res.send("OK") - }).catch((err) => { - let e = pwReset.get(acc?.id||"")?.expiry - if (e) clearTimeout(e) - pwReset.delete(acc?.id||"") - prcIdx.delete(code||"") - ServeError(res, 500, err?.toString()) - }) + return sendMail( + acc.email, + `Emergency login requested for ${acc.username}`, + `Hello there! You are recieving this message because you forgot your password to your monofile account, ${ + acc.username + }. To log in, please click here, or go to https://${ctx.req.header( + "Host" + )}/auth/emergency_login/${code}. If it doesn't appear that you are logged in after visiting this link, please try refreshing. Once you have successfully logged in, you may reset your password.` + ) + .then(() => ctx.text("OK")) + .catch((err) => { + let e = pwReset.get(acc?.id || "")?.expiry + if (e) clearTimeout(e) + pwReset.delete(acc?.id || "") + prcIdx.delete(code || "") + return ServeError(ctx, 500, err?.toString()) + }) }) - authRoutes.get("/emergency_login/:code", (req,res) => { - if (auth.validate(req.cookies.auth || "")) { - ServeError(res, 403, "already logged in") - return + authRoutes.get("/emergency_login/:code", async (ctx) => { + if (auth.validate(getCookie(ctx, "auth") || "")) { + return ServeError(ctx, 403, "already logged in") } - let vcode = prcIdx.get(req.params.code) + let vcode = prcIdx.get(ctx.req.param("code")) - if (!vcode) { ServeError(res, 400, "invalid emergency login code"); return } - - if (typeof req.params.code == "string" && vcode) { - res.cookie("auth",auth.create(vcode,(3*24*60*60*1000))) - res.redirect("/") + if (!vcode) { + return ServeError(ctx, 400, "invalid emergency login code") + } + if (typeof ctx.req.param("code") == "string" && vcode) { + setCookie(ctx, "auth", auth.create(vcode, 3 * 24 * 60 * 60 * 1000)) let e = pwReset.get(vcode)?.expiry if (e) clearTimeout(e) pwReset.delete(vcode) - prcIdx.delete(req.params.code) + prcIdx.delete(ctx.req.param("code")) + return ctx.redirect("/") } else { - ServeError(res, 400, "invalid code") + ServeError(ctx, 400, "invalid code") } }) - authRoutes.post("/change_password", requiresAccount, noAPIAccess, parser, (req,res) => { - let acc = res.locals.acc as Accounts.Account - - if (typeof req.body.password != "string" || req.body.password.length < 8) { - ServeError(res,400,"password must be 8 characters or longer") - return + authRoutes.post( + "/change_password", + requiresAccount, + noAPIAccess, + // Used body-parser + async (ctx) => { + let acc = ctx.get("account") as Accounts.Account + const body = await ctx.req.json() + if (typeof body.password != "string" || body.password.length < 8) { + ServeError(ctx, 400, "password must be 8 characters or longer") + return + } + + let accId = acc.id + + Accounts.password.set(accId, body.password) + + auth.AuthTokens.filter((e) => e.account == accId).forEach((v) => { + auth.invalidate(v.token) + }) + + if (acc.email) { + return sendMail( + acc.email, + `Your login details have been updated`, + `Hello there! This email is to notify you of a password change that you have initiated. You have been logged out of your devices. Thank you for using monofile.` + ) + .then(() => ctx.text("OK")) + .catch((err) => {}) + } + + return ctx.text("password changed - logged out all sessions") } + ) - let accId = acc.id + authRoutes.post( + "/logout_sessions", + requiresAccount, + noAPIAccess, + async (ctx) => { + let acc = ctx.get("account") as Accounts.Account - Accounts.password.set(accId,req.body.password) + let accId = acc.id - auth.AuthTokens.filter(e => e.account == accId).forEach((v) => { - auth.invalidate(v.token) - }) + auth.AuthTokens.filter((e) => e.account == accId).forEach((v) => { + auth.invalidate(v.token) + }) - if (acc.email) { - sendMail(acc.email, `Your login details have been updated`, `Hello there! This email is to notify you of a password change that you have initiated. You have been logged out of your devices. Thank you for using monofile.`).then(() => { - res.send("OK") - }).catch((err) => {}) + return ctx.text("logged out all sessions") } + ) - res.send("password changed - logged out all sessions") + authRoutes.get( + "/me", + requiresAccount, + requiresPermissions("user"), + async (ctx) => { + let acc = ctx.get("account") as Accounts.Account + let sessionToken = auth.tokenFor(ctx)! + let accId = acc.id + return ctx.json({ + ...acc, + sessionCount: auth.AuthTokens.filter( + (e) => + e.type != "App" && + e.account == accId && + (e.expire > Date.now() || !e.expire) + ).length, + sessionExpires: auth.AuthTokens.find( + (e) => e.token == sessionToken + )?.expire, + password: undefined, + email: + auth.getType(sessionToken) == "User" || + auth.getPermissions(sessionToken)?.includes("email") + ? acc.email + : undefined, + }) + } + ) + + authRoutes.get("/customCSS", async (ctx) => { + let acc = ctx.get("account") + if (acc?.customCSS) return ctx.redirect(`/file/${acc.customCSS}`) + else return ctx.text("") }) - - authRoutes.post("/logout_sessions", requiresAccount, noAPIAccess, (req,res) => { - let acc = res.locals.acc as Accounts.Account - - let accId = acc.id - - auth.AuthTokens.filter(e => e.account == accId).forEach((v) => { - auth.invalidate(v.token) - }) - - res.send("logged out all sessions") - }) - - authRoutes.get("/me", requiresAccount, requiresPermissions("user"), (req,res) => { - let acc = res.locals.acc as Accounts.Account - - let sessionToken = auth.tokenFor(req) - let accId = acc.id - res.send({ - ...acc, - sessionCount: auth.AuthTokens.filter(e => e.type != "App" && e.account == accId && (e.expire > Date.now() || !e.expire)).length, - sessionExpires: auth.AuthTokens.find(e => e.token == sessionToken)?.expire, - password: undefined, - email: - auth.getType(sessionToken) == "User" || auth.getPermissions(sessionToken)?.includes("email") - ? acc.email - : undefined - }) - }) - - authRoutes.get("/customCSS", (req,res) => { - let acc = res.locals.acc - if (acc?.customCSS) res.redirect(`/file/${acc.customCSS}`) - else res.send("") - }) - return authRoutes -} \ No newline at end of file +} diff --git a/src/server/routes/api/v0/fileApiRoutes.ts b/src/server/routes/api/v0/fileApiRoutes.ts index 96cd176..de10fe4 100644 --- a/src/server/routes/api/v0/fileApiRoutes.ts +++ b/src/server/routes/api/v0/fileApiRoutes.ts @@ -1,98 +1,129 @@ -import bodyParser from "body-parser"; -import { Router } from "express"; -import * as Accounts from "../../../lib/accounts"; -import * as auth from "../../../lib/auth"; -import bytes from "bytes" -import {writeFile} from "fs"; +import { Hono } from "hono" +import * as Accounts from "../../../lib/accounts" +import { writeFile } from "fs/promises" +import Files from "../../../lib/files" +import { + getAccount, + requiresAccount, + requiresPermissions, +} from "../../../lib/middleware" -import ServeError from "../../../lib/errors"; -import Files from "../../../lib/files"; -import { getAccount, requiresAccount, requiresPermissions } from "../../../lib/middleware"; - -let parser = bodyParser.json({ - type: ["text/plain","application/json"] -}) - -export let fileApiRoutes = Router(); +export let fileApiRoutes = new Hono<{ + Variables: { + account: Accounts.Account + } +}>() let config = require(`${process.cwd()}/config.json`) +fileApiRoutes.use("*", getAccount) // :warning: /list somehow crashes Hono with an internal error! +/* +/home/jack/Code/Web/monofile/node_modules/.pnpm/@hono+node-server@1.2.0/node_modules/@hono/node-server/dist/listener.js:55 + const contentType = res.headers.get("content-type") || ""; + ^ -module.exports = function(files: Files) { +TypeError: Cannot read properties of undefined (reading 'get') + at Server. (/home/jack/Code/Web/monofile/node_modules/.pnpm/@hono+node-server@1.2.0/node_modules/@hono/node-server/dist/listener.js:55:37) + at process.processTicksAndRejections (node:internal/process/task_queues:95:5) +*/ - fileApiRoutes.use(getAccount); +module.exports = function (files: Files) { + fileApiRoutes.get( + "/list", + requiresAccount, + requiresPermissions("user"), + async (ctx) => { + let acc = ctx.get("account") as Accounts.Account - fileApiRoutes.get("/list", requiresAccount, requiresPermissions("user"), (req,res) => { + if (!acc) return + let accId = acc.id - let acc = res.locals.acc as Accounts.Account - - if (!acc) return - let accId = acc.id + ctx.json( + acc.files + .map((e) => { + let fp = files.getFilePointer(e) + if (!fp) { + Accounts.files.deindex(accId, e) + return null + } + return { + ...fp, + messageids: null, + owner: null, + id: e, + } + }) + .filter((e) => e) + ) + } + ) - res.send(acc.files.map((e) => { - let fp = files.getFilePointer(e) - if (!fp) { Accounts.files.deindex(accId, e); return null } - return { - ...fp, - messageids: null, - owner: null, - id:e - } - }).filter(e=>e)) - - }) - - fileApiRoutes.post("/manage", parser, requiresPermissions("manage"), (req,res) => { - - let acc = res.locals.acc as Accounts.Account - - if (!acc) return - if (!req.body.target || !(typeof req.body.target == "object") || req.body.target.length < 1) return - - let modified = 0 - - req.body.target.forEach((e:string) => { - if (!acc.files.includes(e)) return - - let fp = files.getFilePointer(e) - - if (fp.reserved) { + fileApiRoutes.post( + "/manage", + requiresPermissions("manage"), + async (ctx) => { + let acc = ctx.get("account") as Accounts.Account + const body = await ctx.req.json() + if (!acc) return + if ( + !body.target || + !(typeof body.target == "object") || + body.target.length < 1 + ) return - } - switch( req.body.action ) { - case "delete": - files.unlink(e, true) - modified++; - break; + let modified = 0 - case "changeFileVisibility": - if (!["public","anonymous","private"].includes(req.body.value)) return; - files.files[e].visibility = req.body.value; - modified++; - break; + body.target.forEach((e: string) => { + if (!acc.files.includes(e)) return - case "setTag": - if (!req.body.value) delete files.files[e].tag - else { - if (req.body.value.toString().length > 30) return - files.files[e].tag = req.body.value.toString().toLowerCase() - } - modified++; - break; - } - }) + let fp = files.getFilePointer(e) - Accounts.save().then(() => { - writeFile(process.cwd()+"/.data/files.json",JSON.stringify(files.files), (err) => { - if (err) console.log(err) - res.contentType("text/plain") - res.send(`modified ${modified} files`) + if (fp.reserved) { + return + } + + switch (body.action) { + case "delete": + files.unlink(e, true) + modified++ + break + + case "changeFileVisibility": + if ( + !["public", "anonymous", "private"].includes( + body.value + ) + ) + return + files.files[e].visibility = body.value + modified++ + break + + case "setTag": + if (!body.value) delete files.files[e].tag + else { + if (body.value.toString().length > 30) return + files.files[e].tag = body.value + .toString() + .toLowerCase() + } + modified++ + break + } }) - }).catch((err) => console.error(err)) - - }) + return Accounts.save() + .then(() => { + writeFile( + process.cwd() + "/.data/files.json", + JSON.stringify(files.files) + ) + }) + .then(() => ctx.text(`modified ${modified} files`)) + .catch((err) => console.error(err)) + } + ) return fileApiRoutes -} \ No newline at end of file +} diff --git a/src/server/routes/api/v0/primaryApi.ts b/src/server/routes/api/v0/primaryApi.ts index 30b98a6..efc0d67 100644 --- a/src/server/routes/api/v0/primaryApi.ts +++ b/src/server/routes/api/v0/primaryApi.ts @@ -1,11 +1,12 @@ import bodyParser from "body-parser" -import express, { Router } from "express" +import { Hono } from "hono" + import * as Accounts from "../../../lib/accounts" import * as auth from "../../../lib/auth" import axios, { AxiosResponse } from "axios" import { type Range } from "range-parser" import multer, { memoryStorage } from "multer" - +import { Readable } from "stream" import ServeError from "../../../lib/errors" import Files from "../../../lib/files" import { getAccount, requiresPermissions } from "../../../lib/middleware" @@ -14,7 +15,11 @@ let parser = bodyParser.json({ type: ["text/plain", "application/json"], }) -export let primaryApi = Router() +export let primaryApi = new Hono<{ + Variables: { + account: Accounts.Account + } +}>() const multerSetup = multer({ storage: memoryStorage() }) @@ -23,216 +28,210 @@ let config = require(`${process.cwd()}/config.json`) primaryApi.use(getAccount) module.exports = function (files: Files) { - primaryApi.get( - ["/file/:fileId", "/cpt/:fileId/*", "/:fileId"], - async (req: express.Request, res: express.Response) => { - let acc = res.locals.acc as Accounts.Account + // primaryApi.get( + // ["/file/:fileId", "/cpt/:fileId/*", "/:fileId"], + // async (ctx) => { + // let acc = ctx.get("account") as Accounts.Account - let file = files.getFilePointer(req.params.fileId) - res.setHeader("Access-Control-Allow-Origin", "*") - res.setHeader("Content-Security-Policy", "sandbox allow-scripts") - if (req.query.attachment == "1") - res.setHeader("Content-Disposition", "attachment") + // let file = files.getFilePointer(ctx.req.param("fileId")) + // ctx.header("Access-Control-Allow-Origin", "*") + // ctx.header("Content-Security-Policy", "sandbox allow-scripts") + // if (ctx.req.query("attachment") == "1") + // ctx.header("Content-Disposition", "attachment") - if (file) { - if (file.visibility == "private") { - if (acc?.id != file.owner) { - ServeError(res, 403, "you do not own this file") - return - } + // if (file) { + // if (file.visibility == "private") { + // if (acc?.id != file.owner) { + // return ServeError(ctx, 403, "you do not own this file") + // } - if ( - auth.getType(auth.tokenFor(req)) == "App" && - auth - .getPermissions(auth.tokenFor(req)) - ?.includes("private") - ) { - ServeError(res, 403, "insufficient permissions") - return - } - } + // if ( + // auth.getType(auth.tokenFor(ctx)!) == "App" && + // auth + // .getPermissions(auth.tokenFor(ctx)!) + // ?.includes("private") + // ) { + // ServeError(ctx, 403, "insufficient permissions") + // return + // } + // } - let range: Range | undefined + // let range: Range | undefined - res.setHeader("Content-Type", file.mime) - if (file.sizeInBytes) { - res.setHeader("Content-Length", file.sizeInBytes) + // ctx.header("Content-Type", file.mime) + // if (file.sizeInBytes) { + // ctx.header("Content-Length", file.sizeInBytes.toString()) - if (file.chunkSize) { - let rng = req.range(file.sizeInBytes) - if (rng) { - // error handling - if (typeof rng == "number") { - res.status(rng == -1 ? 416 : 400).send() - return - } - if (rng.type != "bytes") { - res.status(400).send() - return - } + // if (file.chunkSize) { + // let range = ctx.range(file.sizeInBytes) + // if (range) { + // // error handling + // if (typeof range == "number") { + // return ctx.status(range == -1 ? 416 : 400) + // } + // if (range.type != "bytes") { + // return ctx.status(400) + // } - // set ranges var - let rngs = Array.from(rng) - if (rngs.length != 1) { - res.status(400).send() - return - } - range = rngs[0] - } - } - } + // // set ranges var + // let rngs = Array.from(range) + // if (rngs.length != 1) { + // return ctx.status(400) + // } + // range = rngs[0] + // } + // } + // } - // supports ranges + // // supports ranges - files - .readFileStream(req.params.fileId, range) - .then(async (stream) => { - if (range) { - res.status(206) - res.header( - "Content-Length", - (range.end - range.start + 1).toString() - ) - res.header( - "Content-Range", - `bytes ${range.start}-${range.end}/${file.sizeInBytes}` - ) - } - stream.pipe(res) - }) - .catch((err) => { - ServeError(res, err.status, err.message) - }) - } else { - ServeError(res, 404, "file not found") - } - } - ) + // return files + // .readFileStream(ctx.req.param("fileId"), range) + // .then(async (stream) => { + // if (range) { + // ctx.status(206) + // ctx.header( + // "Content-Length", + // (range.end - range.start + 1).toString() + // ) + // ctx.header( + // "Content-Range", + // `bytes ${range.start}-${range.end}/${file.sizeInBytes}` + // ) + // } - primaryApi.head( - ["/file/:fileId", "/cpt/:fileId/*", "/:fileId"], - (req: express.Request, res: express.Response) => { - let file = files.getFilePointer(req.params.fileId) + // return ctx.stream((stre) => { + // // Somehow return a stream? + // }) + // }) + // .catch((err) => { + // return ServeError(ctx, err.status, err.message) + // }) + // } else { + // return ServeError(ctx, 404, "file not found") + // } + // } + // ) - if ( - file.visibility == "private" && - (res.locals.acc?.id != file.owner || - (auth.getType(auth.tokenFor(req)) == "App" && - auth - .getPermissions(auth.tokenFor(req)) - ?.includes("private"))) - ) { - res.status(403).send() - return - } + // // primaryApi.head( + // // ["/file/:fileId", "/cpt/:fileId/*", "/:fileId"], + // // async (ctx) => { + // // let file = files.getFilePointer(req.params.fileId) - res.setHeader("Access-Control-Allow-Origin", "*") - res.setHeader("Content-Security-Policy", "sandbox allow-scripts") + // // if ( + // // file.visibility == "private" && + // // (ctx.get("account")?.id != file.owner || + // // (auth.getType(auth.tokenFor(ctx)!) == "App" && + // // auth + // // .getPermissions(auth.tokenFor(ctx)!) + // // ?.includes("private"))) + // // ) { + // // return ctx.status(403) + // // } - if (req.query.attachment == "1") - res.setHeader("Content-Disposition", "attachment") + // // ctx.header("Content-Security-Policy", "sandbox allow-scripts") - if (!file) { - res.status(404) - res.send() - } else { - res.setHeader("Content-Type", file.mime) - if (file.sizeInBytes) { - res.setHeader("Content-Length", file.sizeInBytes) - } - if (file.chunkSize) { - res.setHeader("Accept-Ranges", "bytes") - } - res.send() - } - } - ) + // // if (ctx.req.query("attachment") == "1") + // // ctx.header("Content-Disposition", "attachment") - // upload handlers + // // if (!file) { + // // res.status(404) + // // res.send() + // // } else { + // // ctx.header("Content-Type", file.mime) + // // if (file.sizeInBytes) { + // // ctx.header("Content-Length", file.sizeInBytes) + // // } + // // if (file.chunkSize) { + // // ctx.header("Accept-Ranges", "bytes") + // // } + // // res.send() + // // } + // // } + // // ) - primaryApi.post( - "/upload", - requiresPermissions("upload"), - multerSetup.single("file"), - async (req, res) => { - let acc = res.locals.acc as Accounts.Account + // // upload handlers - if (req.file) { - try { - let prm = req.header("monofile-params") - let params: { [key: string]: any } = {} - if (prm) { - params = JSON.parse(prm) - } + // primaryApi.post( + // "/upload", + // requiresPermissions("upload"), + // multerSetup.single("file"), + // async (ctx) => { + // let acc = ctx.get("account") as Accounts.Account - files - .uploadFile( - { - owner: acc?.id, + // if (req.file) { + // try { + // let prm = req.header("monofile-params") + // let params: { [key: string]: any } = {} + // if (prm) { + // params = JSON.parse(prm) + // } - uploadId: params.uploadId, - filename: req.file.originalname, - mime: req.file.mimetype, - }, - req.file.buffer - ) - .then((uID) => res.send(uID)) - .catch((stat) => { - res.status(stat.status) - res.send(`[err] ${stat.message}`) - }) - } catch { - res.status(400) - res.send("[err] bad request") - } - } else { - res.status(400) - res.send("[err] bad request") - } - } - ) + // files + // .uploadFile( + // { + // owner: acc?.id, - primaryApi.post( - "/clone", - requiresPermissions("upload"), - bodyParser.json({ type: ["text/plain", "application/json"] }), - (req, res) => { - let acc = res.locals.acc as Accounts.Account + // uploadId: params.uploadId, + // filename: req.file.originalname, + // mime: req.file.mimetype, + // }, + // req.file.buffer + // ) + // .then((uID) => res.send(uID)) + // .catch((stat) => { + // res.status(stat.status) + // res.send(`[err] ${stat.message}`) + // }) + // } catch { + // res.status(400) + // res.send("[err] bad request") + // } + // } else { + // res.status(400) + // res.send("[err] bad request") + // } + // } + // ) - try { - axios - .get(req.body.url, { responseType: "arraybuffer" }) - .then((data: AxiosResponse) => { - files - .uploadFile( - { - owner: acc?.id, - filename: - req.body.url.split("/")[ - req.body.url.split("/").length - 1 - ] || "generic", - mime: data.headers["content-type"], - uploadId: req.body.uploadId, - }, - Buffer.from(data.data) - ) - .then((uID) => res.send(uID)) - .catch((stat) => { - res.status(stat.status) - res.send(`[err] ${stat.message}`) - }) - }) - .catch((err) => { - console.log(err) - res.status(400) - res.send(`[err] failed to fetch data`) - }) - } catch { - res.status(500) - res.send("[err] an error occured") - } - } - ) + // primaryApi.post( + // "/clone", + // requiresPermissions("upload"), + // async ctx => { + // let acc = ctx.get("account") as Accounts.Account + + // try { + // return axios + // .get(req.body.url, { responseType: "arraybuffer" }) + // .then((data: AxiosResponse) => { + // files + // .uploadFile( + // { + // owner: acc?.id, + // filename: + // req.body.url.split("/")[ + // req.body.url.split("/").length - 1 + // ] || "generic", + // mime: data.headers["content-type"], + // uploadId: req.body.uploadId, + // }, + // Buffer.from(data.data) + // ) + // .then((uID) => res.send(uID)) + // .catch((stat) => { + // res.status(stat.status) + // res.send(`[err] ${stat.message}`) + // }) + // }) + // .catch((err) => { + // console.log(err) + // return res.text(`[err] failed to fetch data`, 400) + // }) + // } catch { + // return ctx.text("[err] an error occured", 500) + // } + // } + // ) return primaryApi } diff --git a/src/server/routes/api/v1/account.ts b/src/server/routes/api/v1/account.ts index d6791d7..8a39cee 100644 --- a/src/server/routes/api/v1/account.ts +++ b/src/server/routes/api/v1/account.ts @@ -1,214 +1,223 @@ // Modules -import { writeFile } from 'fs' -import { Router } from "express"; -import bodyParser from "body-parser"; + +import { Hono } from "hono" +import { getCookie, setCookie } from "hono/cookie" // Libs -import Files, { id_check_regex } from "../../../lib/files"; -import * as Accounts from '../../../lib/accounts' -import * as Authentication from '../../../lib/auth' -import { assertAPI, getAccount, noAPIAccess, requiresAccount, requiresPermissions } from "../../../lib/middleware"; -import ServeError from "../../../lib/errors"; -import { sendMail } from '../../../lib/mail'; +import Files, { id_check_regex } from "../../../lib/files" +import * as Accounts from "../../../lib/accounts" +import * as Authentication from "../../../lib/auth" +import { + assertAPI, + getAccount, + noAPIAccess, + requiresAccount, + requiresPermissions, +} from "../../../lib/middleware" +import ServeError from "../../../lib/errors" +import { sendMail } from "../../../lib/mail" const Configuration = require(`${process.cwd()}/config.json`) -const parser = bodyParser.json({ - type: [ "type/plain", "application/json" ] -}) +const router = new Hono<{ + Variables: { + account: Accounts.Account + } +}>() -const router = Router() +router.use(getAccount) -router.use(getAccount, parser) - -module.exports = function(files: Files) { - router.post( - "/login", - (req, res) => { - if (typeof req.body.username != "string" || typeof req.body.password != "string") { - ServeError(res, 400, "please provide a username or password") - return - } - - if (Authentication.validate(req.cookies.auth)) { - ServeError(res, 400, "you are already logged in") - return - } - - const Account = Accounts.getFromUsername(req.body.username) - - if (!Account || !Accounts.password.check(Account.id, req.body.password)) { - ServeError(res, 400, "username or password incorrect") - return - } - - res.cookie("auth", - Authentication.create( - Account.id, // account id - (3 * 24 * 60 * 60 * 1000) // expiration time - ) - ) - res.status(200) - res.end() +module.exports = function (files: Files) { + router.post("/login", async (ctx, res) => { + const body = await ctx.req.json() + if ( + typeof body.username != "string" || + typeof body.password != "string" + ) { + ServeError(ctx, 400, "please provide a username or password") + return } - ) - router.post( - "/create", - (req, res) => { - if (!Configuration.accounts.registrationEnabled) { - ServeError(res , 403, "account registration disabled") - return + if (Authentication.validate(getCookie(ctx, "auth")!)) { + ServeError(ctx, 400, "you are already logged in") + return + } + + const Account = Accounts.getFromUsername(body.username) + + if (!Account || !Accounts.password.check(Account.id, body.password)) { + ServeError(ctx, 400, "username or password incorrect") + return + } + setCookie( + ctx, + "auth", + Authentication.create( + Account.id, // account id + 3 * 24 * 60 * 60 * 1000 // expiration time + ), + { + // expires: } + ) + ctx.status(200) + }) - if (Authentication.validate(req.cookies.auth)) { - ServeError(res, 400, "you are already logged in") - return - } + router.post("/create", async (ctx) => { + const body = await ctx.req.json() + if (!Configuration.accounts.registrationEnabled) { + return ServeError(ctx, 403, "account registration disabled") + } - if (Accounts.getFromUsername(req.body.username)) { - ServeError(res, 400, "account with this username already exists") - return - } + if (Authentication.validate(getCookie(ctx, "auth")!)) { + return ServeError(ctx, 400, "you are already logged in") + } - if (req.body.username.length < 3 || req.body.username.length > 20) { - ServeError(res, 400, "username must be over or equal to 3 characters or under or equal to 20 characters in length") - return - } + if (Accounts.getFromUsername(body.username)) { + return ServeError( + ctx, + 400, + "account with this username already exists" + ) + } - if ( - ( - req.body.username.match(/[A-Za-z0-9_\-\.]+/) - || - [] - )[0] != req.body.username - ) { - ServeError(res, 400, "username contains invalid characters") - return - } + if (body.username.length < 3 || body.username.length > 20) { + return ServeError( + ctx, + 400, + "username must be over or equal to 3 characters or under or equal to 20 characters in length" + ) + } - if (req.body.password.length < 8) { - ServeError(res, 400, "password must be 8 characters or longer") - return - } + if ( + (body.username.match(/[A-Za-z0-9_\-\.]+/) || [])[0] != body.username + ) { + return ServeError(ctx, 400, "username contains invalid characters") + } - Accounts.create( - req.body.username, - req.body.password - ).then((Account) => { - res.cookie("auth", Authentication.create( - Account, // account id - (3 * 24 * 60 * 60 * 1000) // expiration time - )) - res.status(200) - res.end() + if (body.password.length < 8) { + return ServeError( + ctx, + 400, + "password must be 8 characters or longer" + ) + } + + return Accounts.create(body.username, body.password) + .then((Account) => { + setCookie( + ctx, + "auth", + Authentication.create( + Account, // account id + 3 * 24 * 60 * 60 * 1000 // expiration time + ), + { + // expires: + } + ) + return ctx.status(200) }) .catch(() => { - ServeError(res, 500, "internal server error") + return ServeError(ctx, 500, "internal server error") }) - } - ) + }) - router.post( - "/logout", - (req, res) => { - if (!Authentication.validate(req.cookies.auth)) { - ServeError(res, 401, "not logged in") - return - } - - Authentication.invalidate(req.cookies.auth) - res.send("logged out") + router.post("/logout", (ctx) => { + if (!Authentication.validate(getCookie(ctx, "auth")!)) { + return ServeError(ctx, 401, "not logged in") } - ) + + Authentication.invalidate(getCookie(ctx, "auth")!) + return ctx.text("logged out") + }) router.patch( "/dfv", - requiresAccount, requiresPermissions("manage"), - (req, res) => { - const Account = res.locals.acc as Accounts.Account - - if (['public', 'private', 'anonymous'].includes(req.body.defaultFileVisibility)) { - Account.defaultFileVisibility = req.body.defaultFileVisibility - - Accounts.save() - - res.send(`dfv has been set to ${Account.defaultFileVisibility}`) - } else { - ServeError(res, 400, "invalid dfv") - } - } - ) - - router.delete("/me", - requiresAccount, noAPIAccess, - parser, - (req, res) => { - const Account = res.locals.acc as Accounts.Account - - const accountId = Account.id - - Authentication.AuthTokens.filter(e => e.account == accountId).forEach((token) => { - Authentication.invalidate(token.token) - }) - - Accounts.deleteAccount(accountId).then(_ => res.send("account deleted")) - } - ) - - router.patch("/me/name", requiresAccount, - noAPIAccess, - parser, - (req, res) => { - const Account = res.locals.acc as Accounts.Account - - const newUsername = req.body.username + requiresPermissions("manage"), + async (ctx) => { + const body = await ctx.req.json() + const Account = ctx.get("account")! as Accounts.Account if ( - typeof newUsername != "string" - || - newUsername.length < 3 - || - req.body.username.length > 20 + ["public", "private", "anonymous"].includes( + body.defaultFileVisibility + ) ) { - ServeError(res, 400, "username must be between 3 and 20 characters in length") - return - } + Account.defaultFileVisibility = body.defaultFileVisibility - if (Accounts.getFromUsername(newUsername)) { - ServeError(res, 400, "account with this username already exists") - } + Accounts.save() - if ( - ( - newUsername.match(/[A-Za-z0-9_\-\.]+/) - || - [] - )[0] != req.body.username - ) { - ServeError(res, 400, "username contains invalid characters") - return - } - - Account.username = newUsername - Accounts.save() - - if (Account.email) { - sendMail( - Account.email, - `Your login details have been updated`, - `Hello there! Your username has been updated to ${newUsername}. Please update your devices accordingly. Thank you for using monofile.` - ).then(() => { - res.send("OK") - }).catch((err) => {}) + return ctx.text( + `dfv has been set to ${Account.defaultFileVisibility}` + ) + } else { + return ServeError(ctx, 400, "invalid dfv") } } ) - + router.delete("/me", requiresAccount, noAPIAccess, async (ctx) => { + const Account = ctx.get("account") as Accounts.Account + const accountId = Account.id + + Authentication.AuthTokens.filter((e) => e.account == accountId).forEach( + (token) => { + Authentication.invalidate(token.token) + } + ) + + await Accounts.deleteAccount(accountId) + return ctx.text("account deleted") + }) + + router.patch("/me/name", requiresAccount, noAPIAccess, async (ctx) => { + const Account = ctx.get("account") as Accounts.Account + const body = await ctx.req.json() + const newUsername = body.username + + if ( + typeof newUsername != "string" || + newUsername.length < 3 || + newUsername.length > 20 + ) { + return ServeError( + ctx, + 400, + "username must be between 3 and 20 characters in length" + ) + } + + if (Accounts.getFromUsername(newUsername)) { + return ServeError( + ctx, + 400, + "account with this username already exists" + ) + } + + if ( + (newUsername.match(/[A-Za-z0-9_\-\.]+/) || [])[0] != body.username + ) { + ServeError(ctx, 400, "username contains invalid characters") + return + } + + Account.username = newUsername + Accounts.save() + + if (Account.email) { + await sendMail( + Account.email, + `Your login details have been updated`, + `Hello there! Your username has been updated to ${newUsername}. Please update your devices accordingly. Thank you for using monofile.` + ).catch() + return ctx.text("OK") + } + }) return router -} \ No newline at end of file +} diff --git a/src/server/routes/api/v1/admin.ts b/src/server/routes/api/v1/admin.ts index fc00b50..a56e821 100644 --- a/src/server/routes/api/v1/admin.ts +++ b/src/server/routes/api/v1/admin.ts @@ -1,120 +1,119 @@ // Modules -import { writeFile } from 'fs' -import { Router } from "express"; -import bodyParser from "body-parser"; +import { writeFile } from "fs/promises" +import { Hono } from "hono" // Libs -import Files, { id_check_regex } from "../../../lib/files"; -import * as Accounts from '../../../lib/accounts' -import * as Authentication from '../../../lib/auth' -import { assertAPI, getAccount, noAPIAccess, requiresAccount, requiresAdmin, requiresPermissions } from "../../../lib/middleware"; -import ServeError from "../../../lib/errors"; -import { sendMail } from '../../../lib/mail'; +import Files, { id_check_regex } from "../../../lib/files" +import * as Accounts from "../../../lib/accounts" +import * as Authentication from "../../../lib/auth" +import { + getAccount, + noAPIAccess, + requiresAccount, + requiresAdmin, +} from "../../../lib/middleware" +import ServeError from "../../../lib/errors" +import { sendMail } from "../../../lib/mail" const Configuration = require(`${process.cwd()}/config.json`) -const parser = bodyParser.json({ - type: [ "type/plain", "application/json" ] -}) +const router = new Hono<{ + Variables: { + account?: Accounts.Account + } +}>() -const router = Router() +router.use(getAccount, requiresAccount, requiresAdmin) -router.use(getAccount, requiresAccount, requiresAdmin, parser) +module.exports = function (files: Files) { + router.patch("/account/:username/password", async (ctx) => { + const Account = ctx.get("account") as Accounts.Account + const body = await ctx.req.json() -module.exports = function(files: Files) { - router.patch( - "/account/:username/password", - (req, res) => { - const Account = res.locals.acc as Accounts.Account + const targetUsername = ctx.req.param("username") + const password = body.password - const targetUsername = req.params.username - const password = req.body.password + if (typeof password !== "string") return ServeError(ctx, 404, "") - if (typeof password !== "string") { - ServeError(res, 404, "") - return - } + const targetAccount = Accounts.getFromUsername(targetUsername) - const targetAccount = Accounts.getFromUsername(targetUsername) + if (!targetAccount) return ServeError(ctx, 404, "") - if (!targetAccount) { - ServeError(res, 404, "") - return - } + Accounts.password.set(targetAccount.id, password) - Accounts.password.set( targetAccount.id, password ) - - Authentication.AuthTokens.filter(e => e.account == targetAccount?.id).forEach((accountToken) => { - Authentication.invalidate(accountToken.token) - }) + Authentication.AuthTokens.filter( + (e) => e.account == targetAccount?.id + ).forEach((accountToken) => { + Authentication.invalidate(accountToken.token) + }) - if (targetAccount.email) { - sendMail(targetAccount.email, `Your login details have been updated`, `Hello there! This email is to notify you of a password change that an administrator, ${Account.username}, has initiated. You have been logged out of your devices. Thank you for using monofile.`).then(() => { - res.send("OK") - }).catch((err) => {}) - } - - res.send() + if (targetAccount.email) { + await sendMail( + targetAccount.email, + `Your login details have been updated`, + `Hello there! This email is to notify you of a password change that an administrator, ${Account.username}, has initiated. You have been logged out of your devices. Thank you for using monofile.` + ).catch() } - ) - router.patch( - "/account/:username/elevate", - (req, res) => { - const targetUsername = req.params.username - const targetAccount = Accounts.getFromUsername(targetUsername) + return ctx.text("") + }) - if (!targetAccount) { - ServeError(res, 404, "") - return - } + router.patch("/account/:username/elevate", (ctx) => { + const targetUsername = ctx.req.param("username") + const targetAccount = Accounts.getFromUsername(targetUsername) - targetAccount.admin = true - Accounts.save() - - res.send() + if (!targetAccount) { + return ServeError(ctx, 404, "") } - ) - router.delete("/account/:username/:deleteFiles", + targetAccount.admin = true + Accounts.save() + + return ctx.text("") + }) + + router.delete( + "/account/:username/:deleteFiles", requiresAccount, noAPIAccess, - parser, - (req, res) => { - const targetUsername = req.params.username - const deleteFiles = req.params.deleteFiles + async (ctx) => { + const targetUsername = ctx.req.param("username") + const deleteFiles = ctx.req.param("deleteFiles") const targetAccount = Accounts.getFromUsername(targetUsername) - if (!targetAccount) { - ServeError(res, 404, "") - return - } + if (!targetAccount) return ServeError(ctx, 404, "") const accountId = targetAccount.id - Authentication.AuthTokens.filter(e => e.account == accountId).forEach((token) => { + Authentication.AuthTokens.filter( + (e) => e.account == accountId + ).forEach((token) => { Authentication.invalidate(token.token) }) - const deleteAccount = () => Accounts.deleteAccount(accountId).then(_ => res.send("account deleted")) + const deleteAccount = () => + Accounts.deleteAccount(accountId).then((_) => + ctx.text("account deleted") + ) if (deleteFiles) { - const Files = targetAccount.files.map(e => e) + const Files = targetAccount.files.map((e) => e) for (let fileId of Files) { - files.unlink(fileId, true).catch(err => console.error) + files.unlink(fileId, true).catch((err) => console.error) } - writeFile(process.cwd() + "/.data/files.json", JSON.stringify(files.files), (err) => { - if (err) console.log(err) - deleteAccount() - }) - } else deleteAccount() + await writeFile( + process.cwd() + "/.data/files.json", + JSON.stringify(files.files) + ) + return deleteAccount() + } else return deleteAccount() } ) return router -} \ No newline at end of file +} diff --git a/src/server/routes/api/v1/customization.ts b/src/server/routes/api/v1/customization.ts index 2986612..1a3308d 100644 --- a/src/server/routes/api/v1/customization.ts +++ b/src/server/routes/api/v1/customization.ts @@ -1,98 +1,97 @@ -// Modules - -import { Router } from "express"; -import bodyParser from "body-parser"; - -// Libs - -import Files, { id_check_regex } from "../../../lib/files"; -import * as Accounts from '../../../lib/accounts' -import { getAccount, requiresAccount, requiresPermissions } from "../../../lib/middleware"; -import ServeError from "../../../lib/errors"; +import { Hono } from "hono" +import Files, { id_check_regex } from "../../../lib/files" +import * as Accounts from "../../../lib/accounts" +import { + getAccount, + requiresAccount, + requiresPermissions, +} from "../../../lib/middleware" +import ServeError from "../../../lib/errors" const Configuration = require(`${process.cwd()}/config.json`) -const parser = bodyParser.json({ - type: [ "type/plain", "application/json" ] -}) +const router = new Hono<{ + Variables: { + account?: Accounts.Account + } +}>() -const router = Router() +router.use(getAccount) -router.use(getAccount, parser) - -module.exports = function(files: Files) { +module.exports = function (files: Files) { router.put( "/css", - requiresAccount, requiresPermissions("customize"), - async (req, res) => { - const Account = res.locals.acc as Accounts.Account - - if (typeof req.body.fileId != "string") req.body.fileId = undefined; - - if ( - !req.body.fileId - || - (req.body.fileId.match(id_check_regex) == req.body.fileId - && req.body.fileId.length <= Configuration.maxUploadIdLength) - ) { - Account.customCSS = req.body.fileId || undefined - - await Accounts.save() - res.send("custom css saved") - } else ServeError(res, 400, "invalid fileId") - } - ) - - router.get('/css', requiresAccount, - (req, res) => { - const Account = res.locals.acc + requiresPermissions("customize"), + async (ctx) => { + const Account = ctx.get("account") as Accounts.Account + const body = await ctx.req.json() + if (typeof body.fileId != "string") body.fileId = undefined - if (Account?.customCSS) res.redirect(`/file/${Account.customCSS}`) - else res.send(""); - } - ) - - router.put("/embed/color", - requiresAccount, requiresPermissions("customize"), - async (req, res) => { - const Account = res.locals.acc as Accounts.Account - - if (typeof req.body.color != "string") req.body.color = undefined; - if ( - !req.body.color - || (req.body.color.toLowerCase().match(/[a-f0-9]+/) == req.body.color.toLowerCase()) - && req.body.color.length == 6 + !body.fileId || + (body.fileId.match(id_check_regex) == body.fileId && + body.fileId.length <= Configuration.maxUploadIdLength) ) { - - if (!Account.embed) Account.embed = {}; - Account.embed.color = req.body.color || undefined + Account.customCSS = body.fileId || undefined await Accounts.save() - res.send("custom embed color saved") - - } else ServeError(res,400,"invalid hex code") + return ctx.text("custom css saved") + } else return ServeError(ctx, 400, "invalid fileId") } ) - router.put("/embed/size", - requiresAccount, requiresPermissions("customize"), - async (req, res) => { - const Account = res.locals.acc as Accounts.Account + router.get("/css", requiresAccount, async (ctx) => { + const Account = ctx.get("account") - if (typeof req.body.largeImage != "boolean") { - ServeError(res, 400, "largeImage must be bool"); + if (Account?.customCSS) + return ctx.redirect(`/file/${Account.customCSS}`) + else return ctx.text("") + }) + + router.put( + "/embed/color", + requiresAccount, + requiresPermissions("customize"), + async (ctx) => { + const Account = ctx.get("account") as Accounts.Account + const body = await ctx.req.json() + if (typeof body.color != "string") body.color = undefined + + if ( + !body.color || + (body.color.toLowerCase().match(/[a-f0-9]+/) == + body.color.toLowerCase() && + body.color.length == 6) + ) { + if (!Account.embed) Account.embed = {} + Account.embed.color = body.color || undefined + + await Accounts.save() + return ctx.text("custom embed color saved") + } else return ServeError(ctx, 400, "invalid hex code") + } + ) + + router.put( + "/embed/size", + requiresAccount, + requiresPermissions("customize"), + async (ctx) => { + const Account = ctx.get("account") as Accounts.Account + const body = await ctx.req.json() + if (typeof body.largeImage != "boolean") { + ServeError(ctx, 400, "largeImage must be bool") return } - if (!Account.embed) Account.embed = {}; - Account.embed.largeImage = req.body.largeImage + if (!Account.embed) Account.embed = {} + Account.embed.largeImage = body.largeImage await Accounts.save() - res.send(`custom embed image size saved`) + return ctx.text(`custom embed image size saved`) } ) return router -} \ No newline at end of file +} diff --git a/src/server/routes/api/v1/file.ts b/src/server/routes/api/v1/file.ts index 8c8168d..8511bc7 100644 --- a/src/server/routes/api/v1/file.ts +++ b/src/server/routes/api/v1/file.ts @@ -1,8 +1,8 @@ -import { Router } from "express"; +import { Hono } from "hono"; import Files from "../../../lib/files"; -let router = Router() +const router = new Hono() module.exports = function(files: Files) { return router -} \ No newline at end of file +} diff --git a/src/server/routes/api/v1/public.ts b/src/server/routes/api/v1/public.ts index 8c8168d..09ce314 100644 --- a/src/server/routes/api/v1/public.ts +++ b/src/server/routes/api/v1/public.ts @@ -1,8 +1,8 @@ -import { Router } from "express"; -import Files from "../../../lib/files"; +import { Hono } from "hono" +import Files from "../../../lib/files" -let router = Router() +const router = new Hono() -module.exports = function(files: Files) { +module.exports = function (files: Files) { return router -} \ No newline at end of file +}