mirror of
https://github.com/mollersuite/monofile.git
synced 2024-11-21 05:26:27 -08:00
refactor: ♻️ Honofile.
This commit is contained in:
parent
6220cd8b0f
commit
0366c91f74
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,48 +1,50 @@
|
|||
import crypto from "crypto"
|
||||
import express from "express"
|
||||
import { getCookie } from "hono/cookie"
|
||||
import type { Context } from "hono"
|
||||
import { readFile, writeFile } from "fs/promises"
|
||||
export let AuthTokens: AuthToken[] = []
|
||||
export let AuthTokenTO:{[key:string]:NodeJS.Timeout} = {}
|
||||
export let AuthTokenTO: { [key: string]: NodeJS.Timeout } = {}
|
||||
|
||||
export const ValidTokenPermissions = [
|
||||
"user", // permissions to /auth/me, with email docked
|
||||
"email", // adds email back to /auth/me
|
||||
"private", // allows app to read private files
|
||||
"upload", // allows an app to upload under an account
|
||||
"manage", // allows an app to manage an account's files
|
||||
"user", // permissions to /auth/me, with email docked
|
||||
"email", // adds email back to /auth/me
|
||||
"private", // allows app to read private files
|
||||
"upload", // allows an app to upload under an account
|
||||
"manage", // allows an app to manage an account's files
|
||||
"customize", // allows an app to change customization settings
|
||||
"admin" // only available for accounts with admin
|
||||
// gives an app access to all admin tools
|
||||
"admin", // only available for accounts with admin
|
||||
// gives an app access to all admin tools
|
||||
] as const
|
||||
|
||||
export type TokenType = "User" | "App"
|
||||
export type TokenPermission = typeof ValidTokenPermissions[number]
|
||||
export type TokenPermission = (typeof ValidTokenPermissions)[number]
|
||||
|
||||
export interface AuthToken {
|
||||
account: string,
|
||||
token: string,
|
||||
expire: number,
|
||||
account: string
|
||||
token: string
|
||||
expire: number
|
||||
|
||||
type?: TokenType, // if !type, assume User
|
||||
type?: TokenType // if !type, assume User
|
||||
tokenPermissions?: TokenPermission[] // default to user if type is App,
|
||||
// give full permissions if type is User
|
||||
// give full permissions if type is User
|
||||
}
|
||||
|
||||
export function create(
|
||||
id:string,
|
||||
expire:number=(24*60*60*1000),
|
||||
type:TokenType="User",
|
||||
tokenPermissions?:TokenPermission[]
|
||||
id: string,
|
||||
expire: number = 24 * 60 * 60 * 1000,
|
||||
type: TokenType = "User",
|
||||
tokenPermissions?: TokenPermission[]
|
||||
) {
|
||||
let token = {
|
||||
account:id,
|
||||
token:crypto.randomBytes(36).toString('hex'),
|
||||
expire: expire ? Date.now()+expire : 0,
|
||||
account: id,
|
||||
token: crypto.randomBytes(36).toString("hex"),
|
||||
expire: expire ? Date.now() + expire : 0,
|
||||
|
||||
type,
|
||||
tokenPermissions: type == "App" ? tokenPermissions || ["user"] : undefined
|
||||
tokenPermissions:
|
||||
type == "App" ? tokenPermissions || ["user"] : undefined,
|
||||
}
|
||||
|
||||
|
||||
AuthTokens.push(token)
|
||||
tokenTimer(token)
|
||||
|
||||
|
@ -51,56 +53,68 @@ export function create(
|
|||
return token.token
|
||||
}
|
||||
|
||||
export function tokenFor(req: express.Request) {
|
||||
return req.cookies.auth || (
|
||||
req.header("authorization")?.startsWith("Bearer ")
|
||||
? req.header("authorization")?.split(" ")[1]
|
||||
: undefined
|
||||
export function tokenFor(ctx: Context) {
|
||||
return (
|
||||
getCookie(ctx, "auth") ||
|
||||
(ctx.req.header("authorization")?.startsWith("Bearer ")
|
||||
? ctx.req.header("authorization")?.split(" ")[1]
|
||||
: undefined)
|
||||
)
|
||||
}
|
||||
|
||||
function getToken(token:string) {
|
||||
return AuthTokens.find(e => e.token == token && (e.expire == 0 || Date.now() < e.expire))
|
||||
function getToken(token: string) {
|
||||
return AuthTokens.find(
|
||||
(e) => e.token == token && (e.expire == 0 || Date.now() < e.expire)
|
||||
)
|
||||
}
|
||||
|
||||
export function validate(token:string) {
|
||||
export function validate(token: string) {
|
||||
return getToken(token)?.account
|
||||
}
|
||||
|
||||
export function getType(token:string): TokenType | undefined {
|
||||
export function getType(token: string): TokenType | undefined {
|
||||
return getToken(token)?.type
|
||||
}
|
||||
|
||||
export function getPermissions(token:string): TokenPermission[] | undefined {
|
||||
export function getPermissions(token: string): TokenPermission[] | undefined {
|
||||
return getToken(token)?.tokenPermissions
|
||||
}
|
||||
|
||||
export function tokenTimer(token:AuthToken) {
|
||||
export function tokenTimer(token: AuthToken) {
|
||||
if (!token.expire) return // justincase
|
||||
if (Date.now() >= token.expire) {
|
||||
invalidate(token.token)
|
||||
return
|
||||
}
|
||||
|
||||
AuthTokenTO[token.token] = setTimeout(() => invalidate(token.token),token.expire-Date.now())
|
||||
AuthTokenTO[token.token] = setTimeout(
|
||||
() => invalidate(token.token),
|
||||
token.expire - Date.now()
|
||||
)
|
||||
}
|
||||
|
||||
export function invalidate(token:string) {
|
||||
export function invalidate(token: string) {
|
||||
if (AuthTokenTO[token]) {
|
||||
clearTimeout(AuthTokenTO[token])
|
||||
}
|
||||
|
||||
AuthTokens.splice(AuthTokens.findIndex(e => e.token == token),1)
|
||||
AuthTokens.splice(
|
||||
AuthTokens.findIndex((e) => e.token == token),
|
||||
1
|
||||
)
|
||||
save()
|
||||
}
|
||||
|
||||
export function save() {
|
||||
writeFile(`${process.cwd()}/.data/tokens.json`,JSON.stringify(AuthTokens))
|
||||
.catch((err) => console.error(err))
|
||||
writeFile(
|
||||
`${process.cwd()}/.data/tokens.json`,
|
||||
JSON.stringify(AuthTokens)
|
||||
).catch((err) => console.error(err))
|
||||
}
|
||||
|
||||
readFile(`${process.cwd()}/.data/tokens.json`)
|
||||
.then((buf) => {
|
||||
AuthTokens = JSON.parse(buf.toString())
|
||||
AuthTokens.forEach(e => tokenTimer(e))
|
||||
}).catch(err => console.error(err))
|
||||
AuthTokens.forEach((e) => tokenTimer(e))
|
||||
})
|
||||
.catch((err) => console.error(err))
|
||||
|
|
|
@ -1,48 +1,36 @@
|
|||
import { Response } from "express";
|
||||
import { readFile } from "fs/promises"
|
||||
import type { Context } from "hono"
|
||||
|
||||
let errorPage:string
|
||||
let errorPage: string
|
||||
|
||||
/**
|
||||
* @description Serves an error as a response to a request with an error page attached
|
||||
* @param res Express response object
|
||||
* @param ctx Express response object
|
||||
* @param code Error code
|
||||
* @param reason Error reason
|
||||
*/
|
||||
export default async function ServeError(
|
||||
res:Response,
|
||||
code:number,
|
||||
reason:string
|
||||
ctx: Context,
|
||||
code: number,
|
||||
reason: string
|
||||
) {
|
||||
// fetch error page if not cached
|
||||
if (!errorPage) {
|
||||
errorPage =
|
||||
(
|
||||
await readFile(`${process.cwd()}/dist/error.html`)
|
||||
.catch((err) => console.error(err))
|
||||
|| "<pre>$code $text</pre>"
|
||||
)
|
||||
.toString()
|
||||
errorPage = (
|
||||
(await readFile(`${process.cwd()}/dist/error.html`).catch((err) =>
|
||||
console.error(err)
|
||||
)) || "<pre>$code $text</pre>"
|
||||
).toString()
|
||||
}
|
||||
|
||||
// serve error
|
||||
res.statusMessage = reason
|
||||
res.status(code)
|
||||
res.header("x-backup-status-message", reason) // glitch default nginx configuration
|
||||
res.send(
|
||||
return ctx.html(
|
||||
errorPage
|
||||
.replaceAll("$code",code.toString())
|
||||
.replaceAll("$text",reason)
|
||||
.replaceAll("$code", code.toString())
|
||||
.replaceAll("$text", reason),
|
||||
code,
|
||||
{
|
||||
"x-backup-status-message": reason, // glitch default nginx configuration
|
||||
}
|
||||
)
|
||||
}
|
||||
/**
|
||||
* @description Redirects a user to another page.
|
||||
* @param res Express response object
|
||||
* @param url Target URL
|
||||
* @deprecated Use `res.redirect` instead.
|
||||
*/
|
||||
export function Redirect(res:Response,url:string) {
|
||||
res.status(302)
|
||||
res.header("Location",url)
|
||||
res.send()
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,31 +2,32 @@ import fs from "fs/promises"
|
|||
import bytes from "bytes"
|
||||
import ServeError from "./lib/errors"
|
||||
import * as Accounts from "./lib/accounts"
|
||||
import type { Handler } from "express"
|
||||
import type { Handler } from "hono"
|
||||
import type Files from "./lib/files"
|
||||
const pkg = require(`${process.cwd()}/package.json`)
|
||||
export = (files: Files): Handler =>
|
||||
async (req, res) => {
|
||||
let acc = res.locals.acc as Accounts.Account
|
||||
const file = files.getFilePointer(req.params.fileId)
|
||||
async (ctx) => {
|
||||
let acc = ctx.get("account") as Accounts.Account
|
||||
const fileId = ctx.req.param("fileId")
|
||||
const host = ctx.req.header("Host")
|
||||
const file = files.getFilePointer(fileId)
|
||||
if (file) {
|
||||
if (file.visibility == "private" && acc?.id != file.owner) {
|
||||
ServeError(res, 403, "you do not own this file")
|
||||
return
|
||||
return ServeError(ctx, 403, "you do not own this file")
|
||||
}
|
||||
|
||||
const template = await fs
|
||||
.readFile(process.cwd() + "/dist/download.html", "utf8")
|
||||
.catch(() => {
|
||||
throw res.sendStatus(500)
|
||||
throw ctx.status(500)
|
||||
})
|
||||
let fileOwner = file.owner
|
||||
? Accounts.getFromId(file.owner)
|
||||
: undefined
|
||||
|
||||
res.send(
|
||||
return ctx.html(
|
||||
template
|
||||
.replaceAll("$FileId", req.params.fileId)
|
||||
.replaceAll("$FileId", fileId)
|
||||
.replaceAll("$Version", pkg.version)
|
||||
.replaceAll(
|
||||
"$FileSize",
|
||||
|
@ -44,18 +45,14 @@ export = (files: Files): Handler =>
|
|||
.replace(
|
||||
"<!--metaTags-->",
|
||||
(file.mime.startsWith("image/")
|
||||
? `<meta name="og:image" content="https://${req.headers.host}/file/${req.params.fileId}" />`
|
||||
? `<meta name="og:image" content="https://${host}/file/${fileId}" />`
|
||||
: file.mime.startsWith("video/")
|
||||
? `<meta property="og:video:url" content="https://${
|
||||
req.headers.host
|
||||
}/cpt/${req.params.fileId}/video.${
|
||||
? `<meta property="og:video:url" content="https://${host}/cpt/${fileId}/video.${
|
||||
file.mime.split("/")[1] == "quicktime"
|
||||
? "mov"
|
||||
: file.mime.split("/")[1]
|
||||
}" />
|
||||
<meta property="og:video:secure_url" content="https://${
|
||||
req.headers.host
|
||||
}/cpt/${req.params.fileId}/video.${
|
||||
<meta property="og:video:secure_url" content="https://${host}/cpt/${fileId}/video.${
|
||||
file.mime.split("/")[1] == "quicktime"
|
||||
? "mov"
|
||||
: file.mime.split("/")[1]
|
||||
|
@ -79,7 +76,7 @@ export = (files: Files): Handler =>
|
|||
`\n<meta name="theme-color" content="${
|
||||
fileOwner?.embed?.color &&
|
||||
file.visibility != "anonymous" &&
|
||||
(req.headers["user-agent"] || "").includes(
|
||||
(ctx.req.header("user-agent") || "").includes(
|
||||
"Discordbot"
|
||||
)
|
||||
? `#${fileOwner.embed.color}`
|
||||
|
@ -89,11 +86,11 @@ export = (files: Files): Handler =>
|
|||
.replace(
|
||||
"<!--preview-->",
|
||||
file.mime.startsWith("image/")
|
||||
? `<div style="min-height:10px"></div><img src="/file/${req.params.fileId}" />`
|
||||
? `<div style="min-height:10px"></div><img src="/file/${fileId}" />`
|
||||
: file.mime.startsWith("video/")
|
||||
? `<div style="min-height:10px"></div><video src="/file/${req.params.fileId}" controls></video>`
|
||||
? `<div style="min-height:10px"></div><video src="/file/${fileId}" controls></video>`
|
||||
: file.mime.startsWith("audio/")
|
||||
? `<div style="min-height:10px"></div><audio src="/file/${req.params.fileId}" controls></audio>`
|
||||
? `<div style="min-height:10px"></div><audio src="/file/${fileId}" controls></audio>`
|
||||
: ""
|
||||
)
|
||||
.replaceAll(
|
||||
|
@ -104,6 +101,6 @@ export = (files: Files): Handler =>
|
|||
)
|
||||
)
|
||||
} else {
|
||||
ServeError(res, 404, "file not found")
|
||||
ServeError(ctx, 404, "file not found")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,20 +1,21 @@
|
|||
import bodyParser from "body-parser";
|
||||
import { Router } from "express";
|
||||
import * as Accounts from "../../../lib/accounts";
|
||||
import * as auth from "../../../lib/auth";
|
||||
import bytes from "bytes"
|
||||
import {writeFile} from "fs";
|
||||
import { sendMail } from "../../../lib/mail";
|
||||
import { getAccount, requiresAccount, requiresAdmin, requiresPermissions } from "../../../lib/middleware"
|
||||
import { Hono } from "hono"
|
||||
import * as Accounts from "../../../lib/accounts"
|
||||
import * as auth from "../../../lib/auth"
|
||||
import { writeFile } from "fs/promises"
|
||||
import { sendMail } from "../../../lib/mail"
|
||||
import {
|
||||
getAccount,
|
||||
requiresAccount,
|
||||
requiresAdmin,
|
||||
requiresPermissions,
|
||||
} from "../../../lib/middleware"
|
||||
import Files from "../../../lib/files"
|
||||
|
||||
import ServeError from "../../../lib/errors";
|
||||
import Files from "../../../lib/files";
|
||||
|
||||
let parser = bodyParser.json({
|
||||
type: ["text/plain","application/json"]
|
||||
})
|
||||
|
||||
export let adminRoutes = Router();
|
||||
export let adminRoutes = new Hono<{
|
||||
Variables: {
|
||||
account: Accounts.Account
|
||||
}
|
||||
}>()
|
||||
adminRoutes
|
||||
.use(getAccount)
|
||||
.use(requiresAccount)
|
||||
|
@ -23,214 +24,198 @@ adminRoutes
|
|||
|
||||
let config = require(`${process.cwd()}/config.json`)
|
||||
|
||||
module.exports = function(files: Files) {
|
||||
|
||||
module.exports = function (files: Files) {
|
||||
adminRoutes.post("/reset", async (ctx) => {
|
||||
let acc = ctx.get("account") as Accounts.Account
|
||||
const body = await ctx.req.json()
|
||||
|
||||
adminRoutes.post("/reset", parser, (req,res) => {
|
||||
|
||||
let acc = res.locals.acc as Accounts.Account
|
||||
|
||||
if (typeof req.body.target !== "string" || typeof req.body.password !== "string") {
|
||||
res.status(404)
|
||||
res.send()
|
||||
return
|
||||
if (
|
||||
typeof body.target !== "string" ||
|
||||
typeof body.password !== "string"
|
||||
) {
|
||||
return ctx.status(404)
|
||||
}
|
||||
|
||||
let targetAccount = Accounts.getFromUsername(req.body.target)
|
||||
let targetAccount = Accounts.getFromUsername(body.target)
|
||||
if (!targetAccount) {
|
||||
res.status(404)
|
||||
res.send()
|
||||
return
|
||||
return ctx.status(404)
|
||||
}
|
||||
|
||||
Accounts.password.set ( targetAccount.id, req.body.password )
|
||||
auth.AuthTokens.filter(e => e.account == targetAccount?.id).forEach((v) => {
|
||||
auth.invalidate(v.token)
|
||||
})
|
||||
Accounts.password.set(targetAccount.id, body.password)
|
||||
auth.AuthTokens.filter((e) => e.account == targetAccount?.id).forEach(
|
||||
(v) => {
|
||||
auth.invalidate(v.token)
|
||||
}
|
||||
)
|
||||
|
||||
if (targetAccount.email) {
|
||||
sendMail(targetAccount.email, `Your login details have been updated`, `<b>Hello there!</b> This email is to notify you of a password change that an administrator, <span username>${acc.username}</span>, has initiated. You have been logged out of your devices. Thank you for using monofile.`).then(() => {
|
||||
res.send("OK")
|
||||
}).catch((err) => {})
|
||||
return sendMail(
|
||||
targetAccount.email,
|
||||
`Your login details have been updated`,
|
||||
`<b>Hello there!</b> This email is to notify you of a password change that an administrator, <span username>${acc.username}</span>, has initiated. You have been logged out of your devices. Thank you for using monofile.`
|
||||
)
|
||||
.then(() => ctx.text("OK"))
|
||||
.catch(() => ctx.status(500))
|
||||
}
|
||||
|
||||
|
||||
res.send()
|
||||
|
||||
})
|
||||
|
||||
adminRoutes.post("/elevate", parser, (req,res) => {
|
||||
adminRoutes.post("/elevate", async (ctx) => {
|
||||
const body = await ctx.req.json()
|
||||
let acc = ctx.get("account") as Accounts.Account
|
||||
|
||||
let acc = res.locals.acc as Accounts.Account
|
||||
|
||||
if (typeof req.body.target !== "string") {
|
||||
res.status(404)
|
||||
res.send()
|
||||
return
|
||||
if (typeof body.target !== "string") {
|
||||
return ctx.status(404)
|
||||
}
|
||||
|
||||
let targetAccount = Accounts.getFromUsername(req.body.target)
|
||||
let targetAccount = Accounts.getFromUsername(body.target)
|
||||
if (!targetAccount) {
|
||||
res.status(404)
|
||||
res.send()
|
||||
return
|
||||
return ctx.status(404)
|
||||
}
|
||||
|
||||
targetAccount.admin = true;
|
||||
Accounts.save()
|
||||
res.send()
|
||||
|
||||
return ctx.text("OK")
|
||||
})
|
||||
|
||||
adminRoutes.post("/delete", parser, (req,res) => {
|
||||
|
||||
if (typeof req.body.target !== "string") {
|
||||
res.status(404)
|
||||
res.send()
|
||||
return
|
||||
adminRoutes.post("/delete", async (ctx) => {
|
||||
const body = await ctx.req.json()
|
||||
if (typeof body.target !== "string") {
|
||||
return ctx.status(404)
|
||||
}
|
||||
|
||||
let targetFile = files.getFilePointer(req.body.target)
|
||||
let targetFile = files.getFilePointer(body.target)
|
||||
|
||||
if (!targetFile) {
|
||||
res.status(404)
|
||||
res.send()
|
||||
return
|
||||
return ctx.status(404)
|
||||
}
|
||||
|
||||
files.unlink(req.body.target).then(() => {
|
||||
res.status(200)
|
||||
}).catch(() => {
|
||||
res.status(500)
|
||||
}).finally(() => res.send())
|
||||
|
||||
return files
|
||||
.unlink(body.target)
|
||||
.then(() => ctx.status(200))
|
||||
.catch(() => ctx.status(500))
|
||||
.finally(() => ctx.status(200))
|
||||
})
|
||||
|
||||
adminRoutes.post("/delete_account", parser, async (req,res) => {
|
||||
|
||||
let acc = res.locals.acc as Accounts.Account
|
||||
|
||||
if (typeof req.body.target !== "string") {
|
||||
res.status(404)
|
||||
res.send()
|
||||
return
|
||||
adminRoutes.post("/delete_account", async (ctx) => {
|
||||
let acc = ctx.get("account") as Accounts.Account
|
||||
const body = await ctx.req.json()
|
||||
if (typeof body.target !== "string") {
|
||||
return ctx.status(404)
|
||||
}
|
||||
|
||||
let targetAccount = Accounts.getFromUsername(req.body.target)
|
||||
let targetAccount = Accounts.getFromUsername(body.target)
|
||||
if (!targetAccount) {
|
||||
res.status(404)
|
||||
res.send()
|
||||
return
|
||||
return ctx.status(404)
|
||||
}
|
||||
|
||||
let accId = targetAccount.id
|
||||
|
||||
auth.AuthTokens.filter(e => e.account == accId).forEach((v) => {
|
||||
auth.AuthTokens.filter((e) => e.account == accId).forEach((v) => {
|
||||
auth.invalidate(v.token)
|
||||
})
|
||||
|
||||
let cpl = () => Accounts.deleteAccount(accId).then(_ => {
|
||||
if (targetAccount?.email) {
|
||||
sendMail(targetAccount.email, "Notice of account deletion", `Your account, <span username>${targetAccount.username}</span>, has been deleted by <span username>${acc.username}</span> for the following reason: <br><br><span style="font-weight:600">${req.body.reason || "(no reason specified)"}</span><br><br> Your files ${req.body.deleteFiles ? "have been deleted" : "have not been modified"}. Thank you for using monofile.`)
|
||||
}
|
||||
res.send("account deleted")
|
||||
})
|
||||
|
||||
if (req.body.deleteFiles) {
|
||||
let f = targetAccount.files.map(e=>e) // make shallow copy so that iterating over it doesnt Die
|
||||
let cpl = () =>
|
||||
Accounts.deleteAccount(accId).then((_) => {
|
||||
if (targetAccount?.email) {
|
||||
sendMail(
|
||||
targetAccount.email,
|
||||
"Notice of account deletion",
|
||||
`Your account, <span username>${
|
||||
targetAccount.username
|
||||
}</span>, has been deleted by <span username>${
|
||||
acc.username
|
||||
}</span> for the following reason: <br><br><span style="font-weight:600">${
|
||||
body.reason || "(no reason specified)"
|
||||
}</span><br><br> Your files ${
|
||||
body.deleteFiles
|
||||
? "have been deleted"
|
||||
: "have not been modified"
|
||||
}. Thank you for using monofile.`
|
||||
)
|
||||
}
|
||||
return ctx.text("account deleted")
|
||||
})
|
||||
|
||||
if (body.deleteFiles) {
|
||||
let f = targetAccount.files.map((e) => e) // make shallow copy so that iterating over it doesnt Die
|
||||
for (let v of f) {
|
||||
files.unlink(v,true).catch(err => console.error(err))
|
||||
files.unlink(v, true).catch((err) => console.error(err))
|
||||
}
|
||||
|
||||
writeFile(process.cwd()+"/.data/files.json",JSON.stringify(files.files), (err) => {
|
||||
if (err) console.log(err)
|
||||
cpl()
|
||||
})
|
||||
} else cpl()
|
||||
return writeFile(
|
||||
process.cwd() + "/.data/files.json",
|
||||
JSON.stringify(files.files)
|
||||
).then(cpl)
|
||||
} else return cpl()
|
||||
})
|
||||
|
||||
adminRoutes.post("/transfer", parser, (req,res) => {
|
||||
|
||||
if (typeof req.body.target !== "string" || typeof req.body.owner !== "string") {
|
||||
res.status(404)
|
||||
res.send()
|
||||
return
|
||||
}
|
||||
|
||||
let targetFile = files.getFilePointer(req.body.target)
|
||||
if (!targetFile) {
|
||||
res.status(404)
|
||||
res.send()
|
||||
return
|
||||
adminRoutes.post("/transfer", async (ctx) => {
|
||||
const body = await ctx.req.json()
|
||||
if (typeof body.target !== "string" || typeof body.owner !== "string") {
|
||||
return ctx.status(404)
|
||||
}
|
||||
|
||||
let newOwner = Accounts.getFromUsername(req.body.owner || "")
|
||||
let targetFile = files.getFilePointer(body.target)
|
||||
if (!targetFile) {
|
||||
return ctx.status(404)
|
||||
}
|
||||
|
||||
let newOwner = Accounts.getFromUsername(body.owner || "")
|
||||
|
||||
// clear old owner
|
||||
|
||||
if (targetFile.owner) {
|
||||
let oldOwner = Accounts.getFromId(targetFile.owner)
|
||||
if (oldOwner) {
|
||||
Accounts.files.deindex(oldOwner.id, req.body.target)
|
||||
}
|
||||
Accounts.files.deindex(oldOwner.id, body.target)
|
||||
}
|
||||
}
|
||||
|
||||
if (newOwner) {
|
||||
Accounts.files.index(newOwner.id, req.body.target)
|
||||
Accounts.files.index(newOwner.id, body.target)
|
||||
}
|
||||
targetFile.owner = newOwner ? newOwner.id : undefined;
|
||||
|
||||
files.writeFile(req.body.target, targetFile).then(() => {
|
||||
res.send()
|
||||
}).catch(() => {
|
||||
res.status(500)
|
||||
res.send()
|
||||
}) // wasting a reassignment but whatee
|
||||
targetFile.owner = newOwner ? newOwner.id : undefined
|
||||
|
||||
files
|
||||
.writeFile(body.target, targetFile)
|
||||
.then(() => ctx.status(200))
|
||||
.catch(() => ctx.status(500))
|
||||
})
|
||||
|
||||
adminRoutes.post("/idchange", parser, (req,res) => {
|
||||
|
||||
if (typeof req.body.target !== "string" || typeof req.body.new !== "string") {
|
||||
res.status(400)
|
||||
res.send()
|
||||
return
|
||||
adminRoutes.post("/idchange", async (ctx) => {
|
||||
const body = await ctx.req.json()
|
||||
if (typeof body.target !== "string" || typeof body.new !== "string") {
|
||||
return ctx.status(400)
|
||||
}
|
||||
|
||||
let targetFile = files.getFilePointer(req.body.target)
|
||||
|
||||
let targetFile = files.getFilePointer(body.target)
|
||||
if (!targetFile) {
|
||||
res.status(404)
|
||||
res.send()
|
||||
return
|
||||
return ctx.status(404)
|
||||
}
|
||||
|
||||
if (files.getFilePointer(req.body.new)) {
|
||||
res.status(400)
|
||||
res.send()
|
||||
return
|
||||
|
||||
if (files.getFilePointer(body.new)) {
|
||||
return ctx.status(400)
|
||||
}
|
||||
|
||||
if (targetFile.owner) {
|
||||
Accounts.files.deindex(targetFile.owner, req.body.target)
|
||||
Accounts.files.index(targetFile.owner, req.body.new)
|
||||
Accounts.files.deindex(targetFile.owner, body.target)
|
||||
Accounts.files.index(targetFile.owner, body.new)
|
||||
}
|
||||
delete files.files[req.body.target]
|
||||
delete files.files[body.target]
|
||||
|
||||
files.writeFile(req.body.new, targetFile).then(() => {
|
||||
res.send()
|
||||
}).catch(() => {
|
||||
files.files[req.body.target] = req.body.new
|
||||
return files
|
||||
.writeFile(body.new, targetFile)
|
||||
.then(() => ctx.status(200))
|
||||
.catch(() => {
|
||||
files.files[body.target] = body.new
|
||||
|
||||
if (targetFile.owner) {
|
||||
Accounts.files.deindex(targetFile.owner, req.body.new)
|
||||
Accounts.files.index(targetFile.owner, req.body.target)
|
||||
}
|
||||
|
||||
res.status(500)
|
||||
res.send()
|
||||
})
|
||||
if (targetFile.owner) {
|
||||
Accounts.files.deindex(targetFile.owner, body.new)
|
||||
Accounts.files.index(targetFile.owner, body.target)
|
||||
}
|
||||
|
||||
return ctx.status(500)
|
||||
})
|
||||
})
|
||||
|
||||
return adminRoutes
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,356 +1,483 @@
|
|||
import bodyParser from "body-parser";
|
||||
import { Router } from "express";
|
||||
import * as Accounts from "../../../lib/accounts";
|
||||
import * as auth from "../../../lib/auth";
|
||||
import { sendMail } from "../../../lib/mail";
|
||||
import { getAccount, noAPIAccess, requiresAccount, requiresPermissions } from "../../../lib/middleware"
|
||||
import { Hono, Handler } from "hono"
|
||||
import { getCookie, setCookie } from "hono/cookie"
|
||||
import * as Accounts from "../../../lib/accounts"
|
||||
import * as auth from "../../../lib/auth"
|
||||
import { sendMail } from "../../../lib/mail"
|
||||
import {
|
||||
getAccount,
|
||||
noAPIAccess,
|
||||
requiresAccount,
|
||||
requiresPermissions,
|
||||
} from "../../../lib/middleware"
|
||||
import { accountRatelimit } from "../../../lib/ratelimit"
|
||||
|
||||
import ServeError from "../../../lib/errors";
|
||||
import Files, { FileVisibility, generateFileId, id_check_regex } from "../../../lib/files";
|
||||
import ServeError from "../../../lib/errors"
|
||||
import Files, {
|
||||
FileVisibility,
|
||||
generateFileId,
|
||||
id_check_regex,
|
||||
} from "../../../lib/files"
|
||||
|
||||
import { writeFile } from "fs";
|
||||
import { writeFile } from "fs/promises"
|
||||
|
||||
let parser = bodyParser.json({
|
||||
type: ["text/plain","application/json"]
|
||||
})
|
||||
|
||||
export let authRoutes = Router();
|
||||
authRoutes.use(getAccount)
|
||||
export let authRoutes = new Hono<{
|
||||
Variables: {
|
||||
account: Accounts.Account
|
||||
}
|
||||
}>()
|
||||
|
||||
let config = require(`${process.cwd()}/config.json`)
|
||||
authRoutes.all("*", getAccount)
|
||||
|
||||
module.exports = function(files: Files) {
|
||||
|
||||
authRoutes.post("/login", parser, (req,res) => {
|
||||
if (typeof req.body.username != "string" || typeof req.body.password != "string") {
|
||||
ServeError(res,400,"please provide a username or password")
|
||||
return
|
||||
module.exports = function (files: Files) {
|
||||
authRoutes.post("/login", async (ctx) => {
|
||||
console.log(ctx)
|
||||
const body = await ctx.req.json()
|
||||
if (
|
||||
typeof body.username != "string" ||
|
||||
typeof body.password != "string"
|
||||
) {
|
||||
return ServeError(ctx, 400, "please provide a username or password")
|
||||
}
|
||||
|
||||
if (auth.validate(req.cookies.auth)) return
|
||||
if (auth.validate(getCookie(ctx, "auth")!))
|
||||
return ctx.text("You are already authed")
|
||||
|
||||
/*
|
||||
check if account exists
|
||||
*/
|
||||
|
||||
let acc = Accounts.getFromUsername(req.body.username)
|
||||
let acc = Accounts.getFromUsername(body.username)
|
||||
|
||||
if (!acc) {
|
||||
ServeError(res,401,"username or password incorrect")
|
||||
return
|
||||
return ServeError(ctx, 401, "username or password incorrect")
|
||||
}
|
||||
|
||||
if (!Accounts.password.check(acc.id,req.body.password)) {
|
||||
ServeError(res,401,"username or password incorrect")
|
||||
return
|
||||
if (!Accounts.password.check(acc.id, body.password)) {
|
||||
return ServeError(ctx, 401, "username or password incorrect")
|
||||
}
|
||||
|
||||
/*
|
||||
assign token
|
||||
*/
|
||||
|
||||
res.cookie("auth",auth.create(acc.id,(3*24*60*60*1000)))
|
||||
res.status(200)
|
||||
res.end()
|
||||
setCookie(ctx, "auth", auth.create(acc.id, 3 * 24 * 60 * 60 * 1000))
|
||||
return ctx.text("")
|
||||
})
|
||||
|
||||
authRoutes.post("/create", parser, (req,res) => {
|
||||
authRoutes.post("/create", async (ctx) => {
|
||||
if (!config.accounts.registrationEnabled) {
|
||||
ServeError(res,403,"account registration disabled")
|
||||
return
|
||||
return ServeError(ctx, 403, "account registration disabled")
|
||||
}
|
||||
|
||||
if (auth.validate(req.cookies.auth)) return
|
||||
|
||||
if (typeof req.body.username != "string" || typeof req.body.password != "string") {
|
||||
ServeError(res,400,"please provide a username or password")
|
||||
return
|
||||
if (auth.validate(getCookie(ctx, "auth")!)) return
|
||||
const body = await ctx.req.json()
|
||||
if (
|
||||
typeof body.username != "string" ||
|
||||
typeof body.password != "string"
|
||||
) {
|
||||
return ServeError(ctx, 400, "please provide a username or password")
|
||||
}
|
||||
|
||||
/*
|
||||
check if account exists
|
||||
*/
|
||||
|
||||
let acc = Accounts.getFromUsername(req.body.username)
|
||||
let acc = Accounts.getFromUsername(body.username)
|
||||
|
||||
if (acc) {
|
||||
ServeError(res,400,"account with this username already exists")
|
||||
ServeError(ctx, 400, "account with this username already exists")
|
||||
return
|
||||
}
|
||||
|
||||
if (req.body.username.length < 3 || req.body.username.length > 20) {
|
||||
ServeError(res,400,"username must be over or equal to 3 characters or under or equal to 20 characters in length")
|
||||
if (body.username.length < 3 || body.username.length > 20) {
|
||||
return ServeError(
|
||||
ctx,
|
||||
400,
|
||||
"username must be over or equal to 3 characters or under or equal to 20 characters in length"
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
(body.username.match(/[A-Za-z0-9_\-\.]+/) || [])[0] != body.username
|
||||
) {
|
||||
return ServeError(ctx, 400, "username contains invalid characters")
|
||||
}
|
||||
|
||||
if (body.password.length < 8) {
|
||||
ServeError(ctx, 400, "password must be 8 characters or longer")
|
||||
return
|
||||
}
|
||||
|
||||
if ((req.body.username.match(/[A-Za-z0-9_\-\.]+/) || [])[0] != req.body.username) {
|
||||
ServeError(res,400,"username contains invalid characters")
|
||||
return
|
||||
}
|
||||
|
||||
if (req.body.password.length < 8) {
|
||||
ServeError(res,400,"password must be 8 characters or longer")
|
||||
return
|
||||
}
|
||||
|
||||
Accounts.create(req.body.username,req.body.password)
|
||||
return Accounts.create(body.username, body.password)
|
||||
.then((newAcc) => {
|
||||
/*
|
||||
assign token
|
||||
*/
|
||||
|
||||
res.cookie("auth",auth.create(newAcc,(3*24*60*60*1000)))
|
||||
res.status(200)
|
||||
res.end()
|
||||
})
|
||||
.catch(() => {
|
||||
ServeError(res,500,"internal server error")
|
||||
setCookie(
|
||||
ctx,
|
||||
"auth",
|
||||
auth.create(newAcc, 3 * 24 * 60 * 60 * 1000)
|
||||
)
|
||||
return ctx.text("")
|
||||
})
|
||||
.catch(() => ServeError(ctx, 500, "internal server error"))
|
||||
})
|
||||
|
||||
authRoutes.post("/logout", (req,res) => {
|
||||
if (!auth.validate(req.cookies.auth)) {
|
||||
ServeError(res, 401, "not logged in")
|
||||
return
|
||||
authRoutes.post("/logout", async (ctx) => {
|
||||
if (!auth.validate(getCookie(ctx, "auth")!)) {
|
||||
return ServeError(ctx, 401, "not logged in")
|
||||
}
|
||||
|
||||
auth.invalidate(req.cookies.auth)
|
||||
res.send("logged out")
|
||||
auth.invalidate(getCookie(ctx, "auth")!)
|
||||
return ctx.text("logged out")
|
||||
})
|
||||
|
||||
authRoutes.post("/dfv", requiresAccount, requiresPermissions("manage"), parser, (req,res) => {
|
||||
let acc = res.locals.acc as Accounts.Account
|
||||
authRoutes.post(
|
||||
"/dfv",
|
||||
requiresAccount,
|
||||
requiresPermissions("manage"),
|
||||
// Used body-parser
|
||||
async (ctx) => {
|
||||
const body = await ctx.req.json()
|
||||
let acc = ctx.get("account") as Accounts.Account
|
||||
|
||||
if (['public','private','anonymous'].includes(req.body.defaultFileVisibility)) {
|
||||
acc.defaultFileVisibility = req.body.defaultFileVisibility
|
||||
Accounts.save()
|
||||
res.send(`dfv has been set to ${acc.defaultFileVisibility}`)
|
||||
} else {
|
||||
res.status(400)
|
||||
res.send("invalid dfv")
|
||||
if (
|
||||
["public", "private", "anonymous"].includes(
|
||||
body.defaultFileVisibility
|
||||
)
|
||||
) {
|
||||
acc.defaultFileVisibility = body.defaultFileVisibility
|
||||
Accounts.save()
|
||||
return ctx.text(
|
||||
`dfv has been set to ${acc.defaultFileVisibility}`
|
||||
)
|
||||
} else {
|
||||
return ctx.text("invalid dfv", 400)
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
authRoutes.post("/customcss", requiresAccount, requiresPermissions("customize"), parser, (req,res) => {
|
||||
let acc = res.locals.acc as Accounts.Account
|
||||
|
||||
if (typeof req.body.fileId != "string") req.body.fileId = undefined;
|
||||
|
||||
if (
|
||||
authRoutes.post(
|
||||
"/customcss",
|
||||
requiresAccount,
|
||||
requiresPermissions("customize"),
|
||||
// Used body-parser
|
||||
async (ctx) => {
|
||||
const body = await ctx.req.json()
|
||||
let acc = ctx.get("account") as Accounts.Account
|
||||
|
||||
!req.body.fileId
|
||||
|| (req.body.fileId.match(id_check_regex) == req.body.fileId
|
||||
&& req.body.fileId.length <= config.maxUploadIdLength)
|
||||
|
||||
) {
|
||||
acc.customCSS = req.body.fileId || undefined
|
||||
if (!req.body.fileId) delete acc.customCSS
|
||||
Accounts.save()
|
||||
res.send(`custom css saved`)
|
||||
} else {
|
||||
res.status(400)
|
||||
res.send("invalid fileid")
|
||||
if (typeof body.fileId != "string") body.fileId = undefined
|
||||
|
||||
if (
|
||||
!body.fileId ||
|
||||
(body.fileId.match(id_check_regex) == body.fileId &&
|
||||
body.fileId.length <= config.maxUploadIdLength)
|
||||
) {
|
||||
acc.customCSS = body.fileId || undefined
|
||||
if (!body.fileId) delete acc.customCSS
|
||||
Accounts.save()
|
||||
return ctx.text(`custom css saved`)
|
||||
} else {
|
||||
return ctx.text("invalid fileid", 400)
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
authRoutes.post("/embedcolor", requiresAccount, requiresPermissions("customize"), parser, (req,res) => {
|
||||
let acc = res.locals.acc as Accounts.Account
|
||||
|
||||
if (typeof req.body.color != "string") req.body.color = undefined;
|
||||
|
||||
if (
|
||||
authRoutes.post(
|
||||
"/embedcolor",
|
||||
requiresAccount,
|
||||
requiresPermissions("customize"),
|
||||
// Used body-parser
|
||||
async (ctx) => {
|
||||
let acc = ctx.get("account") as Accounts.Account
|
||||
const body = await ctx.req.json()
|
||||
if (typeof body.color != "string") body.color = undefined
|
||||
|
||||
if (
|
||||
!body.color ||
|
||||
(body.color.toLowerCase().match(/[a-f0-9]+/) ==
|
||||
body.color.toLowerCase() &&
|
||||
body.color.length == 6)
|
||||
) {
|
||||
if (!acc.embed) acc.embed = {}
|
||||
acc.embed.color = body.color || undefined
|
||||
if (!body.color) delete acc.embed.color
|
||||
Accounts.save()
|
||||
return ctx.text(`custom embed color saved`)
|
||||
} else {
|
||||
return ctx.text("invalid hex code", 400)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
authRoutes.post(
|
||||
"/embedsize",
|
||||
requiresAccount,
|
||||
requiresPermissions("customize"),
|
||||
// Used body-parser
|
||||
async (ctx) => {
|
||||
let acc = ctx.get("account") as Accounts.Account
|
||||
const body = await ctx.req.json()
|
||||
if (typeof body.largeImage != "boolean") body.color = false
|
||||
|
||||
!req.body.color
|
||||
|| (req.body.color.toLowerCase().match(/[a-f0-9]+/) == req.body.color.toLowerCase())
|
||||
&& req.body.color.length == 6
|
||||
|
||||
) {
|
||||
if (!acc.embed) acc.embed = {}
|
||||
acc.embed.color = req.body.color || undefined
|
||||
if (!req.body.color) delete acc.embed.color
|
||||
acc.embed.largeImage = body.largeImage
|
||||
if (!body.largeImage) delete acc.embed.largeImage
|
||||
Accounts.save()
|
||||
res.send(`custom embed color saved`)
|
||||
} else {
|
||||
res.status(400)
|
||||
res.send("invalid hex code")
|
||||
return ctx.text(`custom embed image size saved`)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
authRoutes.post("/embedsize", requiresAccount, requiresPermissions("customize"), parser, (req,res) => {
|
||||
let acc = res.locals.acc as Accounts.Account
|
||||
|
||||
if (typeof req.body.largeImage != "boolean") req.body.color = false;
|
||||
authRoutes.post(
|
||||
"/delete_account",
|
||||
requiresAccount,
|
||||
noAPIAccess,
|
||||
// Used body-parser
|
||||
async (ctx) => {
|
||||
let acc = ctx.get("account") as Accounts.Account
|
||||
const body = await ctx.req.json()
|
||||
let accId = acc.id
|
||||
|
||||
if (!acc.embed) acc.embed = {}
|
||||
acc.embed.largeImage = req.body.largeImage
|
||||
if (!req.body.largeImage) delete acc.embed.largeImage
|
||||
Accounts.save()
|
||||
res.send(`custom embed image size saved`)
|
||||
})
|
||||
auth.AuthTokens.filter((e) => e.account == accId).forEach((v) => {
|
||||
auth.invalidate(v.token)
|
||||
})
|
||||
|
||||
authRoutes.post("/delete_account", requiresAccount, noAPIAccess, parser, async (req,res) => {
|
||||
let acc = res.locals.acc as Accounts.Account
|
||||
|
||||
let accId = acc.id
|
||||
let cpl = () =>
|
||||
Accounts.deleteAccount(accId).then((_) =>
|
||||
ctx.text("account deleted")
|
||||
)
|
||||
|
||||
auth.AuthTokens.filter(e => e.account == accId).forEach((v) => {
|
||||
auth.invalidate(v.token)
|
||||
})
|
||||
if (body.deleteFiles) {
|
||||
let f = acc.files.map((e) => e) // make shallow copy so that iterating over it doesnt Die
|
||||
for (let v of f) {
|
||||
files.unlink(v, true).catch((err) => console.error(err))
|
||||
}
|
||||
|
||||
let cpl = () => Accounts.deleteAccount(accId).then(_ => res.send("account deleted"))
|
||||
|
||||
if (req.body.deleteFiles) {
|
||||
let f = acc.files.map(e=>e) // make shallow copy so that iterating over it doesnt Die
|
||||
for (let v of f) {
|
||||
files.unlink(v,true).catch(err => console.error(err))
|
||||
return writeFile(
|
||||
process.cwd() + "/.data/files.json",
|
||||
JSON.stringify(files.files)
|
||||
).then(cpl)
|
||||
} else cpl()
|
||||
}
|
||||
)
|
||||
|
||||
authRoutes.post(
|
||||
"/change_username",
|
||||
requiresAccount,
|
||||
noAPIAccess,
|
||||
// Used body-parser
|
||||
async (ctx) => {
|
||||
let acc = ctx.get("account") as Accounts.Account
|
||||
const body = await ctx.req.json()
|
||||
if (
|
||||
typeof body.username != "string" ||
|
||||
body.username.length < 3 ||
|
||||
body.username.length > 20
|
||||
) {
|
||||
return ServeError(
|
||||
ctx,
|
||||
400,
|
||||
"username must be between 3 and 20 characters in length"
|
||||
)
|
||||
}
|
||||
|
||||
writeFile(process.cwd()+"/.data/files.json",JSON.stringify(files.files), (err) => {
|
||||
if (err) console.log(err)
|
||||
cpl()
|
||||
})
|
||||
} else cpl()
|
||||
})
|
||||
let _acc = Accounts.getFromUsername(body.username)
|
||||
|
||||
authRoutes.post("/change_username", requiresAccount, noAPIAccess, parser, (req,res) => {
|
||||
let acc = res.locals.acc as Accounts.Account
|
||||
if (_acc) {
|
||||
return ServeError(
|
||||
ctx,
|
||||
400,
|
||||
"account with this username already exists"
|
||||
)
|
||||
}
|
||||
|
||||
if (typeof req.body.username != "string" || req.body.username.length < 3 || req.body.username.length > 20) {
|
||||
ServeError(res,400,"username must be between 3 and 20 characters in length")
|
||||
return
|
||||
if (
|
||||
(body.username.match(/[A-Za-z0-9_\-\.]+/) || [])[0] !=
|
||||
body.username
|
||||
) {
|
||||
return ServeError(
|
||||
ctx,
|
||||
400,
|
||||
"username contains invalid characters"
|
||||
)
|
||||
}
|
||||
|
||||
acc.username = body.username
|
||||
Accounts.save()
|
||||
|
||||
if (acc.email) {
|
||||
return sendMail(
|
||||
acc.email,
|
||||
`Your login details have been updated`,
|
||||
`<b>Hello there!</b> Your username has been updated to <span username>${body.username}</span>. Please update your devices accordingly. Thank you for using monofile.`
|
||||
)
|
||||
.then(() => ctx.text("OK"))
|
||||
.catch((err) => {})
|
||||
}
|
||||
|
||||
return ctx.text("username changed")
|
||||
}
|
||||
|
||||
let _acc = Accounts.getFromUsername(req.body.username)
|
||||
|
||||
if (_acc) {
|
||||
ServeError(res,400,"account with this username already exists")
|
||||
return
|
||||
}
|
||||
|
||||
if ((req.body.username.match(/[A-Za-z0-9_\-\.]+/) || [])[0] != req.body.username) {
|
||||
ServeError(res,400,"username contains invalid characters")
|
||||
return
|
||||
}
|
||||
|
||||
acc.username = req.body.username
|
||||
Accounts.save()
|
||||
|
||||
if (acc.email) {
|
||||
sendMail(acc.email, `Your login details have been updated`, `<b>Hello there!</b> Your username has been updated to <span username>${req.body.username}</span>. Please update your devices accordingly. Thank you for using monofile.`).then(() => {
|
||||
res.send("OK")
|
||||
}).catch((err) => {})
|
||||
}
|
||||
|
||||
res.send("username changed")
|
||||
})
|
||||
)
|
||||
|
||||
// shit way to do this but...
|
||||
|
||||
let verificationCodes = new Map<string, {code: string, email: string, expiry: NodeJS.Timeout}>()
|
||||
let verificationCodes = new Map<
|
||||
string,
|
||||
{ code: string; email: string; expiry: NodeJS.Timeout }
|
||||
>()
|
||||
|
||||
authRoutes.post("/request_email_change", requiresAccount, noAPIAccess, accountRatelimit({ requests: 4, per: 60*60*1000 }), parser, (req,res) => {
|
||||
let acc = res.locals.acc as Accounts.Account
|
||||
|
||||
|
||||
if (typeof req.body.email != "string" || !req.body.email) {
|
||||
ServeError(res,400, "supply an email")
|
||||
return
|
||||
}
|
||||
authRoutes.post(
|
||||
"/request_email_change",
|
||||
requiresAccount,
|
||||
noAPIAccess,
|
||||
accountRatelimit({ requests: 4, per: 60 * 60 * 1000 }),
|
||||
// Used body-parser
|
||||
async (ctx) => {
|
||||
let acc = ctx.get("account") as Accounts.Account
|
||||
const body = await ctx.req.json()
|
||||
if (typeof body.email != "string" || !body.email) {
|
||||
ServeError(ctx, 400, "supply an email")
|
||||
return
|
||||
}
|
||||
|
||||
let vcode = verificationCodes.get(acc.id)
|
||||
let vcode = verificationCodes.get(acc.id)
|
||||
|
||||
// delete previous if any
|
||||
let e = vcode?.expiry
|
||||
if (e) clearTimeout(e)
|
||||
verificationCodes.delete(acc?.id||"")
|
||||
|
||||
let code = generateFileId(12).toUpperCase()
|
||||
|
||||
// set
|
||||
|
||||
verificationCodes.set(acc.id, {
|
||||
code,
|
||||
email: req.body.email,
|
||||
expiry: setTimeout( () => verificationCodes.delete(acc?.id||""), 15*60*1000)
|
||||
})
|
||||
|
||||
// this is a mess but it's fine
|
||||
|
||||
sendMail(req.body.email, `Hey there, ${acc.username} - let's connect your email`, `<b>Hello there!</b> You are recieving this message because you decided to link your email, <span code>${req.body.email.split("@")[0]}<span style="opacity:0.5">@${req.body.email.split("@")[1]}</span></span>, to your account, <span username>${acc.username}</span>. If you would like to continue, please <a href="https://${req.header("Host")}/auth/confirm_email/${code}"><span code>click here</span></a>, or go to https://${req.header("Host")}/auth/confirm_email/${code}.`).then(() => {
|
||||
res.send("OK")
|
||||
}).catch((err) => {
|
||||
let e = verificationCodes.get(acc?.id||"")?.expiry
|
||||
// delete previous if any
|
||||
let e = vcode?.expiry
|
||||
if (e) clearTimeout(e)
|
||||
verificationCodes.delete(acc?.id||"")
|
||||
res.locals.undoCount();
|
||||
ServeError(res, 500, err?.toString())
|
||||
})
|
||||
})
|
||||
verificationCodes.delete(acc?.id || "")
|
||||
|
||||
authRoutes.get("/confirm_email/:code", requiresAccount, noAPIAccess, (req,res) => {
|
||||
let acc = res.locals.acc as Accounts.Account
|
||||
|
||||
let code = generateFileId(12).toUpperCase()
|
||||
|
||||
let vcode = verificationCodes.get(acc.id)
|
||||
// set
|
||||
|
||||
if (!vcode) { ServeError(res, 400, "nothing to confirm"); return }
|
||||
verificationCodes.set(acc.id, {
|
||||
code,
|
||||
email: body.email,
|
||||
expiry: setTimeout(
|
||||
() => verificationCodes.delete(acc?.id || ""),
|
||||
15 * 60 * 1000
|
||||
),
|
||||
})
|
||||
|
||||
if (typeof req.params.code == "string" && req.params.code.toUpperCase() == vcode.code) {
|
||||
acc.email = vcode.email
|
||||
Accounts.save();
|
||||
// this is a mess but it's fine
|
||||
|
||||
let e = verificationCodes.get(acc?.id||"")?.expiry
|
||||
if (e) clearTimeout(e)
|
||||
verificationCodes.delete(acc?.id||"")
|
||||
|
||||
res.redirect("/")
|
||||
} else {
|
||||
ServeError(res, 400, "invalid code")
|
||||
sendMail(
|
||||
body.email,
|
||||
`Hey there, ${acc.username} - let's connect your email`,
|
||||
`<b>Hello there!</b> You are recieving this message because you decided to link your email, <span code>${
|
||||
body.email.split("@")[0]
|
||||
}<span style="opacity:0.5">@${
|
||||
body.email.split("@")[1]
|
||||
}</span></span>, to your account, <span username>${
|
||||
acc.username
|
||||
}</span>. If you would like to continue, please <a href="https://${ctx.req.header(
|
||||
"Host"
|
||||
)}/auth/confirm_email/${code}"><span code>click here</span></a>, or go to https://${ctx.req.header(
|
||||
"Host"
|
||||
)}/auth/confirm_email/${code}.`
|
||||
)
|
||||
.then(() => ctx.text("OK"))
|
||||
.catch((err) => {
|
||||
let e = verificationCodes.get(acc?.id || "")?.expiry
|
||||
if (e) clearTimeout(e)
|
||||
verificationCodes.delete(acc?.id || "")
|
||||
;(ctx.get("undoCount" as never) as () => {})()
|
||||
return ServeError(ctx, 500, err?.toString())
|
||||
})
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
authRoutes.post("/remove_email", requiresAccount, noAPIAccess, (req,res) => {
|
||||
let acc = res.locals.acc as Accounts.Account
|
||||
|
||||
if (acc.email) {
|
||||
delete acc.email;
|
||||
Accounts.save()
|
||||
res.send("email detached")
|
||||
}
|
||||
else ServeError(res, 400, "email not attached")
|
||||
})
|
||||
authRoutes.get(
|
||||
"/confirm_email/:code",
|
||||
requiresAccount,
|
||||
noAPIAccess,
|
||||
async (ctx) => {
|
||||
let acc = ctx.get("account") as Accounts.Account
|
||||
|
||||
let pwReset = new Map<string, {code: string, expiry: NodeJS.Timeout, requestedAt:number}>()
|
||||
let vcode = verificationCodes.get(acc.id)
|
||||
|
||||
if (!vcode) {
|
||||
ServeError(ctx, 400, "nothing to confirm")
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
typeof ctx.req.param("code") == "string" &&
|
||||
ctx.req.param("code").toUpperCase() == vcode.code
|
||||
) {
|
||||
acc.email = vcode.email
|
||||
Accounts.save()
|
||||
|
||||
let e = verificationCodes.get(acc?.id || "")?.expiry
|
||||
if (e) clearTimeout(e)
|
||||
verificationCodes.delete(acc?.id || "")
|
||||
|
||||
return ctx.redirect("/")
|
||||
} else {
|
||||
return ServeError(ctx, 400, "invalid code")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
authRoutes.post(
|
||||
"/remove_email",
|
||||
requiresAccount,
|
||||
noAPIAccess,
|
||||
async (ctx) => {
|
||||
let acc = ctx.get("account") as Accounts.Account
|
||||
|
||||
if (acc.email) {
|
||||
delete acc.email
|
||||
Accounts.save()
|
||||
return ctx.text("email detached")
|
||||
} else return ServeError(ctx, 400, "email not attached")
|
||||
}
|
||||
)
|
||||
|
||||
let pwReset = new Map<
|
||||
string,
|
||||
{ code: string; expiry: NodeJS.Timeout; requestedAt: number }
|
||||
>()
|
||||
let prcIdx = new Map<string, string>()
|
||||
|
||||
authRoutes.post("/request_emergency_login", parser, (req,res) => {
|
||||
if (auth.validate(req.cookies.auth || "")) return
|
||||
|
||||
if (typeof req.body.account != "string" || !req.body.account) {
|
||||
ServeError(res,400, "supply a username")
|
||||
authRoutes.post("/request_emergency_login", async (ctx) => {
|
||||
if (auth.validate(getCookie(ctx, "auth") || "")) return
|
||||
const body = await ctx.req.json()
|
||||
if (typeof body.account != "string" || !body.account) {
|
||||
ServeError(ctx, 400, "supply a username")
|
||||
return
|
||||
}
|
||||
|
||||
let acc = Accounts.getFromUsername(req.body.account)
|
||||
let acc = Accounts.getFromUsername(body.account)
|
||||
if (!acc || !acc.email) {
|
||||
ServeError(res, 400, "this account either does not exist or does not have an email attached; please contact the server's admin for a reset if you would still like to access it")
|
||||
return
|
||||
return ServeError(
|
||||
ctx,
|
||||
400,
|
||||
"this account either does not exist or does not have an email attached; please contact the server's admin for a reset if you would still like to access it"
|
||||
)
|
||||
}
|
||||
|
||||
let pResetCode = pwReset.get(acc.id)
|
||||
let pResetCode = pwReset.get(acc.id)
|
||||
|
||||
if (pResetCode && pResetCode.requestedAt+(15*60*1000) > Date.now()) {
|
||||
ServeError(res, 429, `Please wait a few moments to request another emergency login.`)
|
||||
return
|
||||
if (
|
||||
pResetCode &&
|
||||
pResetCode.requestedAt + 15 * 60 * 1000 > Date.now()
|
||||
) {
|
||||
return ServeError(
|
||||
ctx,
|
||||
429,
|
||||
`Please wait a few moments to request another emergency login.`
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// delete previous if any
|
||||
let e = pResetCode?.expiry
|
||||
if (e) clearTimeout(e)
|
||||
pwReset.delete(acc?.id||"")
|
||||
prcIdx.delete(pResetCode?.code||"")
|
||||
pwReset.delete(acc?.id || "")
|
||||
prcIdx.delete(pResetCode?.code || "")
|
||||
|
||||
let code = generateFileId(12).toUpperCase()
|
||||
|
||||
|
@ -358,107 +485,146 @@ module.exports = function(files: Files) {
|
|||
|
||||
pwReset.set(acc.id, {
|
||||
code,
|
||||
expiry: setTimeout( () => { pwReset.delete(acc?.id||""); prcIdx.delete(pResetCode?.code||"") }, 15*60*1000),
|
||||
requestedAt: Date.now()
|
||||
expiry: setTimeout(() => {
|
||||
pwReset.delete(acc?.id || "")
|
||||
prcIdx.delete(pResetCode?.code || "")
|
||||
}, 15 * 60 * 1000),
|
||||
requestedAt: Date.now(),
|
||||
})
|
||||
|
||||
prcIdx.set(code, acc.id)
|
||||
|
||||
// this is a mess but it's fine
|
||||
|
||||
sendMail(acc.email, `Emergency login requested for ${acc.username}`, `<b>Hello there!</b> You are recieving this message because you forgot your password to your monofile account, <span username>${acc.username}</span>. To log in, please <a href="https://${req.header("Host")}/auth/emergency_login/${code}"><span code>click here</span></a>, or go to https://${req.header("Host")}/auth/emergency_login/${code}. If it doesn't appear that you are logged in after visiting this link, please try refreshing. Once you have successfully logged in, you may reset your password.`).then(() => {
|
||||
res.send("OK")
|
||||
}).catch((err) => {
|
||||
let e = pwReset.get(acc?.id||"")?.expiry
|
||||
if (e) clearTimeout(e)
|
||||
pwReset.delete(acc?.id||"")
|
||||
prcIdx.delete(code||"")
|
||||
ServeError(res, 500, err?.toString())
|
||||
})
|
||||
return sendMail(
|
||||
acc.email,
|
||||
`Emergency login requested for ${acc.username}`,
|
||||
`<b>Hello there!</b> You are recieving this message because you forgot your password to your monofile account, <span username>${
|
||||
acc.username
|
||||
}</span>. To log in, please <a href="https://${ctx.req.header(
|
||||
"Host"
|
||||
)}/auth/emergency_login/${code}"><span code>click here</span></a>, or go to https://${ctx.req.header(
|
||||
"Host"
|
||||
)}/auth/emergency_login/${code}. If it doesn't appear that you are logged in after visiting this link, please try refreshing. Once you have successfully logged in, you may reset your password.`
|
||||
)
|
||||
.then(() => ctx.text("OK"))
|
||||
.catch((err) => {
|
||||
let e = pwReset.get(acc?.id || "")?.expiry
|
||||
if (e) clearTimeout(e)
|
||||
pwReset.delete(acc?.id || "")
|
||||
prcIdx.delete(code || "")
|
||||
return ServeError(ctx, 500, err?.toString())
|
||||
})
|
||||
})
|
||||
|
||||
authRoutes.get("/emergency_login/:code", (req,res) => {
|
||||
if (auth.validate(req.cookies.auth || "")) {
|
||||
ServeError(res, 403, "already logged in")
|
||||
return
|
||||
authRoutes.get("/emergency_login/:code", async (ctx) => {
|
||||
if (auth.validate(getCookie(ctx, "auth") || "")) {
|
||||
return ServeError(ctx, 403, "already logged in")
|
||||
}
|
||||
|
||||
let vcode = prcIdx.get(req.params.code)
|
||||
let vcode = prcIdx.get(ctx.req.param("code"))
|
||||
|
||||
if (!vcode) { ServeError(res, 400, "invalid emergency login code"); return }
|
||||
|
||||
if (typeof req.params.code == "string" && vcode) {
|
||||
res.cookie("auth",auth.create(vcode,(3*24*60*60*1000)))
|
||||
res.redirect("/")
|
||||
if (!vcode) {
|
||||
return ServeError(ctx, 400, "invalid emergency login code")
|
||||
}
|
||||
|
||||
if (typeof ctx.req.param("code") == "string" && vcode) {
|
||||
setCookie(ctx, "auth", auth.create(vcode, 3 * 24 * 60 * 60 * 1000))
|
||||
let e = pwReset.get(vcode)?.expiry
|
||||
if (e) clearTimeout(e)
|
||||
pwReset.delete(vcode)
|
||||
prcIdx.delete(req.params.code)
|
||||
prcIdx.delete(ctx.req.param("code"))
|
||||
return ctx.redirect("/")
|
||||
} else {
|
||||
ServeError(res, 400, "invalid code")
|
||||
ServeError(ctx, 400, "invalid code")
|
||||
}
|
||||
})
|
||||
|
||||
authRoutes.post("/change_password", requiresAccount, noAPIAccess, parser, (req,res) => {
|
||||
let acc = res.locals.acc as Accounts.Account
|
||||
|
||||
if (typeof req.body.password != "string" || req.body.password.length < 8) {
|
||||
ServeError(res,400,"password must be 8 characters or longer")
|
||||
return
|
||||
authRoutes.post(
|
||||
"/change_password",
|
||||
requiresAccount,
|
||||
noAPIAccess,
|
||||
// Used body-parser
|
||||
async (ctx) => {
|
||||
let acc = ctx.get("account") as Accounts.Account
|
||||
const body = await ctx.req.json()
|
||||
if (typeof body.password != "string" || body.password.length < 8) {
|
||||
ServeError(ctx, 400, "password must be 8 characters or longer")
|
||||
return
|
||||
}
|
||||
|
||||
let accId = acc.id
|
||||
|
||||
Accounts.password.set(accId, body.password)
|
||||
|
||||
auth.AuthTokens.filter((e) => e.account == accId).forEach((v) => {
|
||||
auth.invalidate(v.token)
|
||||
})
|
||||
|
||||
if (acc.email) {
|
||||
return sendMail(
|
||||
acc.email,
|
||||
`Your login details have been updated`,
|
||||
`<b>Hello there!</b> This email is to notify you of a password change that you have initiated. You have been logged out of your devices. Thank you for using monofile.`
|
||||
)
|
||||
.then(() => ctx.text("OK"))
|
||||
.catch((err) => {})
|
||||
}
|
||||
|
||||
return ctx.text("password changed - logged out all sessions")
|
||||
}
|
||||
)
|
||||
|
||||
let accId = acc.id
|
||||
authRoutes.post(
|
||||
"/logout_sessions",
|
||||
requiresAccount,
|
||||
noAPIAccess,
|
||||
async (ctx) => {
|
||||
let acc = ctx.get("account") as Accounts.Account
|
||||
|
||||
Accounts.password.set(accId,req.body.password)
|
||||
let accId = acc.id
|
||||
|
||||
auth.AuthTokens.filter(e => e.account == accId).forEach((v) => {
|
||||
auth.invalidate(v.token)
|
||||
})
|
||||
auth.AuthTokens.filter((e) => e.account == accId).forEach((v) => {
|
||||
auth.invalidate(v.token)
|
||||
})
|
||||
|
||||
if (acc.email) {
|
||||
sendMail(acc.email, `Your login details have been updated`, `<b>Hello there!</b> This email is to notify you of a password change that you have initiated. You have been logged out of your devices. Thank you for using monofile.`).then(() => {
|
||||
res.send("OK")
|
||||
}).catch((err) => {})
|
||||
return ctx.text("logged out all sessions")
|
||||
}
|
||||
)
|
||||
|
||||
res.send("password changed - logged out all sessions")
|
||||
authRoutes.get(
|
||||
"/me",
|
||||
requiresAccount,
|
||||
requiresPermissions("user"),
|
||||
async (ctx) => {
|
||||
let acc = ctx.get("account") as Accounts.Account
|
||||
let sessionToken = auth.tokenFor(ctx)!
|
||||
let accId = acc.id
|
||||
return ctx.json({
|
||||
...acc,
|
||||
sessionCount: auth.AuthTokens.filter(
|
||||
(e) =>
|
||||
e.type != "App" &&
|
||||
e.account == accId &&
|
||||
(e.expire > Date.now() || !e.expire)
|
||||
).length,
|
||||
sessionExpires: auth.AuthTokens.find(
|
||||
(e) => e.token == sessionToken
|
||||
)?.expire,
|
||||
password: undefined,
|
||||
email:
|
||||
auth.getType(sessionToken) == "User" ||
|
||||
auth.getPermissions(sessionToken)?.includes("email")
|
||||
? acc.email
|
||||
: undefined,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
authRoutes.get("/customCSS", async (ctx) => {
|
||||
let acc = ctx.get("account")
|
||||
if (acc?.customCSS) return ctx.redirect(`/file/${acc.customCSS}`)
|
||||
else return ctx.text("")
|
||||
})
|
||||
|
||||
authRoutes.post("/logout_sessions", requiresAccount, noAPIAccess, (req,res) => {
|
||||
let acc = res.locals.acc as Accounts.Account
|
||||
|
||||
let accId = acc.id
|
||||
|
||||
auth.AuthTokens.filter(e => e.account == accId).forEach((v) => {
|
||||
auth.invalidate(v.token)
|
||||
})
|
||||
|
||||
res.send("logged out all sessions")
|
||||
})
|
||||
|
||||
authRoutes.get("/me", requiresAccount, requiresPermissions("user"), (req,res) => {
|
||||
let acc = res.locals.acc as Accounts.Account
|
||||
|
||||
let sessionToken = auth.tokenFor(req)
|
||||
let accId = acc.id
|
||||
res.send({
|
||||
...acc,
|
||||
sessionCount: auth.AuthTokens.filter(e => e.type != "App" && e.account == accId && (e.expire > Date.now() || !e.expire)).length,
|
||||
sessionExpires: auth.AuthTokens.find(e => e.token == sessionToken)?.expire,
|
||||
password: undefined,
|
||||
email:
|
||||
auth.getType(sessionToken) == "User" || auth.getPermissions(sessionToken)?.includes("email")
|
||||
? acc.email
|
||||
: undefined
|
||||
})
|
||||
})
|
||||
|
||||
authRoutes.get("/customCSS", (req,res) => {
|
||||
let acc = res.locals.acc
|
||||
if (acc?.customCSS) res.redirect(`/file/${acc.customCSS}`)
|
||||
else res.send("")
|
||||
})
|
||||
|
||||
return authRoutes
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,98 +1,129 @@
|
|||
import bodyParser from "body-parser";
|
||||
import { Router } from "express";
|
||||
import * as Accounts from "../../../lib/accounts";
|
||||
import * as auth from "../../../lib/auth";
|
||||
import bytes from "bytes"
|
||||
import {writeFile} from "fs";
|
||||
import { Hono } from "hono"
|
||||
import * as Accounts from "../../../lib/accounts"
|
||||
import { writeFile } from "fs/promises"
|
||||
import Files from "../../../lib/files"
|
||||
import {
|
||||
getAccount,
|
||||
requiresAccount,
|
||||
requiresPermissions,
|
||||
} from "../../../lib/middleware"
|
||||
|
||||
import ServeError from "../../../lib/errors";
|
||||
import Files from "../../../lib/files";
|
||||
import { getAccount, requiresAccount, requiresPermissions } from "../../../lib/middleware";
|
||||
|
||||
let parser = bodyParser.json({
|
||||
type: ["text/plain","application/json"]
|
||||
})
|
||||
|
||||
export let fileApiRoutes = Router();
|
||||
export let fileApiRoutes = new Hono<{
|
||||
Variables: {
|
||||
account: Accounts.Account
|
||||
}
|
||||
}>()
|
||||
|
||||
let config = require(`${process.cwd()}/config.json`)
|
||||
fileApiRoutes.use("*", getAccount) // :warning: /list somehow crashes Hono with an internal error!
|
||||
/*
|
||||
|
||||
/home/jack/Code/Web/monofile/node_modules/.pnpm/@hono+node-server@1.2.0/node_modules/@hono/node-server/dist/listener.js:55
|
||||
const contentType = res.headers.get("content-type") || "";
|
||||
^
|
||||
|
||||
module.exports = function(files: Files) {
|
||||
TypeError: Cannot read properties of undefined (reading 'get')
|
||||
at Server.<anonymous> (/home/jack/Code/Web/monofile/node_modules/.pnpm/@hono+node-server@1.2.0/node_modules/@hono/node-server/dist/listener.js:55:37)
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
|
||||
*/
|
||||
|
||||
fileApiRoutes.use(getAccount);
|
||||
module.exports = function (files: Files) {
|
||||
fileApiRoutes.get(
|
||||
"/list",
|
||||
requiresAccount,
|
||||
requiresPermissions("user"),
|
||||
async (ctx) => {
|
||||
let acc = ctx.get("account") as Accounts.Account
|
||||
|
||||
fileApiRoutes.get("/list", requiresAccount, requiresPermissions("user"), (req,res) => {
|
||||
if (!acc) return
|
||||
let accId = acc.id
|
||||
|
||||
let acc = res.locals.acc as Accounts.Account
|
||||
|
||||
if (!acc) return
|
||||
let accId = acc.id
|
||||
ctx.json(
|
||||
acc.files
|
||||
.map((e) => {
|
||||
let fp = files.getFilePointer(e)
|
||||
if (!fp) {
|
||||
Accounts.files.deindex(accId, e)
|
||||
return null
|
||||
}
|
||||
return {
|
||||
...fp,
|
||||
messageids: null,
|
||||
owner: null,
|
||||
id: e,
|
||||
}
|
||||
})
|
||||
.filter((e) => e)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
res.send(acc.files.map((e) => {
|
||||
let fp = files.getFilePointer(e)
|
||||
if (!fp) { Accounts.files.deindex(accId, e); return null }
|
||||
return {
|
||||
...fp,
|
||||
messageids: null,
|
||||
owner: null,
|
||||
id:e
|
||||
}
|
||||
}).filter(e=>e))
|
||||
|
||||
})
|
||||
|
||||
fileApiRoutes.post("/manage", parser, requiresPermissions("manage"), (req,res) => {
|
||||
|
||||
let acc = res.locals.acc as Accounts.Account
|
||||
|
||||
if (!acc) return
|
||||
if (!req.body.target || !(typeof req.body.target == "object") || req.body.target.length < 1) return
|
||||
|
||||
let modified = 0
|
||||
|
||||
req.body.target.forEach((e:string) => {
|
||||
if (!acc.files.includes(e)) return
|
||||
|
||||
let fp = files.getFilePointer(e)
|
||||
|
||||
if (fp.reserved) {
|
||||
fileApiRoutes.post(
|
||||
"/manage",
|
||||
requiresPermissions("manage"),
|
||||
async (ctx) => {
|
||||
let acc = ctx.get("account") as Accounts.Account
|
||||
const body = await ctx.req.json()
|
||||
if (!acc) return
|
||||
if (
|
||||
!body.target ||
|
||||
!(typeof body.target == "object") ||
|
||||
body.target.length < 1
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
switch( req.body.action ) {
|
||||
case "delete":
|
||||
files.unlink(e, true)
|
||||
modified++;
|
||||
break;
|
||||
let modified = 0
|
||||
|
||||
case "changeFileVisibility":
|
||||
if (!["public","anonymous","private"].includes(req.body.value)) return;
|
||||
files.files[e].visibility = req.body.value;
|
||||
modified++;
|
||||
break;
|
||||
body.target.forEach((e: string) => {
|
||||
if (!acc.files.includes(e)) return
|
||||
|
||||
case "setTag":
|
||||
if (!req.body.value) delete files.files[e].tag
|
||||
else {
|
||||
if (req.body.value.toString().length > 30) return
|
||||
files.files[e].tag = req.body.value.toString().toLowerCase()
|
||||
}
|
||||
modified++;
|
||||
break;
|
||||
}
|
||||
})
|
||||
let fp = files.getFilePointer(e)
|
||||
|
||||
Accounts.save().then(() => {
|
||||
writeFile(process.cwd()+"/.data/files.json",JSON.stringify(files.files), (err) => {
|
||||
if (err) console.log(err)
|
||||
res.contentType("text/plain")
|
||||
res.send(`modified ${modified} files`)
|
||||
if (fp.reserved) {
|
||||
return
|
||||
}
|
||||
|
||||
switch (body.action) {
|
||||
case "delete":
|
||||
files.unlink(e, true)
|
||||
modified++
|
||||
break
|
||||
|
||||
case "changeFileVisibility":
|
||||
if (
|
||||
!["public", "anonymous", "private"].includes(
|
||||
body.value
|
||||
)
|
||||
)
|
||||
return
|
||||
files.files[e].visibility = body.value
|
||||
modified++
|
||||
break
|
||||
|
||||
case "setTag":
|
||||
if (!body.value) delete files.files[e].tag
|
||||
else {
|
||||
if (body.value.toString().length > 30) return
|
||||
files.files[e].tag = body.value
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
}
|
||||
modified++
|
||||
break
|
||||
}
|
||||
})
|
||||
}).catch((err) => console.error(err))
|
||||
|
||||
|
||||
})
|
||||
return Accounts.save()
|
||||
.then(() => {
|
||||
writeFile(
|
||||
process.cwd() + "/.data/files.json",
|
||||
JSON.stringify(files.files)
|
||||
)
|
||||
})
|
||||
.then(() => ctx.text(`modified ${modified} files`))
|
||||
.catch((err) => console.error(err))
|
||||
}
|
||||
)
|
||||
|
||||
return fileApiRoutes
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -1,214 +1,223 @@
|
|||
// Modules
|
||||
|
||||
import { writeFile } from 'fs'
|
||||
import { Router } from "express";
|
||||
import bodyParser from "body-parser";
|
||||
|
||||
import { Hono } from "hono"
|
||||
import { getCookie, setCookie } from "hono/cookie"
|
||||
|
||||
// Libs
|
||||
|
||||
import Files, { id_check_regex } from "../../../lib/files";
|
||||
import * as Accounts from '../../../lib/accounts'
|
||||
import * as Authentication from '../../../lib/auth'
|
||||
import { assertAPI, getAccount, noAPIAccess, requiresAccount, requiresPermissions } from "../../../lib/middleware";
|
||||
import ServeError from "../../../lib/errors";
|
||||
import { sendMail } from '../../../lib/mail';
|
||||
import Files, { id_check_regex } from "../../../lib/files"
|
||||
import * as Accounts from "../../../lib/accounts"
|
||||
import * as Authentication from "../../../lib/auth"
|
||||
import {
|
||||
assertAPI,
|
||||
getAccount,
|
||||
noAPIAccess,
|
||||
requiresAccount,
|
||||
requiresPermissions,
|
||||
} from "../../../lib/middleware"
|
||||
import ServeError from "../../../lib/errors"
|
||||
import { sendMail } from "../../../lib/mail"
|
||||
|
||||
const Configuration = require(`${process.cwd()}/config.json`)
|
||||
|
||||
const parser = bodyParser.json({
|
||||
type: [ "type/plain", "application/json" ]
|
||||
})
|
||||
const router = new Hono<{
|
||||
Variables: {
|
||||
account: Accounts.Account
|
||||
}
|
||||
}>()
|
||||
|
||||
const router = Router()
|
||||
router.use(getAccount)
|
||||
|
||||
router.use(getAccount, parser)
|
||||
|
||||
module.exports = function(files: Files) {
|
||||
router.post(
|
||||
"/login",
|
||||
(req, res) => {
|
||||
if (typeof req.body.username != "string" || typeof req.body.password != "string") {
|
||||
ServeError(res, 400, "please provide a username or password")
|
||||
return
|
||||
}
|
||||
|
||||
if (Authentication.validate(req.cookies.auth)) {
|
||||
ServeError(res, 400, "you are already logged in")
|
||||
return
|
||||
}
|
||||
|
||||
const Account = Accounts.getFromUsername(req.body.username)
|
||||
|
||||
if (!Account || !Accounts.password.check(Account.id, req.body.password)) {
|
||||
ServeError(res, 400, "username or password incorrect")
|
||||
return
|
||||
}
|
||||
|
||||
res.cookie("auth",
|
||||
Authentication.create(
|
||||
Account.id, // account id
|
||||
(3 * 24 * 60 * 60 * 1000) // expiration time
|
||||
)
|
||||
)
|
||||
res.status(200)
|
||||
res.end()
|
||||
module.exports = function (files: Files) {
|
||||
router.post("/login", async (ctx, res) => {
|
||||
const body = await ctx.req.json()
|
||||
if (
|
||||
typeof body.username != "string" ||
|
||||
typeof body.password != "string"
|
||||
) {
|
||||
ServeError(ctx, 400, "please provide a username or password")
|
||||
return
|
||||
}
|
||||
)
|
||||
|
||||
router.post(
|
||||
"/create",
|
||||
(req, res) => {
|
||||
if (!Configuration.accounts.registrationEnabled) {
|
||||
ServeError(res , 403, "account registration disabled")
|
||||
return
|
||||
if (Authentication.validate(getCookie(ctx, "auth")!)) {
|
||||
ServeError(ctx, 400, "you are already logged in")
|
||||
return
|
||||
}
|
||||
|
||||
const Account = Accounts.getFromUsername(body.username)
|
||||
|
||||
if (!Account || !Accounts.password.check(Account.id, body.password)) {
|
||||
ServeError(ctx, 400, "username or password incorrect")
|
||||
return
|
||||
}
|
||||
setCookie(
|
||||
ctx,
|
||||
"auth",
|
||||
Authentication.create(
|
||||
Account.id, // account id
|
||||
3 * 24 * 60 * 60 * 1000 // expiration time
|
||||
),
|
||||
{
|
||||
// expires:
|
||||
}
|
||||
)
|
||||
ctx.status(200)
|
||||
})
|
||||
|
||||
if (Authentication.validate(req.cookies.auth)) {
|
||||
ServeError(res, 400, "you are already logged in")
|
||||
return
|
||||
}
|
||||
router.post("/create", async (ctx) => {
|
||||
const body = await ctx.req.json()
|
||||
if (!Configuration.accounts.registrationEnabled) {
|
||||
return ServeError(ctx, 403, "account registration disabled")
|
||||
}
|
||||
|
||||
if (Accounts.getFromUsername(req.body.username)) {
|
||||
ServeError(res, 400, "account with this username already exists")
|
||||
return
|
||||
}
|
||||
if (Authentication.validate(getCookie(ctx, "auth")!)) {
|
||||
return ServeError(ctx, 400, "you are already logged in")
|
||||
}
|
||||
|
||||
if (req.body.username.length < 3 || req.body.username.length > 20) {
|
||||
ServeError(res, 400, "username must be over or equal to 3 characters or under or equal to 20 characters in length")
|
||||
return
|
||||
}
|
||||
if (Accounts.getFromUsername(body.username)) {
|
||||
return ServeError(
|
||||
ctx,
|
||||
400,
|
||||
"account with this username already exists"
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
(
|
||||
req.body.username.match(/[A-Za-z0-9_\-\.]+/)
|
||||
||
|
||||
[]
|
||||
)[0] != req.body.username
|
||||
) {
|
||||
ServeError(res, 400, "username contains invalid characters")
|
||||
return
|
||||
}
|
||||
if (body.username.length < 3 || body.username.length > 20) {
|
||||
return ServeError(
|
||||
ctx,
|
||||
400,
|
||||
"username must be over or equal to 3 characters or under or equal to 20 characters in length"
|
||||
)
|
||||
}
|
||||
|
||||
if (req.body.password.length < 8) {
|
||||
ServeError(res, 400, "password must be 8 characters or longer")
|
||||
return
|
||||
}
|
||||
if (
|
||||
(body.username.match(/[A-Za-z0-9_\-\.]+/) || [])[0] != body.username
|
||||
) {
|
||||
return ServeError(ctx, 400, "username contains invalid characters")
|
||||
}
|
||||
|
||||
Accounts.create(
|
||||
req.body.username,
|
||||
req.body.password
|
||||
).then((Account) => {
|
||||
res.cookie("auth", Authentication.create(
|
||||
Account, // account id
|
||||
(3 * 24 * 60 * 60 * 1000) // expiration time
|
||||
))
|
||||
res.status(200)
|
||||
res.end()
|
||||
if (body.password.length < 8) {
|
||||
return ServeError(
|
||||
ctx,
|
||||
400,
|
||||
"password must be 8 characters or longer"
|
||||
)
|
||||
}
|
||||
|
||||
return Accounts.create(body.username, body.password)
|
||||
.then((Account) => {
|
||||
setCookie(
|
||||
ctx,
|
||||
"auth",
|
||||
Authentication.create(
|
||||
Account, // account id
|
||||
3 * 24 * 60 * 60 * 1000 // expiration time
|
||||
),
|
||||
{
|
||||
// expires:
|
||||
}
|
||||
)
|
||||
return ctx.status(200)
|
||||
})
|
||||
.catch(() => {
|
||||
ServeError(res, 500, "internal server error")
|
||||
return ServeError(ctx, 500, "internal server error")
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
router.post(
|
||||
"/logout",
|
||||
(req, res) => {
|
||||
if (!Authentication.validate(req.cookies.auth)) {
|
||||
ServeError(res, 401, "not logged in")
|
||||
return
|
||||
}
|
||||
|
||||
Authentication.invalidate(req.cookies.auth)
|
||||
res.send("logged out")
|
||||
router.post("/logout", (ctx) => {
|
||||
if (!Authentication.validate(getCookie(ctx, "auth")!)) {
|
||||
return ServeError(ctx, 401, "not logged in")
|
||||
}
|
||||
)
|
||||
|
||||
Authentication.invalidate(getCookie(ctx, "auth")!)
|
||||
return ctx.text("logged out")
|
||||
})
|
||||
|
||||
router.patch(
|
||||
"/dfv",
|
||||
requiresAccount, requiresPermissions("manage"),
|
||||
(req, res) => {
|
||||
const Account = res.locals.acc as Accounts.Account
|
||||
|
||||
if (['public', 'private', 'anonymous'].includes(req.body.defaultFileVisibility)) {
|
||||
Account.defaultFileVisibility = req.body.defaultFileVisibility
|
||||
|
||||
Accounts.save()
|
||||
|
||||
res.send(`dfv has been set to ${Account.defaultFileVisibility}`)
|
||||
} else {
|
||||
ServeError(res, 400, "invalid dfv")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
router.delete("/me",
|
||||
requiresAccount, noAPIAccess,
|
||||
parser,
|
||||
(req, res) => {
|
||||
const Account = res.locals.acc as Accounts.Account
|
||||
|
||||
const accountId = Account.id
|
||||
|
||||
Authentication.AuthTokens.filter(e => e.account == accountId).forEach((token) => {
|
||||
Authentication.invalidate(token.token)
|
||||
})
|
||||
|
||||
Accounts.deleteAccount(accountId).then(_ => res.send("account deleted"))
|
||||
}
|
||||
)
|
||||
|
||||
router.patch("/me/name",
|
||||
requiresAccount,
|
||||
noAPIAccess,
|
||||
parser,
|
||||
(req, res) => {
|
||||
const Account = res.locals.acc as Accounts.Account
|
||||
|
||||
const newUsername = req.body.username
|
||||
requiresPermissions("manage"),
|
||||
async (ctx) => {
|
||||
const body = await ctx.req.json()
|
||||
const Account = ctx.get("account")! as Accounts.Account
|
||||
|
||||
if (
|
||||
typeof newUsername != "string"
|
||||
||
|
||||
newUsername.length < 3
|
||||
||
|
||||
req.body.username.length > 20
|
||||
["public", "private", "anonymous"].includes(
|
||||
body.defaultFileVisibility
|
||||
)
|
||||
) {
|
||||
ServeError(res, 400, "username must be between 3 and 20 characters in length")
|
||||
return
|
||||
}
|
||||
Account.defaultFileVisibility = body.defaultFileVisibility
|
||||
|
||||
if (Accounts.getFromUsername(newUsername)) {
|
||||
ServeError(res, 400, "account with this username already exists")
|
||||
}
|
||||
Accounts.save()
|
||||
|
||||
if (
|
||||
(
|
||||
newUsername.match(/[A-Za-z0-9_\-\.]+/)
|
||||
||
|
||||
[]
|
||||
)[0] != req.body.username
|
||||
) {
|
||||
ServeError(res, 400, "username contains invalid characters")
|
||||
return
|
||||
}
|
||||
|
||||
Account.username = newUsername
|
||||
Accounts.save()
|
||||
|
||||
if (Account.email) {
|
||||
sendMail(
|
||||
Account.email,
|
||||
`Your login details have been updated`,
|
||||
`<b>Hello there!</b> Your username has been updated to <span username>${newUsername}</span>. Please update your devices accordingly. Thank you for using monofile.`
|
||||
).then(() => {
|
||||
res.send("OK")
|
||||
}).catch((err) => {})
|
||||
return ctx.text(
|
||||
`dfv has been set to ${Account.defaultFileVisibility}`
|
||||
)
|
||||
} else {
|
||||
return ServeError(ctx, 400, "invalid dfv")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
router.delete("/me", requiresAccount, noAPIAccess, async (ctx) => {
|
||||
const Account = ctx.get("account") as Accounts.Account
|
||||
const accountId = Account.id
|
||||
|
||||
Authentication.AuthTokens.filter((e) => e.account == accountId).forEach(
|
||||
(token) => {
|
||||
Authentication.invalidate(token.token)
|
||||
}
|
||||
)
|
||||
|
||||
await Accounts.deleteAccount(accountId)
|
||||
return ctx.text("account deleted")
|
||||
})
|
||||
|
||||
router.patch("/me/name", requiresAccount, noAPIAccess, async (ctx) => {
|
||||
const Account = ctx.get("account") as Accounts.Account
|
||||
const body = await ctx.req.json()
|
||||
const newUsername = body.username
|
||||
|
||||
if (
|
||||
typeof newUsername != "string" ||
|
||||
newUsername.length < 3 ||
|
||||
newUsername.length > 20
|
||||
) {
|
||||
return ServeError(
|
||||
ctx,
|
||||
400,
|
||||
"username must be between 3 and 20 characters in length"
|
||||
)
|
||||
}
|
||||
|
||||
if (Accounts.getFromUsername(newUsername)) {
|
||||
return ServeError(
|
||||
ctx,
|
||||
400,
|
||||
"account with this username already exists"
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
(newUsername.match(/[A-Za-z0-9_\-\.]+/) || [])[0] != body.username
|
||||
) {
|
||||
ServeError(ctx, 400, "username contains invalid characters")
|
||||
return
|
||||
}
|
||||
|
||||
Account.username = newUsername
|
||||
Accounts.save()
|
||||
|
||||
if (Account.email) {
|
||||
await sendMail(
|
||||
Account.email,
|
||||
`Your login details have been updated`,
|
||||
`<b>Hello there!</b> Your username has been updated to <span username>${newUsername}</span>. Please update your devices accordingly. Thank you for using monofile.`
|
||||
).catch()
|
||||
return ctx.text("OK")
|
||||
}
|
||||
})
|
||||
|
||||
return router
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,120 +1,119 @@
|
|||
// Modules
|
||||
|
||||
import { writeFile } from 'fs'
|
||||
import { Router } from "express";
|
||||
import bodyParser from "body-parser";
|
||||
import { writeFile } from "fs/promises"
|
||||
import { Hono } from "hono"
|
||||
|
||||
// Libs
|
||||
|
||||
import Files, { id_check_regex } from "../../../lib/files";
|
||||
import * as Accounts from '../../../lib/accounts'
|
||||
import * as Authentication from '../../../lib/auth'
|
||||
import { assertAPI, getAccount, noAPIAccess, requiresAccount, requiresAdmin, requiresPermissions } from "../../../lib/middleware";
|
||||
import ServeError from "../../../lib/errors";
|
||||
import { sendMail } from '../../../lib/mail';
|
||||
import Files, { id_check_regex } from "../../../lib/files"
|
||||
import * as Accounts from "../../../lib/accounts"
|
||||
import * as Authentication from "../../../lib/auth"
|
||||
import {
|
||||
getAccount,
|
||||
noAPIAccess,
|
||||
requiresAccount,
|
||||
requiresAdmin,
|
||||
} from "../../../lib/middleware"
|
||||
import ServeError from "../../../lib/errors"
|
||||
import { sendMail } from "../../../lib/mail"
|
||||
|
||||
const Configuration = require(`${process.cwd()}/config.json`)
|
||||
|
||||
const parser = bodyParser.json({
|
||||
type: [ "type/plain", "application/json" ]
|
||||
})
|
||||
const router = new Hono<{
|
||||
Variables: {
|
||||
account?: Accounts.Account
|
||||
}
|
||||
}>()
|
||||
|
||||
const router = Router()
|
||||
router.use(getAccount, requiresAccount, requiresAdmin)
|
||||
|
||||
router.use(getAccount, requiresAccount, requiresAdmin, parser)
|
||||
module.exports = function (files: Files) {
|
||||
router.patch("/account/:username/password", async (ctx) => {
|
||||
const Account = ctx.get("account") as Accounts.Account
|
||||
const body = await ctx.req.json()
|
||||
|
||||
module.exports = function(files: Files) {
|
||||
router.patch(
|
||||
"/account/:username/password",
|
||||
(req, res) => {
|
||||
const Account = res.locals.acc as Accounts.Account
|
||||
const targetUsername = ctx.req.param("username")
|
||||
const password = body.password
|
||||
|
||||
const targetUsername = req.params.username
|
||||
const password = req.body.password
|
||||
if (typeof password !== "string") return ServeError(ctx, 404, "")
|
||||
|
||||
if (typeof password !== "string") {
|
||||
ServeError(res, 404, "")
|
||||
return
|
||||
}
|
||||
const targetAccount = Accounts.getFromUsername(targetUsername)
|
||||
|
||||
const targetAccount = Accounts.getFromUsername(targetUsername)
|
||||
if (!targetAccount) return ServeError(ctx, 404, "")
|
||||
|
||||
if (!targetAccount) {
|
||||
ServeError(res, 404, "")
|
||||
return
|
||||
}
|
||||
Accounts.password.set(targetAccount.id, password)
|
||||
|
||||
Accounts.password.set( targetAccount.id, password )
|
||||
|
||||
Authentication.AuthTokens.filter(e => e.account == targetAccount?.id).forEach((accountToken) => {
|
||||
Authentication.invalidate(accountToken.token)
|
||||
})
|
||||
Authentication.AuthTokens.filter(
|
||||
(e) => e.account == targetAccount?.id
|
||||
).forEach((accountToken) => {
|
||||
Authentication.invalidate(accountToken.token)
|
||||
})
|
||||
|
||||
if (targetAccount.email) {
|
||||
sendMail(targetAccount.email, `Your login details have been updated`, `<b>Hello there!</b> This email is to notify you of a password change that an administrator, <span username>${Account.username}</span>, has initiated. You have been logged out of your devices. Thank you for using monofile.`).then(() => {
|
||||
res.send("OK")
|
||||
}).catch((err) => {})
|
||||
}
|
||||
|
||||
res.send()
|
||||
if (targetAccount.email) {
|
||||
await sendMail(
|
||||
targetAccount.email,
|
||||
`Your login details have been updated`,
|
||||
`<b>Hello there!</b> This email is to notify you of a password change that an administrator, <span username>${Account.username}</span>, has initiated. You have been logged out of your devices. Thank you for using monofile.`
|
||||
).catch()
|
||||
}
|
||||
)
|
||||
|
||||
router.patch(
|
||||
"/account/:username/elevate",
|
||||
(req, res) => {
|
||||
const targetUsername = req.params.username
|
||||
const targetAccount = Accounts.getFromUsername(targetUsername)
|
||||
return ctx.text("")
|
||||
})
|
||||
|
||||
if (!targetAccount) {
|
||||
ServeError(res, 404, "")
|
||||
return
|
||||
}
|
||||
router.patch("/account/:username/elevate", (ctx) => {
|
||||
const targetUsername = ctx.req.param("username")
|
||||
const targetAccount = Accounts.getFromUsername(targetUsername)
|
||||
|
||||
targetAccount.admin = true
|
||||
Accounts.save()
|
||||
|
||||
res.send()
|
||||
if (!targetAccount) {
|
||||
return ServeError(ctx, 404, "")
|
||||
}
|
||||
)
|
||||
|
||||
router.delete("/account/:username/:deleteFiles",
|
||||
targetAccount.admin = true
|
||||
Accounts.save()
|
||||
|
||||
return ctx.text("")
|
||||
})
|
||||
|
||||
router.delete(
|
||||
"/account/:username/:deleteFiles",
|
||||
requiresAccount,
|
||||
noAPIAccess,
|
||||
parser,
|
||||
(req, res) => {
|
||||
const targetUsername = req.params.username
|
||||
const deleteFiles = req.params.deleteFiles
|
||||
async (ctx) => {
|
||||
const targetUsername = ctx.req.param("username")
|
||||
const deleteFiles = ctx.req.param("deleteFiles")
|
||||
|
||||
const targetAccount = Accounts.getFromUsername(targetUsername)
|
||||
|
||||
if (!targetAccount) {
|
||||
ServeError(res, 404, "")
|
||||
return
|
||||
}
|
||||
if (!targetAccount) return ServeError(ctx, 404, "")
|
||||
|
||||
const accountId = targetAccount.id
|
||||
|
||||
Authentication.AuthTokens.filter(e => e.account == accountId).forEach((token) => {
|
||||
Authentication.AuthTokens.filter(
|
||||
(e) => e.account == accountId
|
||||
).forEach((token) => {
|
||||
Authentication.invalidate(token.token)
|
||||
})
|
||||
|
||||
const deleteAccount = () => Accounts.deleteAccount(accountId).then(_ => res.send("account deleted"))
|
||||
const deleteAccount = () =>
|
||||
Accounts.deleteAccount(accountId).then((_) =>
|
||||
ctx.text("account deleted")
|
||||
)
|
||||
|
||||
if (deleteFiles) {
|
||||
const Files = targetAccount.files.map(e => e)
|
||||
const Files = targetAccount.files.map((e) => e)
|
||||
|
||||
for (let fileId of Files) {
|
||||
files.unlink(fileId, true).catch(err => console.error)
|
||||
files.unlink(fileId, true).catch((err) => console.error)
|
||||
}
|
||||
|
||||
writeFile(process.cwd() + "/.data/files.json", JSON.stringify(files.files), (err) => {
|
||||
if (err) console.log(err)
|
||||
deleteAccount()
|
||||
})
|
||||
} else deleteAccount()
|
||||
await writeFile(
|
||||
process.cwd() + "/.data/files.json",
|
||||
JSON.stringify(files.files)
|
||||
)
|
||||
return deleteAccount()
|
||||
} else return deleteAccount()
|
||||
}
|
||||
)
|
||||
|
||||
return router
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue