mirror of
https://github.com/mollersuite/monofile.git
synced 2024-11-21 13:36:25 -08:00
refactor: ♻️ Honofile.
This commit is contained in:
parent
6220cd8b0f
commit
0366c91f74
|
@ -17,6 +17,7 @@
|
||||||
"node": ">=v16.11"
|
"node": ">=v16.11"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@hono/node-server": "^1.2.0",
|
||||||
"@types/body-parser": "^1.19.2",
|
"@types/body-parser": "^1.19.2",
|
||||||
"@types/express": "^4.17.14",
|
"@types/express": "^4.17.14",
|
||||||
"@types/multer": "^1.4.7",
|
"@types/multer": "^1.4.7",
|
||||||
|
|
|
@ -5,6 +5,9 @@ settings:
|
||||||
excludeLinksFromLockfile: false
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@hono/node-server':
|
||||||
|
specifier: ^1.2.0
|
||||||
|
version: 1.2.0
|
||||||
'@types/body-parser':
|
'@types/body-parser':
|
||||||
specifier: ^1.19.2
|
specifier: ^1.19.2
|
||||||
version: 1.19.3
|
version: 1.19.3
|
||||||
|
@ -337,6 +340,11 @@ packages:
|
||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
/@hono/node-server@1.2.0:
|
||||||
|
resolution: {integrity: sha512-aHT8lDMLpd7ioXJ1/057+h+oE/k7rCOWmjklYDsE0jE4CoNB9XzG4f8dRHvw4s5HJFocaYDiGgYM/V0kYbQ0ww==}
|
||||||
|
engines: {node: '>=18.0.0'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@jridgewell/sourcemap-codec@1.4.15:
|
/@jridgewell/sourcemap-codec@1.4.15:
|
||||||
resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==}
|
resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
|
@ -1,45 +1,63 @@
|
||||||
import cookieParser from "cookie-parser"
|
|
||||||
import { IntentsBitField, Client } from "discord.js"
|
import { IntentsBitField, Client } from "discord.js"
|
||||||
import express from "express"
|
import { serve } from "@hono/node-server"
|
||||||
|
import { serveStatic } from "@hono/node-server/serve-static"
|
||||||
|
import { Hono } from "hono"
|
||||||
import fs from "fs"
|
import fs from "fs"
|
||||||
import Files from "./lib/files"
|
import Files from "./lib/files"
|
||||||
import { getAccount } from "./lib/middleware"
|
import { getAccount } from "./lib/middleware"
|
||||||
|
|
||||||
import APIRouter from "./routes/api"
|
import APIRouter from "./routes/api"
|
||||||
import preview from "./preview"
|
import preview from "./preview"
|
||||||
|
|
||||||
require("dotenv").config()
|
require("dotenv").config()
|
||||||
|
|
||||||
const pkg = require(`${process.cwd()}/package.json`)
|
const pkg = require(`${process.cwd()}/package.json`)
|
||||||
let app = express()
|
const app = new Hono()
|
||||||
let config = require(`${process.cwd()}/config.json`)
|
let config = require(`${process.cwd()}/config.json`)
|
||||||
|
|
||||||
app.use("/static/assets", express.static("assets"))
|
app.get(
|
||||||
app.use("/static/vite", express.static("dist/static/vite"))
|
"/static/assets/*",
|
||||||
|
serveStatic({
|
||||||
|
rewriteRequestPath: (path) => {
|
||||||
|
return path.replace("/static/assets", "/assets")
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
app.get(
|
||||||
|
"/static/vite/*",
|
||||||
|
serveStatic({
|
||||||
|
rewriteRequestPath: (path) => {
|
||||||
|
return path.replace("/static/vite", "/dist/static/vite")
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
//app.use(bodyParser.text({limit:(config.maxDiscordFileSize*config.maxDiscordFiles)+1048576,type:["application/json","text/plain"]}))
|
//app.use(bodyParser.text({limit:(config.maxDiscordFileSize*config.maxDiscordFiles)+1048576,type:["application/json","text/plain"]}))
|
||||||
|
|
||||||
app.use(cookieParser())
|
|
||||||
|
|
||||||
// check for ssl, if not redirect
|
// check for ssl, if not redirect
|
||||||
if (config.trustProxy) app.enable("trust proxy")
|
if (config.trustProxy) {
|
||||||
|
// app.enable("trust proxy")
|
||||||
|
}
|
||||||
if (config.forceSSL) {
|
if (config.forceSSL) {
|
||||||
app.use((req, res, next) => {
|
app.use(async (ctx, next) => {
|
||||||
if (req.protocol == "http")
|
if (new URL(ctx.req.url).protocol == "http") {
|
||||||
res.redirect(`https://${req.get("host")}${req.originalUrl}`)
|
return ctx.redirect(
|
||||||
else next()
|
`https://${ctx.req.header("host")}${
|
||||||
|
new URL(ctx.req.url).pathname
|
||||||
|
}`
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return next()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
app.get("/server", (req, res) => {
|
app.get("/server", (ctx) =>
|
||||||
res.send(
|
ctx.json({
|
||||||
JSON.stringify({
|
...config,
|
||||||
...config,
|
version: pkg.version,
|
||||||
version: pkg.version,
|
files: Object.keys(files.files).length,
|
||||||
files: Object.keys(files.files).length,
|
})
|
||||||
})
|
)
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
// funcs
|
// funcs
|
||||||
|
|
||||||
|
@ -60,17 +78,19 @@ let client = new Client({
|
||||||
|
|
||||||
let files = new Files(client, config)
|
let files = new Files(client, config)
|
||||||
|
|
||||||
let apiRouter = new APIRouter(files)
|
const apiRouter = new APIRouter(files)
|
||||||
apiRouter.loadAPIMethods().then(() => {
|
apiRouter.loadAPIMethods().then(() => {
|
||||||
app.use(apiRouter.root)
|
app.route("/", apiRouter.root)
|
||||||
console.log("API OK!")
|
console.log("API OK!")
|
||||||
})
|
})
|
||||||
|
|
||||||
// index, clone
|
// index, clone
|
||||||
|
|
||||||
app.get("/", function (req, res) {
|
app.get("/", async (ctx) =>
|
||||||
res.sendFile(process.cwd() + "/dist/index.html")
|
ctx.html(
|
||||||
})
|
await fs.promises.readFile(process.cwd() + "/dist/index.html", "utf-8")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
// serve download page
|
// serve download page
|
||||||
|
|
||||||
|
@ -87,8 +107,16 @@ app.get("/download/:fileId", getAccount, preview(files))
|
||||||
|
|
||||||
// listen on 3000 or MONOFILE_PORT
|
// listen on 3000 or MONOFILE_PORT
|
||||||
|
|
||||||
app.listen(process.env.MONOFILE_PORT || 3000, function () {
|
serve(
|
||||||
console.log("Web OK!")
|
{
|
||||||
})
|
fetch: app.fetch,
|
||||||
|
port: Number(process.env.MONOFILE_PORT || 3000),
|
||||||
|
},
|
||||||
|
(info) => {
|
||||||
|
console.log("Web OK!", info.port, info.address)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
client.login(process.env.TOKEN)
|
client.login(process.env.TOKEN)
|
||||||
|
|
||||||
|
export = app
|
||||||
|
|
|
@ -1,48 +1,50 @@
|
||||||
import crypto from "crypto"
|
import crypto from "crypto"
|
||||||
import express from "express"
|
import { getCookie } from "hono/cookie"
|
||||||
|
import type { Context } from "hono"
|
||||||
import { readFile, writeFile } from "fs/promises"
|
import { readFile, writeFile } from "fs/promises"
|
||||||
export let AuthTokens: AuthToken[] = []
|
export let AuthTokens: AuthToken[] = []
|
||||||
export let AuthTokenTO:{[key:string]:NodeJS.Timeout} = {}
|
export let AuthTokenTO: { [key: string]: NodeJS.Timeout } = {}
|
||||||
|
|
||||||
export const ValidTokenPermissions = [
|
export const ValidTokenPermissions = [
|
||||||
"user", // permissions to /auth/me, with email docked
|
"user", // permissions to /auth/me, with email docked
|
||||||
"email", // adds email back to /auth/me
|
"email", // adds email back to /auth/me
|
||||||
"private", // allows app to read private files
|
"private", // allows app to read private files
|
||||||
"upload", // allows an app to upload under an account
|
"upload", // allows an app to upload under an account
|
||||||
"manage", // allows an app to manage an account's files
|
"manage", // allows an app to manage an account's files
|
||||||
"customize", // allows an app to change customization settings
|
"customize", // allows an app to change customization settings
|
||||||
"admin" // only available for accounts with admin
|
"admin", // only available for accounts with admin
|
||||||
// gives an app access to all admin tools
|
// gives an app access to all admin tools
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
export type TokenType = "User" | "App"
|
export type TokenType = "User" | "App"
|
||||||
export type TokenPermission = typeof ValidTokenPermissions[number]
|
export type TokenPermission = (typeof ValidTokenPermissions)[number]
|
||||||
|
|
||||||
export interface AuthToken {
|
export interface AuthToken {
|
||||||
account: string,
|
account: string
|
||||||
token: string,
|
token: string
|
||||||
expire: number,
|
expire: number
|
||||||
|
|
||||||
type?: TokenType, // if !type, assume User
|
type?: TokenType // if !type, assume User
|
||||||
tokenPermissions?: TokenPermission[] // default to user if type is App,
|
tokenPermissions?: TokenPermission[] // default to user if type is App,
|
||||||
// give full permissions if type is User
|
// give full permissions if type is User
|
||||||
}
|
}
|
||||||
|
|
||||||
export function create(
|
export function create(
|
||||||
id:string,
|
id: string,
|
||||||
expire:number=(24*60*60*1000),
|
expire: number = 24 * 60 * 60 * 1000,
|
||||||
type:TokenType="User",
|
type: TokenType = "User",
|
||||||
tokenPermissions?:TokenPermission[]
|
tokenPermissions?: TokenPermission[]
|
||||||
) {
|
) {
|
||||||
let token = {
|
let token = {
|
||||||
account:id,
|
account: id,
|
||||||
token:crypto.randomBytes(36).toString('hex'),
|
token: crypto.randomBytes(36).toString("hex"),
|
||||||
expire: expire ? Date.now()+expire : 0,
|
expire: expire ? Date.now() + expire : 0,
|
||||||
|
|
||||||
type,
|
type,
|
||||||
tokenPermissions: type == "App" ? tokenPermissions || ["user"] : undefined
|
tokenPermissions:
|
||||||
|
type == "App" ? tokenPermissions || ["user"] : undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
AuthTokens.push(token)
|
AuthTokens.push(token)
|
||||||
tokenTimer(token)
|
tokenTimer(token)
|
||||||
|
|
||||||
|
@ -51,56 +53,68 @@ export function create(
|
||||||
return token.token
|
return token.token
|
||||||
}
|
}
|
||||||
|
|
||||||
export function tokenFor(req: express.Request) {
|
export function tokenFor(ctx: Context) {
|
||||||
return req.cookies.auth || (
|
return (
|
||||||
req.header("authorization")?.startsWith("Bearer ")
|
getCookie(ctx, "auth") ||
|
||||||
? req.header("authorization")?.split(" ")[1]
|
(ctx.req.header("authorization")?.startsWith("Bearer ")
|
||||||
: undefined
|
? ctx.req.header("authorization")?.split(" ")[1]
|
||||||
|
: undefined)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getToken(token:string) {
|
function getToken(token: string) {
|
||||||
return AuthTokens.find(e => e.token == token && (e.expire == 0 || Date.now() < e.expire))
|
return AuthTokens.find(
|
||||||
|
(e) => e.token == token && (e.expire == 0 || Date.now() < e.expire)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validate(token:string) {
|
export function validate(token: string) {
|
||||||
return getToken(token)?.account
|
return getToken(token)?.account
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getType(token:string): TokenType | undefined {
|
export function getType(token: string): TokenType | undefined {
|
||||||
return getToken(token)?.type
|
return getToken(token)?.type
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPermissions(token:string): TokenPermission[] | undefined {
|
export function getPermissions(token: string): TokenPermission[] | undefined {
|
||||||
return getToken(token)?.tokenPermissions
|
return getToken(token)?.tokenPermissions
|
||||||
}
|
}
|
||||||
|
|
||||||
export function tokenTimer(token:AuthToken) {
|
export function tokenTimer(token: AuthToken) {
|
||||||
if (!token.expire) return // justincase
|
if (!token.expire) return // justincase
|
||||||
if (Date.now() >= token.expire) {
|
if (Date.now() >= token.expire) {
|
||||||
invalidate(token.token)
|
invalidate(token.token)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
AuthTokenTO[token.token] = setTimeout(() => invalidate(token.token),token.expire-Date.now())
|
AuthTokenTO[token.token] = setTimeout(
|
||||||
|
() => invalidate(token.token),
|
||||||
|
token.expire - Date.now()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function invalidate(token:string) {
|
export function invalidate(token: string) {
|
||||||
if (AuthTokenTO[token]) {
|
if (AuthTokenTO[token]) {
|
||||||
clearTimeout(AuthTokenTO[token])
|
clearTimeout(AuthTokenTO[token])
|
||||||
}
|
}
|
||||||
|
|
||||||
AuthTokens.splice(AuthTokens.findIndex(e => e.token == token),1)
|
AuthTokens.splice(
|
||||||
|
AuthTokens.findIndex((e) => e.token == token),
|
||||||
|
1
|
||||||
|
)
|
||||||
save()
|
save()
|
||||||
}
|
}
|
||||||
|
|
||||||
export function save() {
|
export function save() {
|
||||||
writeFile(`${process.cwd()}/.data/tokens.json`,JSON.stringify(AuthTokens))
|
writeFile(
|
||||||
.catch((err) => console.error(err))
|
`${process.cwd()}/.data/tokens.json`,
|
||||||
|
JSON.stringify(AuthTokens)
|
||||||
|
).catch((err) => console.error(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
readFile(`${process.cwd()}/.data/tokens.json`)
|
readFile(`${process.cwd()}/.data/tokens.json`)
|
||||||
.then((buf) => {
|
.then((buf) => {
|
||||||
AuthTokens = JSON.parse(buf.toString())
|
AuthTokens = JSON.parse(buf.toString())
|
||||||
AuthTokens.forEach(e => tokenTimer(e))
|
AuthTokens.forEach((e) => tokenTimer(e))
|
||||||
}).catch(err => console.error(err))
|
})
|
||||||
|
.catch((err) => console.error(err))
|
||||||
|
|
|
@ -1,48 +1,36 @@
|
||||||
import { Response } from "express";
|
|
||||||
import { readFile } from "fs/promises"
|
import { readFile } from "fs/promises"
|
||||||
|
import type { Context } from "hono"
|
||||||
|
|
||||||
let errorPage:string
|
let errorPage: string
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Serves an error as a response to a request with an error page attached
|
* @description Serves an error as a response to a request with an error page attached
|
||||||
* @param res Express response object
|
* @param ctx Express response object
|
||||||
* @param code Error code
|
* @param code Error code
|
||||||
* @param reason Error reason
|
* @param reason Error reason
|
||||||
*/
|
*/
|
||||||
export default async function ServeError(
|
export default async function ServeError(
|
||||||
res:Response,
|
ctx: Context,
|
||||||
code:number,
|
code: number,
|
||||||
reason:string
|
reason: string
|
||||||
) {
|
) {
|
||||||
// fetch error page if not cached
|
// fetch error page if not cached
|
||||||
if (!errorPage) {
|
if (!errorPage) {
|
||||||
errorPage =
|
errorPage = (
|
||||||
(
|
(await readFile(`${process.cwd()}/dist/error.html`).catch((err) =>
|
||||||
await readFile(`${process.cwd()}/dist/error.html`)
|
console.error(err)
|
||||||
.catch((err) => console.error(err))
|
)) || "<pre>$code $text</pre>"
|
||||||
|| "<pre>$code $text</pre>"
|
).toString()
|
||||||
)
|
|
||||||
.toString()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// serve error
|
// serve error
|
||||||
res.statusMessage = reason
|
return ctx.html(
|
||||||
res.status(code)
|
|
||||||
res.header("x-backup-status-message", reason) // glitch default nginx configuration
|
|
||||||
res.send(
|
|
||||||
errorPage
|
errorPage
|
||||||
.replaceAll("$code",code.toString())
|
.replaceAll("$code", code.toString())
|
||||||
.replaceAll("$text",reason)
|
.replaceAll("$text", reason),
|
||||||
|
code,
|
||||||
|
{
|
||||||
|
"x-backup-status-message": reason, // glitch default nginx configuration
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* @description Redirects a user to another page.
|
|
||||||
* @param res Express response object
|
|
||||||
* @param url Target URL
|
|
||||||
* @deprecated Use `res.redirect` instead.
|
|
||||||
*/
|
|
||||||
export function Redirect(res:Response,url:string) {
|
|
||||||
res.status(302)
|
|
||||||
res.header("Location",url)
|
|
||||||
res.send()
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,36 +1,34 @@
|
||||||
import * as Accounts from "./accounts";
|
import * as Accounts from "./accounts"
|
||||||
import express, { type RequestHandler } from "express"
|
import { Handler as RequestHandler } from "hono"
|
||||||
import ServeError from "../lib/errors";
|
import ServeError from "../lib/errors"
|
||||||
import * as auth from "./auth";
|
import * as auth from "./auth"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Middleware which adds an account, if any, to res.locals.acc
|
* @description Middleware which adds an account, if any, to ctx.get("account")
|
||||||
*/
|
*/
|
||||||
export const getAccount: RequestHandler = function(req, res, next) {
|
export const getAccount: RequestHandler = function (ctx, next) {
|
||||||
res.locals.acc = Accounts.getFromToken(auth.tokenFor(req))
|
ctx.set("account", Accounts.getFromToken(auth.tokenFor(ctx)!))
|
||||||
next()
|
return next()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Middleware which blocks requests which do not have res.locals.acc set
|
* @description Middleware which blocks requests which do not have ctx.get("account") set
|
||||||
*/
|
*/
|
||||||
export const requiresAccount: RequestHandler = function(_req, res, next) {
|
export const requiresAccount: RequestHandler = function (ctx, next) {
|
||||||
if (!res.locals.acc) {
|
if (!ctx.get("account")) {
|
||||||
ServeError(res, 401, "not logged in")
|
return ServeError(ctx, 401, "not logged in")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
next()
|
return next()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Middleware which blocks requests that have res.locals.acc.admin set to a falsy value
|
* @description Middleware which blocks requests that have ctx.get("account").admin set to a falsy value
|
||||||
*/
|
*/
|
||||||
export const requiresAdmin: RequestHandler = function(_req, res, next) {
|
export const requiresAdmin: RequestHandler = function (ctx, next) {
|
||||||
if (!res.locals.acc.admin) {
|
if (!ctx.get("account").admin) {
|
||||||
ServeError(res, 403, "you are not an administrator")
|
return ServeError(ctx, 403, "you are not an administrator")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
next()
|
return next()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -39,48 +37,58 @@ export const requiresAdmin: RequestHandler = function(_req, res, next) {
|
||||||
* @returns Express middleware
|
* @returns Express middleware
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const requiresPermissions = function(...tokenPermissions: auth.TokenPermission[]): RequestHandler {
|
export const requiresPermissions = function (
|
||||||
return function(req, res, next) {
|
...tokenPermissions: auth.TokenPermission[]
|
||||||
let token = auth.tokenFor(req)
|
): RequestHandler {
|
||||||
|
return function (ctx, next) {
|
||||||
|
let token = auth.tokenFor(ctx)!
|
||||||
let type = auth.getType(token)
|
let type = auth.getType(token)
|
||||||
|
|
||||||
if (type == "App") {
|
if (type == "App") {
|
||||||
let permissions = auth.getPermissions(token)
|
let permissions = auth.getPermissions(token)
|
||||||
|
|
||||||
if (!permissions) ServeError(res, 403, "insufficient permissions")
|
|
||||||
else {
|
|
||||||
|
|
||||||
|
if (!permissions) return ServeError(ctx, 403, "insufficient permissions")
|
||||||
|
else {
|
||||||
for (let v of tokenPermissions) {
|
for (let v of tokenPermissions) {
|
||||||
if (!permissions.includes(v as auth.TokenPermission)) {
|
if (!permissions.includes(v as auth.TokenPermission)) {
|
||||||
ServeError(res,403,"insufficient permissions")
|
return ServeError(ctx, 403, "insufficient permissions")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
next()
|
return next()
|
||||||
|
|
||||||
}
|
}
|
||||||
} else next()
|
} else return next()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Blocks requests based on whether or not the token being used to access the route is of type `User`.
|
* @description Blocks requests based on whether or not the token being used to access the route is of type `User`.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const noAPIAccess: RequestHandler = function(req, res, next) {
|
export const noAPIAccess: RequestHandler = function (ctx, next) {
|
||||||
if (auth.getType(auth.tokenFor(req)) == "App") ServeError(res, 403, "apps are not allowed to access this endpoint")
|
if (auth.getType(auth.tokenFor(ctx)!) == "App")
|
||||||
else next()
|
return ServeError(ctx, 403, "apps are not allowed to access this endpoint")
|
||||||
|
else return next()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@description Add a restriction to this route; the condition must be true to allow API requests.
|
@description Add a restriction to this route; the condition must be true to allow API requests.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const assertAPI = function(condition: (acc:Accounts.Account, token:string) => boolean):RequestHandler {
|
export const assertAPI = function (
|
||||||
return function(req, res, next) {
|
condition: (acc: Accounts.Account, token: string) => boolean
|
||||||
let reqToken = auth.tokenFor(req)
|
): RequestHandler {
|
||||||
if (auth.getType(reqToken) == "App" && condition(res.locals.acc, reqToken)) ServeError(res, 403, "apps are not allowed to access this endpoint")
|
return function (ctx, next) {
|
||||||
else next()
|
let reqToken = auth.tokenFor(ctx)!
|
||||||
|
if (
|
||||||
|
auth.getType(reqToken) == "App" &&
|
||||||
|
condition(ctx.get("account"), reqToken)
|
||||||
|
)
|
||||||
|
return ServeError(
|
||||||
|
ctx,
|
||||||
|
403,
|
||||||
|
"apps are not allowed to access this endpoint"
|
||||||
|
)
|
||||||
|
else return next()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,21 +102,10 @@ interface SchemeObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SchemeArray {
|
interface SchemeArray {
|
||||||
type: "array",
|
type: "array"
|
||||||
children: SchemeParameter /* All children of the array must be this type */
|
children:
|
||||||
| SchemeParameter[] /* Array must match this pattern */
|
| SchemeParameter /* All children of the array must be this type */
|
||||||
|
| SchemeParameter[] /* Array must match this pattern */
|
||||||
}
|
}
|
||||||
|
|
||||||
type SchemeParameter = SchemeType | SchemeObject | SchemeArray
|
type SchemeParameter = SchemeType | SchemeObject | SchemeArray
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Blocks requests based on whether or not the token being used to access the route is of type `User` unless a condition is met.
|
|
||||||
* @param tokenPermissions Permissions which your route requires.
|
|
||||||
* @returns Express middleware
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const sanitize = function(scheme: SchemeObject):RequestHandler {
|
|
||||||
return function(req, res, next) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,50 +1,50 @@
|
||||||
import { RequestHandler } from "express"
|
import type { Handler } from "hono"
|
||||||
import { type Account } from "./accounts"
|
|
||||||
import ServeError from "./errors"
|
import ServeError from "./errors"
|
||||||
|
|
||||||
interface RatelimitSettings {
|
interface RatelimitSettings {
|
||||||
|
|
||||||
requests: number
|
requests: number
|
||||||
per: number
|
per: number
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Ratelimits a route based on res.locals.acc
|
* @description Ratelimits a route based on ctx.get("account")
|
||||||
* @param settings Ratelimit settings
|
* @param settings Ratelimit settings
|
||||||
* @returns Express middleware
|
* @returns Express middleware
|
||||||
*/
|
*/
|
||||||
export function accountRatelimit( settings: RatelimitSettings ): RequestHandler {
|
export function accountRatelimit(settings: RatelimitSettings): Handler {
|
||||||
let activeLimits: {
|
let activeLimits: {
|
||||||
[ key: string ]: {
|
[key: string]: {
|
||||||
requests: number,
|
requests: number
|
||||||
expirationHold: NodeJS.Timeout
|
expirationHold: NodeJS.Timeout
|
||||||
}
|
}
|
||||||
} = {}
|
} = {}
|
||||||
|
|
||||||
return (req, res, next) => {
|
return (ctx, next) => {
|
||||||
if (res.locals.acc) {
|
if (ctx.get("account")) {
|
||||||
let accId = res.locals.acc.id
|
let accId = ctx.get("account").id
|
||||||
let aL = activeLimits[accId]
|
let aL = activeLimits[accId]
|
||||||
|
|
||||||
if (!aL) {
|
if (!aL) {
|
||||||
activeLimits[accId] = {
|
activeLimits[accId] = {
|
||||||
requests: 0,
|
requests: 0,
|
||||||
expirationHold: setTimeout(() => delete activeLimits[accId], settings.per)
|
expirationHold: setTimeout(
|
||||||
|
() => delete activeLimits[accId],
|
||||||
|
settings.per
|
||||||
|
),
|
||||||
}
|
}
|
||||||
aL = activeLimits[accId]
|
aL = activeLimits[accId]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (aL.requests < settings.requests) {
|
if (aL.requests < settings.requests) {
|
||||||
res.locals.undoCount = () => {
|
ctx.set("undoCount", () => {
|
||||||
if (activeLimits[accId]) {
|
if (activeLimits[accId]) {
|
||||||
activeLimits[accId].requests--
|
activeLimits[accId].requests--
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
next()
|
return next()
|
||||||
} else {
|
} else {
|
||||||
ServeError(res, 429, "too many requests")
|
return ServeError(ctx, 429, "too many requests")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,31 +2,32 @@ import fs from "fs/promises"
|
||||||
import bytes from "bytes"
|
import bytes from "bytes"
|
||||||
import ServeError from "./lib/errors"
|
import ServeError from "./lib/errors"
|
||||||
import * as Accounts from "./lib/accounts"
|
import * as Accounts from "./lib/accounts"
|
||||||
import type { Handler } from "express"
|
import type { Handler } from "hono"
|
||||||
import type Files from "./lib/files"
|
import type Files from "./lib/files"
|
||||||
const pkg = require(`${process.cwd()}/package.json`)
|
const pkg = require(`${process.cwd()}/package.json`)
|
||||||
export = (files: Files): Handler =>
|
export = (files: Files): Handler =>
|
||||||
async (req, res) => {
|
async (ctx) => {
|
||||||
let acc = res.locals.acc as Accounts.Account
|
let acc = ctx.get("account") as Accounts.Account
|
||||||
const file = files.getFilePointer(req.params.fileId)
|
const fileId = ctx.req.param("fileId")
|
||||||
|
const host = ctx.req.header("Host")
|
||||||
|
const file = files.getFilePointer(fileId)
|
||||||
if (file) {
|
if (file) {
|
||||||
if (file.visibility == "private" && acc?.id != file.owner) {
|
if (file.visibility == "private" && acc?.id != file.owner) {
|
||||||
ServeError(res, 403, "you do not own this file")
|
return ServeError(ctx, 403, "you do not own this file")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const template = await fs
|
const template = await fs
|
||||||
.readFile(process.cwd() + "/dist/download.html", "utf8")
|
.readFile(process.cwd() + "/dist/download.html", "utf8")
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
throw res.sendStatus(500)
|
throw ctx.status(500)
|
||||||
})
|
})
|
||||||
let fileOwner = file.owner
|
let fileOwner = file.owner
|
||||||
? Accounts.getFromId(file.owner)
|
? Accounts.getFromId(file.owner)
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
res.send(
|
return ctx.html(
|
||||||
template
|
template
|
||||||
.replaceAll("$FileId", req.params.fileId)
|
.replaceAll("$FileId", fileId)
|
||||||
.replaceAll("$Version", pkg.version)
|
.replaceAll("$Version", pkg.version)
|
||||||
.replaceAll(
|
.replaceAll(
|
||||||
"$FileSize",
|
"$FileSize",
|
||||||
|
@ -44,18 +45,14 @@ export = (files: Files): Handler =>
|
||||||
.replace(
|
.replace(
|
||||||
"<!--metaTags-->",
|
"<!--metaTags-->",
|
||||||
(file.mime.startsWith("image/")
|
(file.mime.startsWith("image/")
|
||||||
? `<meta name="og:image" content="https://${req.headers.host}/file/${req.params.fileId}" />`
|
? `<meta name="og:image" content="https://${host}/file/${fileId}" />`
|
||||||
: file.mime.startsWith("video/")
|
: file.mime.startsWith("video/")
|
||||||
? `<meta property="og:video:url" content="https://${
|
? `<meta property="og:video:url" content="https://${host}/cpt/${fileId}/video.${
|
||||||
req.headers.host
|
|
||||||
}/cpt/${req.params.fileId}/video.${
|
|
||||||
file.mime.split("/")[1] == "quicktime"
|
file.mime.split("/")[1] == "quicktime"
|
||||||
? "mov"
|
? "mov"
|
||||||
: file.mime.split("/")[1]
|
: file.mime.split("/")[1]
|
||||||
}" />
|
}" />
|
||||||
<meta property="og:video:secure_url" content="https://${
|
<meta property="og:video:secure_url" content="https://${host}/cpt/${fileId}/video.${
|
||||||
req.headers.host
|
|
||||||
}/cpt/${req.params.fileId}/video.${
|
|
||||||
file.mime.split("/")[1] == "quicktime"
|
file.mime.split("/")[1] == "quicktime"
|
||||||
? "mov"
|
? "mov"
|
||||||
: file.mime.split("/")[1]
|
: file.mime.split("/")[1]
|
||||||
|
@ -79,7 +76,7 @@ export = (files: Files): Handler =>
|
||||||
`\n<meta name="theme-color" content="${
|
`\n<meta name="theme-color" content="${
|
||||||
fileOwner?.embed?.color &&
|
fileOwner?.embed?.color &&
|
||||||
file.visibility != "anonymous" &&
|
file.visibility != "anonymous" &&
|
||||||
(req.headers["user-agent"] || "").includes(
|
(ctx.req.header("user-agent") || "").includes(
|
||||||
"Discordbot"
|
"Discordbot"
|
||||||
)
|
)
|
||||||
? `#${fileOwner.embed.color}`
|
? `#${fileOwner.embed.color}`
|
||||||
|
@ -89,11 +86,11 @@ export = (files: Files): Handler =>
|
||||||
.replace(
|
.replace(
|
||||||
"<!--preview-->",
|
"<!--preview-->",
|
||||||
file.mime.startsWith("image/")
|
file.mime.startsWith("image/")
|
||||||
? `<div style="min-height:10px"></div><img src="/file/${req.params.fileId}" />`
|
? `<div style="min-height:10px"></div><img src="/file/${fileId}" />`
|
||||||
: file.mime.startsWith("video/")
|
: file.mime.startsWith("video/")
|
||||||
? `<div style="min-height:10px"></div><video src="/file/${req.params.fileId}" controls></video>`
|
? `<div style="min-height:10px"></div><video src="/file/${fileId}" controls></video>`
|
||||||
: file.mime.startsWith("audio/")
|
: file.mime.startsWith("audio/")
|
||||||
? `<div style="min-height:10px"></div><audio src="/file/${req.params.fileId}" controls></audio>`
|
? `<div style="min-height:10px"></div><audio src="/file/${fileId}" controls></audio>`
|
||||||
: ""
|
: ""
|
||||||
)
|
)
|
||||||
.replaceAll(
|
.replaceAll(
|
||||||
|
@ -104,6 +101,6 @@ export = (files: Files): Handler =>
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
ServeError(res, 404, "file not found")
|
ServeError(ctx, 404, "file not found")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { Router } from "express";
|
import { Hono } from "hono"
|
||||||
import { readFile, readdir } from "fs/promises";
|
import { readFile, readdir } from "fs/promises"
|
||||||
import Files from "../lib/files";
|
import Files from "../lib/files"
|
||||||
|
|
||||||
const APIDirectory = __dirname+"/api"
|
const APIDirectory = __dirname + "/api"
|
||||||
|
|
||||||
interface APIMount {
|
interface APIMount {
|
||||||
file: string
|
file: string
|
||||||
|
@ -18,35 +18,35 @@ interface APIDefinition {
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveMount(mount: APIMountResolvable): APIMount {
|
function resolveMount(mount: APIMountResolvable): APIMount {
|
||||||
return typeof mount == "string" ? { file: mount, to: "/"+mount } : mount
|
return typeof mount == "string" ? { file: mount, to: "/" + mount } : mount
|
||||||
}
|
}
|
||||||
|
|
||||||
class APIVersion {
|
class APIVersion {
|
||||||
readonly definition: APIDefinition;
|
readonly definition: APIDefinition
|
||||||
readonly apiPath: string;
|
readonly apiPath: string
|
||||||
readonly root: Router = Router();
|
readonly root: Hono = new Hono()
|
||||||
|
|
||||||
constructor(definition: APIDefinition, files: Files) {
|
constructor(definition: APIDefinition, files: Files) {
|
||||||
|
this.definition = definition
|
||||||
this.definition = definition;
|
|
||||||
this.apiPath = APIDirectory + "/" + definition.name
|
this.apiPath = APIDirectory + "/" + definition.name
|
||||||
|
|
||||||
for (let _mount of definition.mount) {
|
for (let _mount of definition.mount) {
|
||||||
let mount = resolveMount(_mount)
|
let mount = resolveMount(_mount)
|
||||||
// no idea if there's a better way to do this but this is all i can think of
|
// no idea if there's a better way to do this but this is all i can think of
|
||||||
let route = require(`${this.apiPath}/${mount.file}.js`) as (files:Files)=>Router
|
let route = require(`${this.apiPath}/${mount.file}.js`) as (
|
||||||
this.root.use(mount.to, route(files))
|
files: Files
|
||||||
|
) => Hono
|
||||||
|
this.root.route(mount.to, route(files))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class APIRouter {
|
export default class APIRouter {
|
||||||
|
|
||||||
readonly files: Files
|
readonly files: Files
|
||||||
readonly root: Router = Router();
|
readonly root: Hono = new Hono()
|
||||||
|
|
||||||
constructor(files: Files) {
|
constructor(files: Files) {
|
||||||
this.files = files;
|
this.files = files
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -55,24 +55,26 @@ export default class APIRouter {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
private mount(definition: APIDefinition) {
|
private mount(definition: APIDefinition) {
|
||||||
|
|
||||||
console.log(`mounting APIDefinition ${definition.name}`)
|
console.log(`mounting APIDefinition ${definition.name}`)
|
||||||
|
|
||||||
this.root.use(
|
|
||||||
definition.baseURL,
|
|
||||||
(new APIVersion(definition, this.files)).root
|
|
||||||
)
|
|
||||||
|
|
||||||
|
this.root.route(
|
||||||
|
definition.baseURL,
|
||||||
|
new APIVersion(definition, this.files).root
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadAPIMethods() {
|
async loadAPIMethods() {
|
||||||
|
|
||||||
let files = await readdir(APIDirectory)
|
let files = await readdir(APIDirectory)
|
||||||
for (let v of files) { /// temporary (hopefully). need to figure out something else for this
|
for (let version of files) {
|
||||||
let def = JSON.parse((await readFile(`${process.cwd()}/src/server/routes/api/${v}/api.json`)).toString()) as APIDefinition
|
/// temporary (hopefully). need to figure out something else for this
|
||||||
|
let def = JSON.parse(
|
||||||
|
(
|
||||||
|
await readFile(
|
||||||
|
`${process.cwd()}/src/server/routes/api/${version}/api.json`
|
||||||
|
)
|
||||||
|
).toString()
|
||||||
|
) as APIDefinition
|
||||||
this.mount(def)
|
this.mount(def)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
|
|
|
@ -1,20 +1,21 @@
|
||||||
import bodyParser from "body-parser";
|
import { Hono } from "hono"
|
||||||
import { Router } from "express";
|
import * as Accounts from "../../../lib/accounts"
|
||||||
import * as Accounts from "../../../lib/accounts";
|
import * as auth from "../../../lib/auth"
|
||||||
import * as auth from "../../../lib/auth";
|
import { writeFile } from "fs/promises"
|
||||||
import bytes from "bytes"
|
import { sendMail } from "../../../lib/mail"
|
||||||
import {writeFile} from "fs";
|
import {
|
||||||
import { sendMail } from "../../../lib/mail";
|
getAccount,
|
||||||
import { getAccount, requiresAccount, requiresAdmin, requiresPermissions } from "../../../lib/middleware"
|
requiresAccount,
|
||||||
|
requiresAdmin,
|
||||||
|
requiresPermissions,
|
||||||
|
} from "../../../lib/middleware"
|
||||||
|
import Files from "../../../lib/files"
|
||||||
|
|
||||||
import ServeError from "../../../lib/errors";
|
export let adminRoutes = new Hono<{
|
||||||
import Files from "../../../lib/files";
|
Variables: {
|
||||||
|
account: Accounts.Account
|
||||||
let parser = bodyParser.json({
|
}
|
||||||
type: ["text/plain","application/json"]
|
}>()
|
||||||
})
|
|
||||||
|
|
||||||
export let adminRoutes = Router();
|
|
||||||
adminRoutes
|
adminRoutes
|
||||||
.use(getAccount)
|
.use(getAccount)
|
||||||
.use(requiresAccount)
|
.use(requiresAccount)
|
||||||
|
@ -23,214 +24,198 @@ adminRoutes
|
||||||
|
|
||||||
let config = require(`${process.cwd()}/config.json`)
|
let config = require(`${process.cwd()}/config.json`)
|
||||||
|
|
||||||
module.exports = function(files: Files) {
|
module.exports = function (files: Files) {
|
||||||
|
adminRoutes.post("/reset", async (ctx) => {
|
||||||
|
let acc = ctx.get("account") as Accounts.Account
|
||||||
|
const body = await ctx.req.json()
|
||||||
|
|
||||||
adminRoutes.post("/reset", parser, (req,res) => {
|
if (
|
||||||
|
typeof body.target !== "string" ||
|
||||||
let acc = res.locals.acc as Accounts.Account
|
typeof body.password !== "string"
|
||||||
|
) {
|
||||||
if (typeof req.body.target !== "string" || typeof req.body.password !== "string") {
|
return ctx.status(404)
|
||||||
res.status(404)
|
|
||||||
res.send()
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let targetAccount = Accounts.getFromUsername(req.body.target)
|
let targetAccount = Accounts.getFromUsername(body.target)
|
||||||
if (!targetAccount) {
|
if (!targetAccount) {
|
||||||
res.status(404)
|
return ctx.status(404)
|
||||||
res.send()
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Accounts.password.set ( targetAccount.id, req.body.password )
|
Accounts.password.set(targetAccount.id, body.password)
|
||||||
auth.AuthTokens.filter(e => e.account == targetAccount?.id).forEach((v) => {
|
auth.AuthTokens.filter((e) => e.account == targetAccount?.id).forEach(
|
||||||
auth.invalidate(v.token)
|
(v) => {
|
||||||
})
|
auth.invalidate(v.token)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
if (targetAccount.email) {
|
if (targetAccount.email) {
|
||||||
sendMail(targetAccount.email, `Your login details have been updated`, `<b>Hello there!</b> This email is to notify you of a password change that an administrator, <span username>${acc.username}</span>, has initiated. You have been logged out of your devices. Thank you for using monofile.`).then(() => {
|
return sendMail(
|
||||||
res.send("OK")
|
targetAccount.email,
|
||||||
}).catch((err) => {})
|
`Your login details have been updated`,
|
||||||
|
`<b>Hello there!</b> This email is to notify you of a password change that an administrator, <span username>${acc.username}</span>, has initiated. You have been logged out of your devices. Thank you for using monofile.`
|
||||||
|
)
|
||||||
|
.then(() => ctx.text("OK"))
|
||||||
|
.catch(() => ctx.status(500))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
res.send()
|
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
adminRoutes.post("/elevate", parser, (req,res) => {
|
adminRoutes.post("/elevate", async (ctx) => {
|
||||||
|
const body = await ctx.req.json()
|
||||||
|
let acc = ctx.get("account") as Accounts.Account
|
||||||
|
|
||||||
let acc = res.locals.acc as Accounts.Account
|
if (typeof body.target !== "string") {
|
||||||
|
return ctx.status(404)
|
||||||
if (typeof req.body.target !== "string") {
|
|
||||||
res.status(404)
|
|
||||||
res.send()
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let targetAccount = Accounts.getFromUsername(req.body.target)
|
let targetAccount = Accounts.getFromUsername(body.target)
|
||||||
if (!targetAccount) {
|
if (!targetAccount) {
|
||||||
res.status(404)
|
return ctx.status(404)
|
||||||
res.send()
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
targetAccount.admin = true;
|
|
||||||
Accounts.save()
|
Accounts.save()
|
||||||
res.send()
|
return ctx.text("OK")
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
adminRoutes.post("/delete", parser, (req,res) => {
|
adminRoutes.post("/delete", async (ctx) => {
|
||||||
|
const body = await ctx.req.json()
|
||||||
if (typeof req.body.target !== "string") {
|
if (typeof body.target !== "string") {
|
||||||
res.status(404)
|
return ctx.status(404)
|
||||||
res.send()
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let targetFile = files.getFilePointer(req.body.target)
|
let targetFile = files.getFilePointer(body.target)
|
||||||
|
|
||||||
if (!targetFile) {
|
if (!targetFile) {
|
||||||
res.status(404)
|
return ctx.status(404)
|
||||||
res.send()
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
files.unlink(req.body.target).then(() => {
|
return files
|
||||||
res.status(200)
|
.unlink(body.target)
|
||||||
}).catch(() => {
|
.then(() => ctx.status(200))
|
||||||
res.status(500)
|
.catch(() => ctx.status(500))
|
||||||
}).finally(() => res.send())
|
.finally(() => ctx.status(200))
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
adminRoutes.post("/delete_account", parser, async (req,res) => {
|
adminRoutes.post("/delete_account", async (ctx) => {
|
||||||
|
let acc = ctx.get("account") as Accounts.Account
|
||||||
let acc = res.locals.acc as Accounts.Account
|
const body = await ctx.req.json()
|
||||||
|
if (typeof body.target !== "string") {
|
||||||
if (typeof req.body.target !== "string") {
|
return ctx.status(404)
|
||||||
res.status(404)
|
|
||||||
res.send()
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let targetAccount = Accounts.getFromUsername(req.body.target)
|
let targetAccount = Accounts.getFromUsername(body.target)
|
||||||
if (!targetAccount) {
|
if (!targetAccount) {
|
||||||
res.status(404)
|
return ctx.status(404)
|
||||||
res.send()
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let accId = targetAccount.id
|
let accId = targetAccount.id
|
||||||
|
|
||||||
auth.AuthTokens.filter(e => e.account == accId).forEach((v) => {
|
auth.AuthTokens.filter((e) => e.account == accId).forEach((v) => {
|
||||||
auth.invalidate(v.token)
|
auth.invalidate(v.token)
|
||||||
})
|
})
|
||||||
|
|
||||||
let cpl = () => Accounts.deleteAccount(accId).then(_ => {
|
let cpl = () =>
|
||||||
if (targetAccount?.email) {
|
Accounts.deleteAccount(accId).then((_) => {
|
||||||
sendMail(targetAccount.email, "Notice of account deletion", `Your account, <span username>${targetAccount.username}</span>, has been deleted by <span username>${acc.username}</span> for the following reason: <br><br><span style="font-weight:600">${req.body.reason || "(no reason specified)"}</span><br><br> Your files ${req.body.deleteFiles ? "have been deleted" : "have not been modified"}. Thank you for using monofile.`)
|
if (targetAccount?.email) {
|
||||||
}
|
sendMail(
|
||||||
res.send("account deleted")
|
targetAccount.email,
|
||||||
})
|
"Notice of account deletion",
|
||||||
|
`Your account, <span username>${
|
||||||
if (req.body.deleteFiles) {
|
targetAccount.username
|
||||||
let f = targetAccount.files.map(e=>e) // make shallow copy so that iterating over it doesnt Die
|
}</span>, has been deleted by <span username>${
|
||||||
|
acc.username
|
||||||
|
}</span> for the following reason: <br><br><span style="font-weight:600">${
|
||||||
|
body.reason || "(no reason specified)"
|
||||||
|
}</span><br><br> Your files ${
|
||||||
|
body.deleteFiles
|
||||||
|
? "have been deleted"
|
||||||
|
: "have not been modified"
|
||||||
|
}. Thank you for using monofile.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return ctx.text("account deleted")
|
||||||
|
})
|
||||||
|
|
||||||
|
if (body.deleteFiles) {
|
||||||
|
let f = targetAccount.files.map((e) => e) // make shallow copy so that iterating over it doesnt Die
|
||||||
for (let v of f) {
|
for (let v of f) {
|
||||||
files.unlink(v,true).catch(err => console.error(err))
|
files.unlink(v, true).catch((err) => console.error(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
writeFile(process.cwd()+"/.data/files.json",JSON.stringify(files.files), (err) => {
|
return writeFile(
|
||||||
if (err) console.log(err)
|
process.cwd() + "/.data/files.json",
|
||||||
cpl()
|
JSON.stringify(files.files)
|
||||||
})
|
).then(cpl)
|
||||||
} else cpl()
|
} else return cpl()
|
||||||
})
|
})
|
||||||
|
|
||||||
adminRoutes.post("/transfer", parser, (req,res) => {
|
adminRoutes.post("/transfer", async (ctx) => {
|
||||||
|
const body = await ctx.req.json()
|
||||||
if (typeof req.body.target !== "string" || typeof req.body.owner !== "string") {
|
if (typeof body.target !== "string" || typeof body.owner !== "string") {
|
||||||
res.status(404)
|
return ctx.status(404)
|
||||||
res.send()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let targetFile = files.getFilePointer(req.body.target)
|
|
||||||
if (!targetFile) {
|
|
||||||
res.status(404)
|
|
||||||
res.send()
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let newOwner = Accounts.getFromUsername(req.body.owner || "")
|
let targetFile = files.getFilePointer(body.target)
|
||||||
|
if (!targetFile) {
|
||||||
|
return ctx.status(404)
|
||||||
|
}
|
||||||
|
|
||||||
|
let newOwner = Accounts.getFromUsername(body.owner || "")
|
||||||
|
|
||||||
// clear old owner
|
// clear old owner
|
||||||
|
|
||||||
if (targetFile.owner) {
|
if (targetFile.owner) {
|
||||||
let oldOwner = Accounts.getFromId(targetFile.owner)
|
let oldOwner = Accounts.getFromId(targetFile.owner)
|
||||||
if (oldOwner) {
|
if (oldOwner) {
|
||||||
Accounts.files.deindex(oldOwner.id, req.body.target)
|
Accounts.files.deindex(oldOwner.id, body.target)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newOwner) {
|
if (newOwner) {
|
||||||
Accounts.files.index(newOwner.id, req.body.target)
|
Accounts.files.index(newOwner.id, body.target)
|
||||||
}
|
}
|
||||||
targetFile.owner = newOwner ? newOwner.id : undefined;
|
targetFile.owner = newOwner ? newOwner.id : undefined
|
||||||
|
|
||||||
files.writeFile(req.body.target, targetFile).then(() => {
|
|
||||||
res.send()
|
|
||||||
}).catch(() => {
|
|
||||||
res.status(500)
|
|
||||||
res.send()
|
|
||||||
}) // wasting a reassignment but whatee
|
|
||||||
|
|
||||||
|
files
|
||||||
|
.writeFile(body.target, targetFile)
|
||||||
|
.then(() => ctx.status(200))
|
||||||
|
.catch(() => ctx.status(500))
|
||||||
})
|
})
|
||||||
|
|
||||||
adminRoutes.post("/idchange", parser, (req,res) => {
|
adminRoutes.post("/idchange", async (ctx) => {
|
||||||
|
const body = await ctx.req.json()
|
||||||
if (typeof req.body.target !== "string" || typeof req.body.new !== "string") {
|
if (typeof body.target !== "string" || typeof body.new !== "string") {
|
||||||
res.status(400)
|
return ctx.status(400)
|
||||||
res.send()
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let targetFile = files.getFilePointer(req.body.target)
|
let targetFile = files.getFilePointer(body.target)
|
||||||
if (!targetFile) {
|
if (!targetFile) {
|
||||||
res.status(404)
|
return ctx.status(404)
|
||||||
res.send()
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (files.getFilePointer(req.body.new)) {
|
if (files.getFilePointer(body.new)) {
|
||||||
res.status(400)
|
return ctx.status(400)
|
||||||
res.send()
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetFile.owner) {
|
if (targetFile.owner) {
|
||||||
Accounts.files.deindex(targetFile.owner, req.body.target)
|
Accounts.files.deindex(targetFile.owner, body.target)
|
||||||
Accounts.files.index(targetFile.owner, req.body.new)
|
Accounts.files.index(targetFile.owner, body.new)
|
||||||
}
|
}
|
||||||
delete files.files[req.body.target]
|
delete files.files[body.target]
|
||||||
|
|
||||||
files.writeFile(req.body.new, targetFile).then(() => {
|
return files
|
||||||
res.send()
|
.writeFile(body.new, targetFile)
|
||||||
}).catch(() => {
|
.then(() => ctx.status(200))
|
||||||
files.files[req.body.target] = req.body.new
|
.catch(() => {
|
||||||
|
files.files[body.target] = body.new
|
||||||
|
|
||||||
if (targetFile.owner) {
|
if (targetFile.owner) {
|
||||||
Accounts.files.deindex(targetFile.owner, req.body.new)
|
Accounts.files.deindex(targetFile.owner, body.new)
|
||||||
Accounts.files.index(targetFile.owner, req.body.target)
|
Accounts.files.index(targetFile.owner, body.target)
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(500)
|
|
||||||
res.send()
|
|
||||||
})
|
|
||||||
|
|
||||||
|
return ctx.status(500)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return adminRoutes
|
return adminRoutes
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,356 +1,483 @@
|
||||||
import bodyParser from "body-parser";
|
import { Hono, Handler } from "hono"
|
||||||
import { Router } from "express";
|
import { getCookie, setCookie } from "hono/cookie"
|
||||||
import * as Accounts from "../../../lib/accounts";
|
import * as Accounts from "../../../lib/accounts"
|
||||||
import * as auth from "../../../lib/auth";
|
import * as auth from "../../../lib/auth"
|
||||||
import { sendMail } from "../../../lib/mail";
|
import { sendMail } from "../../../lib/mail"
|
||||||
import { getAccount, noAPIAccess, requiresAccount, requiresPermissions } from "../../../lib/middleware"
|
import {
|
||||||
|
getAccount,
|
||||||
|
noAPIAccess,
|
||||||
|
requiresAccount,
|
||||||
|
requiresPermissions,
|
||||||
|
} from "../../../lib/middleware"
|
||||||
import { accountRatelimit } from "../../../lib/ratelimit"
|
import { accountRatelimit } from "../../../lib/ratelimit"
|
||||||
|
|
||||||
import ServeError from "../../../lib/errors";
|
import ServeError from "../../../lib/errors"
|
||||||
import Files, { FileVisibility, generateFileId, id_check_regex } from "../../../lib/files";
|
import Files, {
|
||||||
|
FileVisibility,
|
||||||
|
generateFileId,
|
||||||
|
id_check_regex,
|
||||||
|
} from "../../../lib/files"
|
||||||
|
|
||||||
import { writeFile } from "fs";
|
import { writeFile } from "fs/promises"
|
||||||
|
|
||||||
let parser = bodyParser.json({
|
export let authRoutes = new Hono<{
|
||||||
type: ["text/plain","application/json"]
|
Variables: {
|
||||||
})
|
account: Accounts.Account
|
||||||
|
}
|
||||||
export let authRoutes = Router();
|
}>()
|
||||||
authRoutes.use(getAccount)
|
|
||||||
|
|
||||||
let config = require(`${process.cwd()}/config.json`)
|
let config = require(`${process.cwd()}/config.json`)
|
||||||
|
authRoutes.all("*", getAccount)
|
||||||
|
|
||||||
module.exports = function(files: Files) {
|
module.exports = function (files: Files) {
|
||||||
|
authRoutes.post("/login", async (ctx) => {
|
||||||
authRoutes.post("/login", parser, (req,res) => {
|
console.log(ctx)
|
||||||
if (typeof req.body.username != "string" || typeof req.body.password != "string") {
|
const body = await ctx.req.json()
|
||||||
ServeError(res,400,"please provide a username or password")
|
if (
|
||||||
return
|
typeof body.username != "string" ||
|
||||||
|
typeof body.password != "string"
|
||||||
|
) {
|
||||||
|
return ServeError(ctx, 400, "please provide a username or password")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (auth.validate(req.cookies.auth)) return
|
if (auth.validate(getCookie(ctx, "auth")!))
|
||||||
|
return ctx.text("You are already authed")
|
||||||
|
|
||||||
/*
|
/*
|
||||||
check if account exists
|
check if account exists
|
||||||
*/
|
*/
|
||||||
|
|
||||||
let acc = Accounts.getFromUsername(req.body.username)
|
let acc = Accounts.getFromUsername(body.username)
|
||||||
|
|
||||||
if (!acc) {
|
if (!acc) {
|
||||||
ServeError(res,401,"username or password incorrect")
|
return ServeError(ctx, 401, "username or password incorrect")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Accounts.password.check(acc.id,req.body.password)) {
|
if (!Accounts.password.check(acc.id, body.password)) {
|
||||||
ServeError(res,401,"username or password incorrect")
|
return ServeError(ctx, 401, "username or password incorrect")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
assign token
|
assign token
|
||||||
*/
|
*/
|
||||||
|
|
||||||
res.cookie("auth",auth.create(acc.id,(3*24*60*60*1000)))
|
setCookie(ctx, "auth", auth.create(acc.id, 3 * 24 * 60 * 60 * 1000))
|
||||||
res.status(200)
|
return ctx.text("")
|
||||||
res.end()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
authRoutes.post("/create", parser, (req,res) => {
|
authRoutes.post("/create", async (ctx) => {
|
||||||
if (!config.accounts.registrationEnabled) {
|
if (!config.accounts.registrationEnabled) {
|
||||||
ServeError(res,403,"account registration disabled")
|
return ServeError(ctx, 403, "account registration disabled")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (auth.validate(req.cookies.auth)) return
|
if (auth.validate(getCookie(ctx, "auth")!)) return
|
||||||
|
const body = await ctx.req.json()
|
||||||
if (typeof req.body.username != "string" || typeof req.body.password != "string") {
|
if (
|
||||||
ServeError(res,400,"please provide a username or password")
|
typeof body.username != "string" ||
|
||||||
return
|
typeof body.password != "string"
|
||||||
|
) {
|
||||||
|
return ServeError(ctx, 400, "please provide a username or password")
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
check if account exists
|
check if account exists
|
||||||
*/
|
*/
|
||||||
|
|
||||||
let acc = Accounts.getFromUsername(req.body.username)
|
let acc = Accounts.getFromUsername(body.username)
|
||||||
|
|
||||||
if (acc) {
|
if (acc) {
|
||||||
ServeError(res,400,"account with this username already exists")
|
ServeError(ctx, 400, "account with this username already exists")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.body.username.length < 3 || req.body.username.length > 20) {
|
if (body.username.length < 3 || body.username.length > 20) {
|
||||||
ServeError(res,400,"username must be over or equal to 3 characters or under or equal to 20 characters in length")
|
return ServeError(
|
||||||
|
ctx,
|
||||||
|
400,
|
||||||
|
"username must be over or equal to 3 characters or under or equal to 20 characters in length"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(body.username.match(/[A-Za-z0-9_\-\.]+/) || [])[0] != body.username
|
||||||
|
) {
|
||||||
|
return ServeError(ctx, 400, "username contains invalid characters")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body.password.length < 8) {
|
||||||
|
ServeError(ctx, 400, "password must be 8 characters or longer")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((req.body.username.match(/[A-Za-z0-9_\-\.]+/) || [])[0] != req.body.username) {
|
return Accounts.create(body.username, body.password)
|
||||||
ServeError(res,400,"username contains invalid characters")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (req.body.password.length < 8) {
|
|
||||||
ServeError(res,400,"password must be 8 characters or longer")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Accounts.create(req.body.username,req.body.password)
|
|
||||||
.then((newAcc) => {
|
.then((newAcc) => {
|
||||||
/*
|
/*
|
||||||
assign token
|
assign token
|
||||||
*/
|
*/
|
||||||
|
|
||||||
res.cookie("auth",auth.create(newAcc,(3*24*60*60*1000)))
|
setCookie(
|
||||||
res.status(200)
|
ctx,
|
||||||
res.end()
|
"auth",
|
||||||
})
|
auth.create(newAcc, 3 * 24 * 60 * 60 * 1000)
|
||||||
.catch(() => {
|
)
|
||||||
ServeError(res,500,"internal server error")
|
return ctx.text("")
|
||||||
})
|
})
|
||||||
|
.catch(() => ServeError(ctx, 500, "internal server error"))
|
||||||
})
|
})
|
||||||
|
|
||||||
authRoutes.post("/logout", (req,res) => {
|
authRoutes.post("/logout", async (ctx) => {
|
||||||
if (!auth.validate(req.cookies.auth)) {
|
if (!auth.validate(getCookie(ctx, "auth")!)) {
|
||||||
ServeError(res, 401, "not logged in")
|
return ServeError(ctx, 401, "not logged in")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auth.invalidate(req.cookies.auth)
|
auth.invalidate(getCookie(ctx, "auth")!)
|
||||||
res.send("logged out")
|
return ctx.text("logged out")
|
||||||
})
|
})
|
||||||
|
|
||||||
authRoutes.post("/dfv", requiresAccount, requiresPermissions("manage"), parser, (req,res) => {
|
authRoutes.post(
|
||||||
let acc = res.locals.acc as Accounts.Account
|
"/dfv",
|
||||||
|
requiresAccount,
|
||||||
|
requiresPermissions("manage"),
|
||||||
|
// Used body-parser
|
||||||
|
async (ctx) => {
|
||||||
|
const body = await ctx.req.json()
|
||||||
|
let acc = ctx.get("account") as Accounts.Account
|
||||||
|
|
||||||
if (['public','private','anonymous'].includes(req.body.defaultFileVisibility)) {
|
if (
|
||||||
acc.defaultFileVisibility = req.body.defaultFileVisibility
|
["public", "private", "anonymous"].includes(
|
||||||
Accounts.save()
|
body.defaultFileVisibility
|
||||||
res.send(`dfv has been set to ${acc.defaultFileVisibility}`)
|
)
|
||||||
} else {
|
) {
|
||||||
res.status(400)
|
acc.defaultFileVisibility = body.defaultFileVisibility
|
||||||
res.send("invalid dfv")
|
Accounts.save()
|
||||||
|
return ctx.text(
|
||||||
|
`dfv has been set to ${acc.defaultFileVisibility}`
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return ctx.text("invalid dfv", 400)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
|
|
||||||
authRoutes.post("/customcss", requiresAccount, requiresPermissions("customize"), parser, (req,res) => {
|
authRoutes.post(
|
||||||
let acc = res.locals.acc as Accounts.Account
|
"/customcss",
|
||||||
|
requiresAccount,
|
||||||
if (typeof req.body.fileId != "string") req.body.fileId = undefined;
|
requiresPermissions("customize"),
|
||||||
|
// Used body-parser
|
||||||
if (
|
async (ctx) => {
|
||||||
|
const body = await ctx.req.json()
|
||||||
|
let acc = ctx.get("account") as Accounts.Account
|
||||||
|
|
||||||
!req.body.fileId
|
if (typeof body.fileId != "string") body.fileId = undefined
|
||||||
|| (req.body.fileId.match(id_check_regex) == req.body.fileId
|
|
||||||
&& req.body.fileId.length <= config.maxUploadIdLength)
|
if (
|
||||||
|
!body.fileId ||
|
||||||
) {
|
(body.fileId.match(id_check_regex) == body.fileId &&
|
||||||
acc.customCSS = req.body.fileId || undefined
|
body.fileId.length <= config.maxUploadIdLength)
|
||||||
if (!req.body.fileId) delete acc.customCSS
|
) {
|
||||||
Accounts.save()
|
acc.customCSS = body.fileId || undefined
|
||||||
res.send(`custom css saved`)
|
if (!body.fileId) delete acc.customCSS
|
||||||
} else {
|
Accounts.save()
|
||||||
res.status(400)
|
return ctx.text(`custom css saved`)
|
||||||
res.send("invalid fileid")
|
} else {
|
||||||
|
return ctx.text("invalid fileid", 400)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
|
|
||||||
authRoutes.post("/embedcolor", requiresAccount, requiresPermissions("customize"), parser, (req,res) => {
|
authRoutes.post(
|
||||||
let acc = res.locals.acc as Accounts.Account
|
"/embedcolor",
|
||||||
|
requiresAccount,
|
||||||
if (typeof req.body.color != "string") req.body.color = undefined;
|
requiresPermissions("customize"),
|
||||||
|
// Used body-parser
|
||||||
if (
|
async (ctx) => {
|
||||||
|
let acc = ctx.get("account") as Accounts.Account
|
||||||
|
const body = await ctx.req.json()
|
||||||
|
if (typeof body.color != "string") body.color = undefined
|
||||||
|
|
||||||
|
if (
|
||||||
|
!body.color ||
|
||||||
|
(body.color.toLowerCase().match(/[a-f0-9]+/) ==
|
||||||
|
body.color.toLowerCase() &&
|
||||||
|
body.color.length == 6)
|
||||||
|
) {
|
||||||
|
if (!acc.embed) acc.embed = {}
|
||||||
|
acc.embed.color = body.color || undefined
|
||||||
|
if (!body.color) delete acc.embed.color
|
||||||
|
Accounts.save()
|
||||||
|
return ctx.text(`custom embed color saved`)
|
||||||
|
} else {
|
||||||
|
return ctx.text("invalid hex code", 400)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
authRoutes.post(
|
||||||
|
"/embedsize",
|
||||||
|
requiresAccount,
|
||||||
|
requiresPermissions("customize"),
|
||||||
|
// Used body-parser
|
||||||
|
async (ctx) => {
|
||||||
|
let acc = ctx.get("account") as Accounts.Account
|
||||||
|
const body = await ctx.req.json()
|
||||||
|
if (typeof body.largeImage != "boolean") body.color = false
|
||||||
|
|
||||||
!req.body.color
|
|
||||||
|| (req.body.color.toLowerCase().match(/[a-f0-9]+/) == req.body.color.toLowerCase())
|
|
||||||
&& req.body.color.length == 6
|
|
||||||
|
|
||||||
) {
|
|
||||||
if (!acc.embed) acc.embed = {}
|
if (!acc.embed) acc.embed = {}
|
||||||
acc.embed.color = req.body.color || undefined
|
acc.embed.largeImage = body.largeImage
|
||||||
if (!req.body.color) delete acc.embed.color
|
if (!body.largeImage) delete acc.embed.largeImage
|
||||||
Accounts.save()
|
Accounts.save()
|
||||||
res.send(`custom embed color saved`)
|
return ctx.text(`custom embed image size saved`)
|
||||||
} else {
|
|
||||||
res.status(400)
|
|
||||||
res.send("invalid hex code")
|
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
|
|
||||||
authRoutes.post("/embedsize", requiresAccount, requiresPermissions("customize"), parser, (req,res) => {
|
authRoutes.post(
|
||||||
let acc = res.locals.acc as Accounts.Account
|
"/delete_account",
|
||||||
|
requiresAccount,
|
||||||
if (typeof req.body.largeImage != "boolean") req.body.color = false;
|
noAPIAccess,
|
||||||
|
// Used body-parser
|
||||||
|
async (ctx) => {
|
||||||
|
let acc = ctx.get("account") as Accounts.Account
|
||||||
|
const body = await ctx.req.json()
|
||||||
|
let accId = acc.id
|
||||||
|
|
||||||
if (!acc.embed) acc.embed = {}
|
auth.AuthTokens.filter((e) => e.account == accId).forEach((v) => {
|
||||||
acc.embed.largeImage = req.body.largeImage
|
auth.invalidate(v.token)
|
||||||
if (!req.body.largeImage) delete acc.embed.largeImage
|
})
|
||||||
Accounts.save()
|
|
||||||
res.send(`custom embed image size saved`)
|
|
||||||
})
|
|
||||||
|
|
||||||
authRoutes.post("/delete_account", requiresAccount, noAPIAccess, parser, async (req,res) => {
|
let cpl = () =>
|
||||||
let acc = res.locals.acc as Accounts.Account
|
Accounts.deleteAccount(accId).then((_) =>
|
||||||
|
ctx.text("account deleted")
|
||||||
let accId = acc.id
|
)
|
||||||
|
|
||||||
auth.AuthTokens.filter(e => e.account == accId).forEach((v) => {
|
if (body.deleteFiles) {
|
||||||
auth.invalidate(v.token)
|
let f = acc.files.map((e) => e) // make shallow copy so that iterating over it doesnt Die
|
||||||
})
|
for (let v of f) {
|
||||||
|
files.unlink(v, true).catch((err) => console.error(err))
|
||||||
|
}
|
||||||
|
|
||||||
let cpl = () => Accounts.deleteAccount(accId).then(_ => res.send("account deleted"))
|
return writeFile(
|
||||||
|
process.cwd() + "/.data/files.json",
|
||||||
if (req.body.deleteFiles) {
|
JSON.stringify(files.files)
|
||||||
let f = acc.files.map(e=>e) // make shallow copy so that iterating over it doesnt Die
|
).then(cpl)
|
||||||
for (let v of f) {
|
} else cpl()
|
||||||
files.unlink(v,true).catch(err => console.error(err))
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
authRoutes.post(
|
||||||
|
"/change_username",
|
||||||
|
requiresAccount,
|
||||||
|
noAPIAccess,
|
||||||
|
// Used body-parser
|
||||||
|
async (ctx) => {
|
||||||
|
let acc = ctx.get("account") as Accounts.Account
|
||||||
|
const body = await ctx.req.json()
|
||||||
|
if (
|
||||||
|
typeof body.username != "string" ||
|
||||||
|
body.username.length < 3 ||
|
||||||
|
body.username.length > 20
|
||||||
|
) {
|
||||||
|
return ServeError(
|
||||||
|
ctx,
|
||||||
|
400,
|
||||||
|
"username must be between 3 and 20 characters in length"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
writeFile(process.cwd()+"/.data/files.json",JSON.stringify(files.files), (err) => {
|
let _acc = Accounts.getFromUsername(body.username)
|
||||||
if (err) console.log(err)
|
|
||||||
cpl()
|
|
||||||
})
|
|
||||||
} else cpl()
|
|
||||||
})
|
|
||||||
|
|
||||||
authRoutes.post("/change_username", requiresAccount, noAPIAccess, parser, (req,res) => {
|
if (_acc) {
|
||||||
let acc = res.locals.acc as Accounts.Account
|
return ServeError(
|
||||||
|
ctx,
|
||||||
|
400,
|
||||||
|
"account with this username already exists"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof req.body.username != "string" || req.body.username.length < 3 || req.body.username.length > 20) {
|
if (
|
||||||
ServeError(res,400,"username must be between 3 and 20 characters in length")
|
(body.username.match(/[A-Za-z0-9_\-\.]+/) || [])[0] !=
|
||||||
return
|
body.username
|
||||||
|
) {
|
||||||
|
return ServeError(
|
||||||
|
ctx,
|
||||||
|
400,
|
||||||
|
"username contains invalid characters"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
acc.username = body.username
|
||||||
|
Accounts.save()
|
||||||
|
|
||||||
|
if (acc.email) {
|
||||||
|
return sendMail(
|
||||||
|
acc.email,
|
||||||
|
`Your login details have been updated`,
|
||||||
|
`<b>Hello there!</b> Your username has been updated to <span username>${body.username}</span>. Please update your devices accordingly. Thank you for using monofile.`
|
||||||
|
)
|
||||||
|
.then(() => ctx.text("OK"))
|
||||||
|
.catch((err) => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.text("username changed")
|
||||||
}
|
}
|
||||||
|
)
|
||||||
let _acc = Accounts.getFromUsername(req.body.username)
|
|
||||||
|
|
||||||
if (_acc) {
|
|
||||||
ServeError(res,400,"account with this username already exists")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((req.body.username.match(/[A-Za-z0-9_\-\.]+/) || [])[0] != req.body.username) {
|
|
||||||
ServeError(res,400,"username contains invalid characters")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
acc.username = req.body.username
|
|
||||||
Accounts.save()
|
|
||||||
|
|
||||||
if (acc.email) {
|
|
||||||
sendMail(acc.email, `Your login details have been updated`, `<b>Hello there!</b> Your username has been updated to <span username>${req.body.username}</span>. Please update your devices accordingly. Thank you for using monofile.`).then(() => {
|
|
||||||
res.send("OK")
|
|
||||||
}).catch((err) => {})
|
|
||||||
}
|
|
||||||
|
|
||||||
res.send("username changed")
|
|
||||||
})
|
|
||||||
|
|
||||||
// shit way to do this but...
|
// shit way to do this but...
|
||||||
|
|
||||||
let verificationCodes = new Map<string, {code: string, email: string, expiry: NodeJS.Timeout}>()
|
let verificationCodes = new Map<
|
||||||
|
string,
|
||||||
|
{ code: string; email: string; expiry: NodeJS.Timeout }
|
||||||
|
>()
|
||||||
|
|
||||||
authRoutes.post("/request_email_change", requiresAccount, noAPIAccess, accountRatelimit({ requests: 4, per: 60*60*1000 }), parser, (req,res) => {
|
authRoutes.post(
|
||||||
let acc = res.locals.acc as Accounts.Account
|
"/request_email_change",
|
||||||
|
requiresAccount,
|
||||||
|
noAPIAccess,
|
||||||
if (typeof req.body.email != "string" || !req.body.email) {
|
accountRatelimit({ requests: 4, per: 60 * 60 * 1000 }),
|
||||||
ServeError(res,400, "supply an email")
|
// Used body-parser
|
||||||
return
|
async (ctx) => {
|
||||||
}
|
let acc = ctx.get("account") as Accounts.Account
|
||||||
|
const body = await ctx.req.json()
|
||||||
|
if (typeof body.email != "string" || !body.email) {
|
||||||
|
ServeError(ctx, 400, "supply an email")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let vcode = verificationCodes.get(acc.id)
|
let vcode = verificationCodes.get(acc.id)
|
||||||
|
|
||||||
// delete previous if any
|
// delete previous if any
|
||||||
let e = vcode?.expiry
|
let e = vcode?.expiry
|
||||||
if (e) clearTimeout(e)
|
|
||||||
verificationCodes.delete(acc?.id||"")
|
|
||||||
|
|
||||||
let code = generateFileId(12).toUpperCase()
|
|
||||||
|
|
||||||
// set
|
|
||||||
|
|
||||||
verificationCodes.set(acc.id, {
|
|
||||||
code,
|
|
||||||
email: req.body.email,
|
|
||||||
expiry: setTimeout( () => verificationCodes.delete(acc?.id||""), 15*60*1000)
|
|
||||||
})
|
|
||||||
|
|
||||||
// this is a mess but it's fine
|
|
||||||
|
|
||||||
sendMail(req.body.email, `Hey there, ${acc.username} - let's connect your email`, `<b>Hello there!</b> You are recieving this message because you decided to link your email, <span code>${req.body.email.split("@")[0]}<span style="opacity:0.5">@${req.body.email.split("@")[1]}</span></span>, to your account, <span username>${acc.username}</span>. If you would like to continue, please <a href="https://${req.header("Host")}/auth/confirm_email/${code}"><span code>click here</span></a>, or go to https://${req.header("Host")}/auth/confirm_email/${code}.`).then(() => {
|
|
||||||
res.send("OK")
|
|
||||||
}).catch((err) => {
|
|
||||||
let e = verificationCodes.get(acc?.id||"")?.expiry
|
|
||||||
if (e) clearTimeout(e)
|
if (e) clearTimeout(e)
|
||||||
verificationCodes.delete(acc?.id||"")
|
verificationCodes.delete(acc?.id || "")
|
||||||
res.locals.undoCount();
|
|
||||||
ServeError(res, 500, err?.toString())
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
authRoutes.get("/confirm_email/:code", requiresAccount, noAPIAccess, (req,res) => {
|
let code = generateFileId(12).toUpperCase()
|
||||||
let acc = res.locals.acc as Accounts.Account
|
|
||||||
|
|
||||||
|
|
||||||
let vcode = verificationCodes.get(acc.id)
|
// set
|
||||||
|
|
||||||
if (!vcode) { ServeError(res, 400, "nothing to confirm"); return }
|
verificationCodes.set(acc.id, {
|
||||||
|
code,
|
||||||
|
email: body.email,
|
||||||
|
expiry: setTimeout(
|
||||||
|
() => verificationCodes.delete(acc?.id || ""),
|
||||||
|
15 * 60 * 1000
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
if (typeof req.params.code == "string" && req.params.code.toUpperCase() == vcode.code) {
|
// this is a mess but it's fine
|
||||||
acc.email = vcode.email
|
|
||||||
Accounts.save();
|
|
||||||
|
|
||||||
let e = verificationCodes.get(acc?.id||"")?.expiry
|
sendMail(
|
||||||
if (e) clearTimeout(e)
|
body.email,
|
||||||
verificationCodes.delete(acc?.id||"")
|
`Hey there, ${acc.username} - let's connect your email`,
|
||||||
|
`<b>Hello there!</b> You are recieving this message because you decided to link your email, <span code>${
|
||||||
res.redirect("/")
|
body.email.split("@")[0]
|
||||||
} else {
|
}<span style="opacity:0.5">@${
|
||||||
ServeError(res, 400, "invalid code")
|
body.email.split("@")[1]
|
||||||
|
}</span></span>, to your account, <span username>${
|
||||||
|
acc.username
|
||||||
|
}</span>. If you would like to continue, please <a href="https://${ctx.req.header(
|
||||||
|
"Host"
|
||||||
|
)}/auth/confirm_email/${code}"><span code>click here</span></a>, or go to https://${ctx.req.header(
|
||||||
|
"Host"
|
||||||
|
)}/auth/confirm_email/${code}.`
|
||||||
|
)
|
||||||
|
.then(() => ctx.text("OK"))
|
||||||
|
.catch((err) => {
|
||||||
|
let e = verificationCodes.get(acc?.id || "")?.expiry
|
||||||
|
if (e) clearTimeout(e)
|
||||||
|
verificationCodes.delete(acc?.id || "")
|
||||||
|
;(ctx.get("undoCount" as never) as () => {})()
|
||||||
|
return ServeError(ctx, 500, err?.toString())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
|
|
||||||
authRoutes.post("/remove_email", requiresAccount, noAPIAccess, (req,res) => {
|
authRoutes.get(
|
||||||
let acc = res.locals.acc as Accounts.Account
|
"/confirm_email/:code",
|
||||||
|
requiresAccount,
|
||||||
if (acc.email) {
|
noAPIAccess,
|
||||||
delete acc.email;
|
async (ctx) => {
|
||||||
Accounts.save()
|
let acc = ctx.get("account") as Accounts.Account
|
||||||
res.send("email detached")
|
|
||||||
}
|
|
||||||
else ServeError(res, 400, "email not attached")
|
|
||||||
})
|
|
||||||
|
|
||||||
let pwReset = new Map<string, {code: string, expiry: NodeJS.Timeout, requestedAt:number}>()
|
let vcode = verificationCodes.get(acc.id)
|
||||||
|
|
||||||
|
if (!vcode) {
|
||||||
|
ServeError(ctx, 400, "nothing to confirm")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof ctx.req.param("code") == "string" &&
|
||||||
|
ctx.req.param("code").toUpperCase() == vcode.code
|
||||||
|
) {
|
||||||
|
acc.email = vcode.email
|
||||||
|
Accounts.save()
|
||||||
|
|
||||||
|
let e = verificationCodes.get(acc?.id || "")?.expiry
|
||||||
|
if (e) clearTimeout(e)
|
||||||
|
verificationCodes.delete(acc?.id || "")
|
||||||
|
|
||||||
|
return ctx.redirect("/")
|
||||||
|
} else {
|
||||||
|
return ServeError(ctx, 400, "invalid code")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
authRoutes.post(
|
||||||
|
"/remove_email",
|
||||||
|
requiresAccount,
|
||||||
|
noAPIAccess,
|
||||||
|
async (ctx) => {
|
||||||
|
let acc = ctx.get("account") as Accounts.Account
|
||||||
|
|
||||||
|
if (acc.email) {
|
||||||
|
delete acc.email
|
||||||
|
Accounts.save()
|
||||||
|
return ctx.text("email detached")
|
||||||
|
} else return ServeError(ctx, 400, "email not attached")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
let pwReset = new Map<
|
||||||
|
string,
|
||||||
|
{ code: string; expiry: NodeJS.Timeout; requestedAt: number }
|
||||||
|
>()
|
||||||
let prcIdx = new Map<string, string>()
|
let prcIdx = new Map<string, string>()
|
||||||
|
|
||||||
authRoutes.post("/request_emergency_login", parser, (req,res) => {
|
authRoutes.post("/request_emergency_login", async (ctx) => {
|
||||||
if (auth.validate(req.cookies.auth || "")) return
|
if (auth.validate(getCookie(ctx, "auth") || "")) return
|
||||||
|
const body = await ctx.req.json()
|
||||||
if (typeof req.body.account != "string" || !req.body.account) {
|
if (typeof body.account != "string" || !body.account) {
|
||||||
ServeError(res,400, "supply a username")
|
ServeError(ctx, 400, "supply a username")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let acc = Accounts.getFromUsername(req.body.account)
|
let acc = Accounts.getFromUsername(body.account)
|
||||||
if (!acc || !acc.email) {
|
if (!acc || !acc.email) {
|
||||||
ServeError(res, 400, "this account either does not exist or does not have an email attached; please contact the server's admin for a reset if you would still like to access it")
|
return ServeError(
|
||||||
return
|
ctx,
|
||||||
|
400,
|
||||||
|
"this account either does not exist or does not have an email attached; please contact the server's admin for a reset if you would still like to access it"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
let pResetCode = pwReset.get(acc.id)
|
let pResetCode = pwReset.get(acc.id)
|
||||||
|
|
||||||
if (pResetCode && pResetCode.requestedAt+(15*60*1000) > Date.now()) {
|
if (
|
||||||
ServeError(res, 429, `Please wait a few moments to request another emergency login.`)
|
pResetCode &&
|
||||||
return
|
pResetCode.requestedAt + 15 * 60 * 1000 > Date.now()
|
||||||
|
) {
|
||||||
|
return ServeError(
|
||||||
|
ctx,
|
||||||
|
429,
|
||||||
|
`Please wait a few moments to request another emergency login.`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// delete previous if any
|
// delete previous if any
|
||||||
let e = pResetCode?.expiry
|
let e = pResetCode?.expiry
|
||||||
if (e) clearTimeout(e)
|
if (e) clearTimeout(e)
|
||||||
pwReset.delete(acc?.id||"")
|
pwReset.delete(acc?.id || "")
|
||||||
prcIdx.delete(pResetCode?.code||"")
|
prcIdx.delete(pResetCode?.code || "")
|
||||||
|
|
||||||
let code = generateFileId(12).toUpperCase()
|
let code = generateFileId(12).toUpperCase()
|
||||||
|
|
||||||
|
@ -358,107 +485,146 @@ module.exports = function(files: Files) {
|
||||||
|
|
||||||
pwReset.set(acc.id, {
|
pwReset.set(acc.id, {
|
||||||
code,
|
code,
|
||||||
expiry: setTimeout( () => { pwReset.delete(acc?.id||""); prcIdx.delete(pResetCode?.code||"") }, 15*60*1000),
|
expiry: setTimeout(() => {
|
||||||
requestedAt: Date.now()
|
pwReset.delete(acc?.id || "")
|
||||||
|
prcIdx.delete(pResetCode?.code || "")
|
||||||
|
}, 15 * 60 * 1000),
|
||||||
|
requestedAt: Date.now(),
|
||||||
})
|
})
|
||||||
|
|
||||||
prcIdx.set(code, acc.id)
|
prcIdx.set(code, acc.id)
|
||||||
|
|
||||||
// this is a mess but it's fine
|
// this is a mess but it's fine
|
||||||
|
|
||||||
sendMail(acc.email, `Emergency login requested for ${acc.username}`, `<b>Hello there!</b> You are recieving this message because you forgot your password to your monofile account, <span username>${acc.username}</span>. To log in, please <a href="https://${req.header("Host")}/auth/emergency_login/${code}"><span code>click here</span></a>, or go to https://${req.header("Host")}/auth/emergency_login/${code}. If it doesn't appear that you are logged in after visiting this link, please try refreshing. Once you have successfully logged in, you may reset your password.`).then(() => {
|
return sendMail(
|
||||||
res.send("OK")
|
acc.email,
|
||||||
}).catch((err) => {
|
`Emergency login requested for ${acc.username}`,
|
||||||
let e = pwReset.get(acc?.id||"")?.expiry
|
`<b>Hello there!</b> You are recieving this message because you forgot your password to your monofile account, <span username>${
|
||||||
if (e) clearTimeout(e)
|
acc.username
|
||||||
pwReset.delete(acc?.id||"")
|
}</span>. To log in, please <a href="https://${ctx.req.header(
|
||||||
prcIdx.delete(code||"")
|
"Host"
|
||||||
ServeError(res, 500, err?.toString())
|
)}/auth/emergency_login/${code}"><span code>click here</span></a>, or go to https://${ctx.req.header(
|
||||||
})
|
"Host"
|
||||||
|
)}/auth/emergency_login/${code}. If it doesn't appear that you are logged in after visiting this link, please try refreshing. Once you have successfully logged in, you may reset your password.`
|
||||||
|
)
|
||||||
|
.then(() => ctx.text("OK"))
|
||||||
|
.catch((err) => {
|
||||||
|
let e = pwReset.get(acc?.id || "")?.expiry
|
||||||
|
if (e) clearTimeout(e)
|
||||||
|
pwReset.delete(acc?.id || "")
|
||||||
|
prcIdx.delete(code || "")
|
||||||
|
return ServeError(ctx, 500, err?.toString())
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
authRoutes.get("/emergency_login/:code", (req,res) => {
|
authRoutes.get("/emergency_login/:code", async (ctx) => {
|
||||||
if (auth.validate(req.cookies.auth || "")) {
|
if (auth.validate(getCookie(ctx, "auth") || "")) {
|
||||||
ServeError(res, 403, "already logged in")
|
return ServeError(ctx, 403, "already logged in")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let vcode = prcIdx.get(req.params.code)
|
let vcode = prcIdx.get(ctx.req.param("code"))
|
||||||
|
|
||||||
if (!vcode) { ServeError(res, 400, "invalid emergency login code"); return }
|
if (!vcode) {
|
||||||
|
return ServeError(ctx, 400, "invalid emergency login code")
|
||||||
if (typeof req.params.code == "string" && vcode) {
|
}
|
||||||
res.cookie("auth",auth.create(vcode,(3*24*60*60*1000)))
|
|
||||||
res.redirect("/")
|
|
||||||
|
|
||||||
|
if (typeof ctx.req.param("code") == "string" && vcode) {
|
||||||
|
setCookie(ctx, "auth", auth.create(vcode, 3 * 24 * 60 * 60 * 1000))
|
||||||
let e = pwReset.get(vcode)?.expiry
|
let e = pwReset.get(vcode)?.expiry
|
||||||
if (e) clearTimeout(e)
|
if (e) clearTimeout(e)
|
||||||
pwReset.delete(vcode)
|
pwReset.delete(vcode)
|
||||||
prcIdx.delete(req.params.code)
|
prcIdx.delete(ctx.req.param("code"))
|
||||||
|
return ctx.redirect("/")
|
||||||
} else {
|
} else {
|
||||||
ServeError(res, 400, "invalid code")
|
ServeError(ctx, 400, "invalid code")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
authRoutes.post("/change_password", requiresAccount, noAPIAccess, parser, (req,res) => {
|
authRoutes.post(
|
||||||
let acc = res.locals.acc as Accounts.Account
|
"/change_password",
|
||||||
|
requiresAccount,
|
||||||
if (typeof req.body.password != "string" || req.body.password.length < 8) {
|
noAPIAccess,
|
||||||
ServeError(res,400,"password must be 8 characters or longer")
|
// Used body-parser
|
||||||
return
|
async (ctx) => {
|
||||||
|
let acc = ctx.get("account") as Accounts.Account
|
||||||
|
const body = await ctx.req.json()
|
||||||
|
if (typeof body.password != "string" || body.password.length < 8) {
|
||||||
|
ServeError(ctx, 400, "password must be 8 characters or longer")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let accId = acc.id
|
||||||
|
|
||||||
|
Accounts.password.set(accId, body.password)
|
||||||
|
|
||||||
|
auth.AuthTokens.filter((e) => e.account == accId).forEach((v) => {
|
||||||
|
auth.invalidate(v.token)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (acc.email) {
|
||||||
|
return sendMail(
|
||||||
|
acc.email,
|
||||||
|
`Your login details have been updated`,
|
||||||
|
`<b>Hello there!</b> This email is to notify you of a password change that you have initiated. You have been logged out of your devices. Thank you for using monofile.`
|
||||||
|
)
|
||||||
|
.then(() => ctx.text("OK"))
|
||||||
|
.catch((err) => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.text("password changed - logged out all sessions")
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
|
||||||
let accId = acc.id
|
authRoutes.post(
|
||||||
|
"/logout_sessions",
|
||||||
|
requiresAccount,
|
||||||
|
noAPIAccess,
|
||||||
|
async (ctx) => {
|
||||||
|
let acc = ctx.get("account") as Accounts.Account
|
||||||
|
|
||||||
Accounts.password.set(accId,req.body.password)
|
let accId = acc.id
|
||||||
|
|
||||||
auth.AuthTokens.filter(e => e.account == accId).forEach((v) => {
|
auth.AuthTokens.filter((e) => e.account == accId).forEach((v) => {
|
||||||
auth.invalidate(v.token)
|
auth.invalidate(v.token)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (acc.email) {
|
return ctx.text("logged out all sessions")
|
||||||
sendMail(acc.email, `Your login details have been updated`, `<b>Hello there!</b> This email is to notify you of a password change that you have initiated. You have been logged out of your devices. Thank you for using monofile.`).then(() => {
|
|
||||||
res.send("OK")
|
|
||||||
}).catch((err) => {})
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
|
||||||
res.send("password changed - logged out all sessions")
|
authRoutes.get(
|
||||||
|
"/me",
|
||||||
|
requiresAccount,
|
||||||
|
requiresPermissions("user"),
|
||||||
|
async (ctx) => {
|
||||||
|
let acc = ctx.get("account") as Accounts.Account
|
||||||
|
let sessionToken = auth.tokenFor(ctx)!
|
||||||
|
let accId = acc.id
|
||||||
|
return ctx.json({
|
||||||
|
...acc,
|
||||||
|
sessionCount: auth.AuthTokens.filter(
|
||||||
|
(e) =>
|
||||||
|
e.type != "App" &&
|
||||||
|
e.account == accId &&
|
||||||
|
(e.expire > Date.now() || !e.expire)
|
||||||
|
).length,
|
||||||
|
sessionExpires: auth.AuthTokens.find(
|
||||||
|
(e) => e.token == sessionToken
|
||||||
|
)?.expire,
|
||||||
|
password: undefined,
|
||||||
|
email:
|
||||||
|
auth.getType(sessionToken) == "User" ||
|
||||||
|
auth.getPermissions(sessionToken)?.includes("email")
|
||||||
|
? acc.email
|
||||||
|
: undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
authRoutes.get("/customCSS", async (ctx) => {
|
||||||
|
let acc = ctx.get("account")
|
||||||
|
if (acc?.customCSS) return ctx.redirect(`/file/${acc.customCSS}`)
|
||||||
|
else return ctx.text("")
|
||||||
})
|
})
|
||||||
|
|
||||||
authRoutes.post("/logout_sessions", requiresAccount, noAPIAccess, (req,res) => {
|
|
||||||
let acc = res.locals.acc as Accounts.Account
|
|
||||||
|
|
||||||
let accId = acc.id
|
|
||||||
|
|
||||||
auth.AuthTokens.filter(e => e.account == accId).forEach((v) => {
|
|
||||||
auth.invalidate(v.token)
|
|
||||||
})
|
|
||||||
|
|
||||||
res.send("logged out all sessions")
|
|
||||||
})
|
|
||||||
|
|
||||||
authRoutes.get("/me", requiresAccount, requiresPermissions("user"), (req,res) => {
|
|
||||||
let acc = res.locals.acc as Accounts.Account
|
|
||||||
|
|
||||||
let sessionToken = auth.tokenFor(req)
|
|
||||||
let accId = acc.id
|
|
||||||
res.send({
|
|
||||||
...acc,
|
|
||||||
sessionCount: auth.AuthTokens.filter(e => e.type != "App" && e.account == accId && (e.expire > Date.now() || !e.expire)).length,
|
|
||||||
sessionExpires: auth.AuthTokens.find(e => e.token == sessionToken)?.expire,
|
|
||||||
password: undefined,
|
|
||||||
email:
|
|
||||||
auth.getType(sessionToken) == "User" || auth.getPermissions(sessionToken)?.includes("email")
|
|
||||||
? acc.email
|
|
||||||
: undefined
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
authRoutes.get("/customCSS", (req,res) => {
|
|
||||||
let acc = res.locals.acc
|
|
||||||
if (acc?.customCSS) res.redirect(`/file/${acc.customCSS}`)
|
|
||||||
else res.send("")
|
|
||||||
})
|
|
||||||
|
|
||||||
return authRoutes
|
return authRoutes
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,98 +1,129 @@
|
||||||
import bodyParser from "body-parser";
|
import { Hono } from "hono"
|
||||||
import { Router } from "express";
|
import * as Accounts from "../../../lib/accounts"
|
||||||
import * as Accounts from "../../../lib/accounts";
|
import { writeFile } from "fs/promises"
|
||||||
import * as auth from "../../../lib/auth";
|
import Files from "../../../lib/files"
|
||||||
import bytes from "bytes"
|
import {
|
||||||
import {writeFile} from "fs";
|
getAccount,
|
||||||
|
requiresAccount,
|
||||||
|
requiresPermissions,
|
||||||
|
} from "../../../lib/middleware"
|
||||||
|
|
||||||
import ServeError from "../../../lib/errors";
|
export let fileApiRoutes = new Hono<{
|
||||||
import Files from "../../../lib/files";
|
Variables: {
|
||||||
import { getAccount, requiresAccount, requiresPermissions } from "../../../lib/middleware";
|
account: Accounts.Account
|
||||||
|
}
|
||||||
let parser = bodyParser.json({
|
}>()
|
||||||
type: ["text/plain","application/json"]
|
|
||||||
})
|
|
||||||
|
|
||||||
export let fileApiRoutes = Router();
|
|
||||||
|
|
||||||
let config = require(`${process.cwd()}/config.json`)
|
let config = require(`${process.cwd()}/config.json`)
|
||||||
|
fileApiRoutes.use("*", getAccount) // :warning: /list somehow crashes Hono with an internal error!
|
||||||
|
/*
|
||||||
|
|
||||||
|
/home/jack/Code/Web/monofile/node_modules/.pnpm/@hono+node-server@1.2.0/node_modules/@hono/node-server/dist/listener.js:55
|
||||||
|
const contentType = res.headers.get("content-type") || "";
|
||||||
|
^
|
||||||
|
|
||||||
module.exports = function(files: Files) {
|
TypeError: Cannot read properties of undefined (reading 'get')
|
||||||
|
at Server.<anonymous> (/home/jack/Code/Web/monofile/node_modules/.pnpm/@hono+node-server@1.2.0/node_modules/@hono/node-server/dist/listener.js:55:37)
|
||||||
|
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
|
||||||
|
*/
|
||||||
|
|
||||||
fileApiRoutes.use(getAccount);
|
module.exports = function (files: Files) {
|
||||||
|
fileApiRoutes.get(
|
||||||
|
"/list",
|
||||||
|
requiresAccount,
|
||||||
|
requiresPermissions("user"),
|
||||||
|
async (ctx) => {
|
||||||
|
let acc = ctx.get("account") as Accounts.Account
|
||||||
|
|
||||||
fileApiRoutes.get("/list", requiresAccount, requiresPermissions("user"), (req,res) => {
|
if (!acc) return
|
||||||
|
let accId = acc.id
|
||||||
|
|
||||||
let acc = res.locals.acc as Accounts.Account
|
ctx.json(
|
||||||
|
acc.files
|
||||||
if (!acc) return
|
.map((e) => {
|
||||||
let accId = acc.id
|
let fp = files.getFilePointer(e)
|
||||||
|
if (!fp) {
|
||||||
|
Accounts.files.deindex(accId, e)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...fp,
|
||||||
|
messageids: null,
|
||||||
|
owner: null,
|
||||||
|
id: e,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((e) => e)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
res.send(acc.files.map((e) => {
|
fileApiRoutes.post(
|
||||||
let fp = files.getFilePointer(e)
|
"/manage",
|
||||||
if (!fp) { Accounts.files.deindex(accId, e); return null }
|
requiresPermissions("manage"),
|
||||||
return {
|
async (ctx) => {
|
||||||
...fp,
|
let acc = ctx.get("account") as Accounts.Account
|
||||||
messageids: null,
|
const body = await ctx.req.json()
|
||||||
owner: null,
|
if (!acc) return
|
||||||
id:e
|
if (
|
||||||
}
|
!body.target ||
|
||||||
}).filter(e=>e))
|
!(typeof body.target == "object") ||
|
||||||
|
body.target.length < 1
|
||||||
})
|
)
|
||||||
|
|
||||||
fileApiRoutes.post("/manage", parser, requiresPermissions("manage"), (req,res) => {
|
|
||||||
|
|
||||||
let acc = res.locals.acc as Accounts.Account
|
|
||||||
|
|
||||||
if (!acc) return
|
|
||||||
if (!req.body.target || !(typeof req.body.target == "object") || req.body.target.length < 1) return
|
|
||||||
|
|
||||||
let modified = 0
|
|
||||||
|
|
||||||
req.body.target.forEach((e:string) => {
|
|
||||||
if (!acc.files.includes(e)) return
|
|
||||||
|
|
||||||
let fp = files.getFilePointer(e)
|
|
||||||
|
|
||||||
if (fp.reserved) {
|
|
||||||
return
|
return
|
||||||
}
|
|
||||||
|
|
||||||
switch( req.body.action ) {
|
let modified = 0
|
||||||
case "delete":
|
|
||||||
files.unlink(e, true)
|
|
||||||
modified++;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "changeFileVisibility":
|
body.target.forEach((e: string) => {
|
||||||
if (!["public","anonymous","private"].includes(req.body.value)) return;
|
if (!acc.files.includes(e)) return
|
||||||
files.files[e].visibility = req.body.value;
|
|
||||||
modified++;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "setTag":
|
let fp = files.getFilePointer(e)
|
||||||
if (!req.body.value) delete files.files[e].tag
|
|
||||||
else {
|
|
||||||
if (req.body.value.toString().length > 30) return
|
|
||||||
files.files[e].tag = req.body.value.toString().toLowerCase()
|
|
||||||
}
|
|
||||||
modified++;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
Accounts.save().then(() => {
|
if (fp.reserved) {
|
||||||
writeFile(process.cwd()+"/.data/files.json",JSON.stringify(files.files), (err) => {
|
return
|
||||||
if (err) console.log(err)
|
}
|
||||||
res.contentType("text/plain")
|
|
||||||
res.send(`modified ${modified} files`)
|
switch (body.action) {
|
||||||
|
case "delete":
|
||||||
|
files.unlink(e, true)
|
||||||
|
modified++
|
||||||
|
break
|
||||||
|
|
||||||
|
case "changeFileVisibility":
|
||||||
|
if (
|
||||||
|
!["public", "anonymous", "private"].includes(
|
||||||
|
body.value
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
files.files[e].visibility = body.value
|
||||||
|
modified++
|
||||||
|
break
|
||||||
|
|
||||||
|
case "setTag":
|
||||||
|
if (!body.value) delete files.files[e].tag
|
||||||
|
else {
|
||||||
|
if (body.value.toString().length > 30) return
|
||||||
|
files.files[e].tag = body.value
|
||||||
|
.toString()
|
||||||
|
.toLowerCase()
|
||||||
|
}
|
||||||
|
modified++
|
||||||
|
break
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}).catch((err) => console.error(err))
|
|
||||||
|
|
||||||
|
|
||||||
})
|
return Accounts.save()
|
||||||
|
.then(() => {
|
||||||
|
writeFile(
|
||||||
|
process.cwd() + "/.data/files.json",
|
||||||
|
JSON.stringify(files.files)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.then(() => ctx.text(`modified ${modified} files`))
|
||||||
|
.catch((err) => console.error(err))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return fileApiRoutes
|
return fileApiRoutes
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import bodyParser from "body-parser"
|
import bodyParser from "body-parser"
|
||||||
import express, { Router } from "express"
|
import { Hono } from "hono"
|
||||||
|
|
||||||
import * as Accounts from "../../../lib/accounts"
|
import * as Accounts from "../../../lib/accounts"
|
||||||
import * as auth from "../../../lib/auth"
|
import * as auth from "../../../lib/auth"
|
||||||
import axios, { AxiosResponse } from "axios"
|
import axios, { AxiosResponse } from "axios"
|
||||||
import { type Range } from "range-parser"
|
import { type Range } from "range-parser"
|
||||||
import multer, { memoryStorage } from "multer"
|
import multer, { memoryStorage } from "multer"
|
||||||
|
import { Readable } from "stream"
|
||||||
import ServeError from "../../../lib/errors"
|
import ServeError from "../../../lib/errors"
|
||||||
import Files from "../../../lib/files"
|
import Files from "../../../lib/files"
|
||||||
import { getAccount, requiresPermissions } from "../../../lib/middleware"
|
import { getAccount, requiresPermissions } from "../../../lib/middleware"
|
||||||
|
@ -14,7 +15,11 @@ let parser = bodyParser.json({
|
||||||
type: ["text/plain", "application/json"],
|
type: ["text/plain", "application/json"],
|
||||||
})
|
})
|
||||||
|
|
||||||
export let primaryApi = Router()
|
export let primaryApi = new Hono<{
|
||||||
|
Variables: {
|
||||||
|
account: Accounts.Account
|
||||||
|
}
|
||||||
|
}>()
|
||||||
|
|
||||||
const multerSetup = multer({ storage: memoryStorage() })
|
const multerSetup = multer({ storage: memoryStorage() })
|
||||||
|
|
||||||
|
@ -23,216 +28,210 @@ let config = require(`${process.cwd()}/config.json`)
|
||||||
primaryApi.use(getAccount)
|
primaryApi.use(getAccount)
|
||||||
|
|
||||||
module.exports = function (files: Files) {
|
module.exports = function (files: Files) {
|
||||||
primaryApi.get(
|
// primaryApi.get(
|
||||||
["/file/:fileId", "/cpt/:fileId/*", "/:fileId"],
|
// ["/file/:fileId", "/cpt/:fileId/*", "/:fileId"],
|
||||||
async (req: express.Request, res: express.Response) => {
|
// async (ctx) => {
|
||||||
let acc = res.locals.acc as Accounts.Account
|
// let acc = ctx.get("account") as Accounts.Account
|
||||||
|
|
||||||
let file = files.getFilePointer(req.params.fileId)
|
// let file = files.getFilePointer(ctx.req.param("fileId"))
|
||||||
res.setHeader("Access-Control-Allow-Origin", "*")
|
// ctx.header("Access-Control-Allow-Origin", "*")
|
||||||
res.setHeader("Content-Security-Policy", "sandbox allow-scripts")
|
// ctx.header("Content-Security-Policy", "sandbox allow-scripts")
|
||||||
if (req.query.attachment == "1")
|
// if (ctx.req.query("attachment") == "1")
|
||||||
res.setHeader("Content-Disposition", "attachment")
|
// ctx.header("Content-Disposition", "attachment")
|
||||||
|
|
||||||
if (file) {
|
// if (file) {
|
||||||
if (file.visibility == "private") {
|
// if (file.visibility == "private") {
|
||||||
if (acc?.id != file.owner) {
|
// if (acc?.id != file.owner) {
|
||||||
ServeError(res, 403, "you do not own this file")
|
// return ServeError(ctx, 403, "you do not own this file")
|
||||||
return
|
// }
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
// if (
|
||||||
auth.getType(auth.tokenFor(req)) == "App" &&
|
// auth.getType(auth.tokenFor(ctx)!) == "App" &&
|
||||||
auth
|
// auth
|
||||||
.getPermissions(auth.tokenFor(req))
|
// .getPermissions(auth.tokenFor(ctx)!)
|
||||||
?.includes("private")
|
// ?.includes("private")
|
||||||
) {
|
// ) {
|
||||||
ServeError(res, 403, "insufficient permissions")
|
// ServeError(ctx, 403, "insufficient permissions")
|
||||||
return
|
// return
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
let range: Range | undefined
|
// let range: Range | undefined
|
||||||
|
|
||||||
res.setHeader("Content-Type", file.mime)
|
// ctx.header("Content-Type", file.mime)
|
||||||
if (file.sizeInBytes) {
|
// if (file.sizeInBytes) {
|
||||||
res.setHeader("Content-Length", file.sizeInBytes)
|
// ctx.header("Content-Length", file.sizeInBytes.toString())
|
||||||
|
|
||||||
if (file.chunkSize) {
|
// if (file.chunkSize) {
|
||||||
let rng = req.range(file.sizeInBytes)
|
// let range = ctx.range(file.sizeInBytes)
|
||||||
if (rng) {
|
// if (range) {
|
||||||
// error handling
|
// // error handling
|
||||||
if (typeof rng == "number") {
|
// if (typeof range == "number") {
|
||||||
res.status(rng == -1 ? 416 : 400).send()
|
// return ctx.status(range == -1 ? 416 : 400)
|
||||||
return
|
// }
|
||||||
}
|
// if (range.type != "bytes") {
|
||||||
if (rng.type != "bytes") {
|
// return ctx.status(400)
|
||||||
res.status(400).send()
|
// }
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// set ranges var
|
// // set ranges var
|
||||||
let rngs = Array.from(rng)
|
// let rngs = Array.from(range)
|
||||||
if (rngs.length != 1) {
|
// if (rngs.length != 1) {
|
||||||
res.status(400).send()
|
// return ctx.status(400)
|
||||||
return
|
// }
|
||||||
}
|
// range = rngs[0]
|
||||||
range = rngs[0]
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
|
||||||
|
|
||||||
// supports ranges
|
// // supports ranges
|
||||||
|
|
||||||
files
|
// return files
|
||||||
.readFileStream(req.params.fileId, range)
|
// .readFileStream(ctx.req.param("fileId"), range)
|
||||||
.then(async (stream) => {
|
// .then(async (stream) => {
|
||||||
if (range) {
|
// if (range) {
|
||||||
res.status(206)
|
// ctx.status(206)
|
||||||
res.header(
|
// ctx.header(
|
||||||
"Content-Length",
|
// "Content-Length",
|
||||||
(range.end - range.start + 1).toString()
|
// (range.end - range.start + 1).toString()
|
||||||
)
|
// )
|
||||||
res.header(
|
// ctx.header(
|
||||||
"Content-Range",
|
// "Content-Range",
|
||||||
`bytes ${range.start}-${range.end}/${file.sizeInBytes}`
|
// `bytes ${range.start}-${range.end}/${file.sizeInBytes}`
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
stream.pipe(res)
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
ServeError(res, err.status, err.message)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
ServeError(res, 404, "file not found")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
primaryApi.head(
|
// return ctx.stream((stre) => {
|
||||||
["/file/:fileId", "/cpt/:fileId/*", "/:fileId"],
|
// // Somehow return a stream?
|
||||||
(req: express.Request, res: express.Response) => {
|
// })
|
||||||
let file = files.getFilePointer(req.params.fileId)
|
// })
|
||||||
|
// .catch((err) => {
|
||||||
|
// return ServeError(ctx, err.status, err.message)
|
||||||
|
// })
|
||||||
|
// } else {
|
||||||
|
// return ServeError(ctx, 404, "file not found")
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
|
||||||
if (
|
// // primaryApi.head(
|
||||||
file.visibility == "private" &&
|
// // ["/file/:fileId", "/cpt/:fileId/*", "/:fileId"],
|
||||||
(res.locals.acc?.id != file.owner ||
|
// // async (ctx) => {
|
||||||
(auth.getType(auth.tokenFor(req)) == "App" &&
|
// // let file = files.getFilePointer(req.params.fileId)
|
||||||
auth
|
|
||||||
.getPermissions(auth.tokenFor(req))
|
|
||||||
?.includes("private")))
|
|
||||||
) {
|
|
||||||
res.status(403).send()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
res.setHeader("Access-Control-Allow-Origin", "*")
|
// // if (
|
||||||
res.setHeader("Content-Security-Policy", "sandbox allow-scripts")
|
// // file.visibility == "private" &&
|
||||||
|
// // (ctx.get("account")?.id != file.owner ||
|
||||||
|
// // (auth.getType(auth.tokenFor(ctx)!) == "App" &&
|
||||||
|
// // auth
|
||||||
|
// // .getPermissions(auth.tokenFor(ctx)!)
|
||||||
|
// // ?.includes("private")))
|
||||||
|
// // ) {
|
||||||
|
// // return ctx.status(403)
|
||||||
|
// // }
|
||||||
|
|
||||||
if (req.query.attachment == "1")
|
// // ctx.header("Content-Security-Policy", "sandbox allow-scripts")
|
||||||
res.setHeader("Content-Disposition", "attachment")
|
|
||||||
|
|
||||||
if (!file) {
|
// // if (ctx.req.query("attachment") == "1")
|
||||||
res.status(404)
|
// // ctx.header("Content-Disposition", "attachment")
|
||||||
res.send()
|
|
||||||
} else {
|
|
||||||
res.setHeader("Content-Type", file.mime)
|
|
||||||
if (file.sizeInBytes) {
|
|
||||||
res.setHeader("Content-Length", file.sizeInBytes)
|
|
||||||
}
|
|
||||||
if (file.chunkSize) {
|
|
||||||
res.setHeader("Accept-Ranges", "bytes")
|
|
||||||
}
|
|
||||||
res.send()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// upload handlers
|
// // if (!file) {
|
||||||
|
// // res.status(404)
|
||||||
|
// // res.send()
|
||||||
|
// // } else {
|
||||||
|
// // ctx.header("Content-Type", file.mime)
|
||||||
|
// // if (file.sizeInBytes) {
|
||||||
|
// // ctx.header("Content-Length", file.sizeInBytes)
|
||||||
|
// // }
|
||||||
|
// // if (file.chunkSize) {
|
||||||
|
// // ctx.header("Accept-Ranges", "bytes")
|
||||||
|
// // }
|
||||||
|
// // res.send()
|
||||||
|
// // }
|
||||||
|
// // }
|
||||||
|
// // )
|
||||||
|
|
||||||
primaryApi.post(
|
// // upload handlers
|
||||||
"/upload",
|
|
||||||
requiresPermissions("upload"),
|
|
||||||
multerSetup.single("file"),
|
|
||||||
async (req, res) => {
|
|
||||||
let acc = res.locals.acc as Accounts.Account
|
|
||||||
|
|
||||||
if (req.file) {
|
// primaryApi.post(
|
||||||
try {
|
// "/upload",
|
||||||
let prm = req.header("monofile-params")
|
// requiresPermissions("upload"),
|
||||||
let params: { [key: string]: any } = {}
|
// multerSetup.single("file"),
|
||||||
if (prm) {
|
// async (ctx) => {
|
||||||
params = JSON.parse(prm)
|
// let acc = ctx.get("account") as Accounts.Account
|
||||||
}
|
|
||||||
|
|
||||||
files
|
// if (req.file) {
|
||||||
.uploadFile(
|
// try {
|
||||||
{
|
// let prm = req.header("monofile-params")
|
||||||
owner: acc?.id,
|
// let params: { [key: string]: any } = {}
|
||||||
|
// if (prm) {
|
||||||
|
// params = JSON.parse(prm)
|
||||||
|
// }
|
||||||
|
|
||||||
uploadId: params.uploadId,
|
// files
|
||||||
filename: req.file.originalname,
|
// .uploadFile(
|
||||||
mime: req.file.mimetype,
|
// {
|
||||||
},
|
// owner: acc?.id,
|
||||||
req.file.buffer
|
|
||||||
)
|
|
||||||
.then((uID) => res.send(uID))
|
|
||||||
.catch((stat) => {
|
|
||||||
res.status(stat.status)
|
|
||||||
res.send(`[err] ${stat.message}`)
|
|
||||||
})
|
|
||||||
} catch {
|
|
||||||
res.status(400)
|
|
||||||
res.send("[err] bad request")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
res.status(400)
|
|
||||||
res.send("[err] bad request")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
primaryApi.post(
|
// uploadId: params.uploadId,
|
||||||
"/clone",
|
// filename: req.file.originalname,
|
||||||
requiresPermissions("upload"),
|
// mime: req.file.mimetype,
|
||||||
bodyParser.json({ type: ["text/plain", "application/json"] }),
|
// },
|
||||||
(req, res) => {
|
// req.file.buffer
|
||||||
let acc = res.locals.acc as Accounts.Account
|
// )
|
||||||
|
// .then((uID) => res.send(uID))
|
||||||
|
// .catch((stat) => {
|
||||||
|
// res.status(stat.status)
|
||||||
|
// res.send(`[err] ${stat.message}`)
|
||||||
|
// })
|
||||||
|
// } catch {
|
||||||
|
// res.status(400)
|
||||||
|
// res.send("[err] bad request")
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// res.status(400)
|
||||||
|
// res.send("[err] bad request")
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
|
||||||
try {
|
// primaryApi.post(
|
||||||
axios
|
// "/clone",
|
||||||
.get(req.body.url, { responseType: "arraybuffer" })
|
// requiresPermissions("upload"),
|
||||||
.then((data: AxiosResponse) => {
|
// async ctx => {
|
||||||
files
|
// let acc = ctx.get("account") as Accounts.Account
|
||||||
.uploadFile(
|
|
||||||
{
|
// try {
|
||||||
owner: acc?.id,
|
// return axios
|
||||||
filename:
|
// .get(req.body.url, { responseType: "arraybuffer" })
|
||||||
req.body.url.split("/")[
|
// .then((data: AxiosResponse) => {
|
||||||
req.body.url.split("/").length - 1
|
// files
|
||||||
] || "generic",
|
// .uploadFile(
|
||||||
mime: data.headers["content-type"],
|
// {
|
||||||
uploadId: req.body.uploadId,
|
// owner: acc?.id,
|
||||||
},
|
// filename:
|
||||||
Buffer.from(data.data)
|
// req.body.url.split("/")[
|
||||||
)
|
// req.body.url.split("/").length - 1
|
||||||
.then((uID) => res.send(uID))
|
// ] || "generic",
|
||||||
.catch((stat) => {
|
// mime: data.headers["content-type"],
|
||||||
res.status(stat.status)
|
// uploadId: req.body.uploadId,
|
||||||
res.send(`[err] ${stat.message}`)
|
// },
|
||||||
})
|
// Buffer.from(data.data)
|
||||||
})
|
// )
|
||||||
.catch((err) => {
|
// .then((uID) => res.send(uID))
|
||||||
console.log(err)
|
// .catch((stat) => {
|
||||||
res.status(400)
|
// res.status(stat.status)
|
||||||
res.send(`[err] failed to fetch data`)
|
// res.send(`[err] ${stat.message}`)
|
||||||
})
|
// })
|
||||||
} catch {
|
// })
|
||||||
res.status(500)
|
// .catch((err) => {
|
||||||
res.send("[err] an error occured")
|
// console.log(err)
|
||||||
}
|
// return res.text(`[err] failed to fetch data`, 400)
|
||||||
}
|
// })
|
||||||
)
|
// } catch {
|
||||||
|
// return ctx.text("[err] an error occured", 500)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
|
||||||
return primaryApi
|
return primaryApi
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,214 +1,223 @@
|
||||||
// Modules
|
// Modules
|
||||||
|
|
||||||
import { writeFile } from 'fs'
|
|
||||||
import { Router } from "express";
|
import { Hono } from "hono"
|
||||||
import bodyParser from "body-parser";
|
import { getCookie, setCookie } from "hono/cookie"
|
||||||
|
|
||||||
// Libs
|
// Libs
|
||||||
|
|
||||||
import Files, { id_check_regex } from "../../../lib/files";
|
import Files, { id_check_regex } from "../../../lib/files"
|
||||||
import * as Accounts from '../../../lib/accounts'
|
import * as Accounts from "../../../lib/accounts"
|
||||||
import * as Authentication from '../../../lib/auth'
|
import * as Authentication from "../../../lib/auth"
|
||||||
import { assertAPI, getAccount, noAPIAccess, requiresAccount, requiresPermissions } from "../../../lib/middleware";
|
import {
|
||||||
import ServeError from "../../../lib/errors";
|
assertAPI,
|
||||||
import { sendMail } from '../../../lib/mail';
|
getAccount,
|
||||||
|
noAPIAccess,
|
||||||
|
requiresAccount,
|
||||||
|
requiresPermissions,
|
||||||
|
} from "../../../lib/middleware"
|
||||||
|
import ServeError from "../../../lib/errors"
|
||||||
|
import { sendMail } from "../../../lib/mail"
|
||||||
|
|
||||||
const Configuration = require(`${process.cwd()}/config.json`)
|
const Configuration = require(`${process.cwd()}/config.json`)
|
||||||
|
|
||||||
const parser = bodyParser.json({
|
const router = new Hono<{
|
||||||
type: [ "type/plain", "application/json" ]
|
Variables: {
|
||||||
})
|
account: Accounts.Account
|
||||||
|
}
|
||||||
|
}>()
|
||||||
|
|
||||||
const router = Router()
|
router.use(getAccount)
|
||||||
|
|
||||||
router.use(getAccount, parser)
|
module.exports = function (files: Files) {
|
||||||
|
router.post("/login", async (ctx, res) => {
|
||||||
module.exports = function(files: Files) {
|
const body = await ctx.req.json()
|
||||||
router.post(
|
if (
|
||||||
"/login",
|
typeof body.username != "string" ||
|
||||||
(req, res) => {
|
typeof body.password != "string"
|
||||||
if (typeof req.body.username != "string" || typeof req.body.password != "string") {
|
) {
|
||||||
ServeError(res, 400, "please provide a username or password")
|
ServeError(ctx, 400, "please provide a username or password")
|
||||||
return
|
return
|
||||||
}
|
|
||||||
|
|
||||||
if (Authentication.validate(req.cookies.auth)) {
|
|
||||||
ServeError(res, 400, "you are already logged in")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const Account = Accounts.getFromUsername(req.body.username)
|
|
||||||
|
|
||||||
if (!Account || !Accounts.password.check(Account.id, req.body.password)) {
|
|
||||||
ServeError(res, 400, "username or password incorrect")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
res.cookie("auth",
|
|
||||||
Authentication.create(
|
|
||||||
Account.id, // account id
|
|
||||||
(3 * 24 * 60 * 60 * 1000) // expiration time
|
|
||||||
)
|
|
||||||
)
|
|
||||||
res.status(200)
|
|
||||||
res.end()
|
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
router.post(
|
if (Authentication.validate(getCookie(ctx, "auth")!)) {
|
||||||
"/create",
|
ServeError(ctx, 400, "you are already logged in")
|
||||||
(req, res) => {
|
return
|
||||||
if (!Configuration.accounts.registrationEnabled) {
|
}
|
||||||
ServeError(res , 403, "account registration disabled")
|
|
||||||
return
|
const Account = Accounts.getFromUsername(body.username)
|
||||||
|
|
||||||
|
if (!Account || !Accounts.password.check(Account.id, body.password)) {
|
||||||
|
ServeError(ctx, 400, "username or password incorrect")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setCookie(
|
||||||
|
ctx,
|
||||||
|
"auth",
|
||||||
|
Authentication.create(
|
||||||
|
Account.id, // account id
|
||||||
|
3 * 24 * 60 * 60 * 1000 // expiration time
|
||||||
|
),
|
||||||
|
{
|
||||||
|
// expires:
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
ctx.status(200)
|
||||||
|
})
|
||||||
|
|
||||||
if (Authentication.validate(req.cookies.auth)) {
|
router.post("/create", async (ctx) => {
|
||||||
ServeError(res, 400, "you are already logged in")
|
const body = await ctx.req.json()
|
||||||
return
|
if (!Configuration.accounts.registrationEnabled) {
|
||||||
}
|
return ServeError(ctx, 403, "account registration disabled")
|
||||||
|
}
|
||||||
|
|
||||||
if (Accounts.getFromUsername(req.body.username)) {
|
if (Authentication.validate(getCookie(ctx, "auth")!)) {
|
||||||
ServeError(res, 400, "account with this username already exists")
|
return ServeError(ctx, 400, "you are already logged in")
|
||||||
return
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (req.body.username.length < 3 || req.body.username.length > 20) {
|
if (Accounts.getFromUsername(body.username)) {
|
||||||
ServeError(res, 400, "username must be over or equal to 3 characters or under or equal to 20 characters in length")
|
return ServeError(
|
||||||
return
|
ctx,
|
||||||
}
|
400,
|
||||||
|
"account with this username already exists"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (body.username.length < 3 || body.username.length > 20) {
|
||||||
(
|
return ServeError(
|
||||||
req.body.username.match(/[A-Za-z0-9_\-\.]+/)
|
ctx,
|
||||||
||
|
400,
|
||||||
[]
|
"username must be over or equal to 3 characters or under or equal to 20 characters in length"
|
||||||
)[0] != req.body.username
|
)
|
||||||
) {
|
}
|
||||||
ServeError(res, 400, "username contains invalid characters")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (req.body.password.length < 8) {
|
if (
|
||||||
ServeError(res, 400, "password must be 8 characters or longer")
|
(body.username.match(/[A-Za-z0-9_\-\.]+/) || [])[0] != body.username
|
||||||
return
|
) {
|
||||||
}
|
return ServeError(ctx, 400, "username contains invalid characters")
|
||||||
|
}
|
||||||
|
|
||||||
Accounts.create(
|
if (body.password.length < 8) {
|
||||||
req.body.username,
|
return ServeError(
|
||||||
req.body.password
|
ctx,
|
||||||
).then((Account) => {
|
400,
|
||||||
res.cookie("auth", Authentication.create(
|
"password must be 8 characters or longer"
|
||||||
Account, // account id
|
)
|
||||||
(3 * 24 * 60 * 60 * 1000) // expiration time
|
}
|
||||||
))
|
|
||||||
res.status(200)
|
return Accounts.create(body.username, body.password)
|
||||||
res.end()
|
.then((Account) => {
|
||||||
|
setCookie(
|
||||||
|
ctx,
|
||||||
|
"auth",
|
||||||
|
Authentication.create(
|
||||||
|
Account, // account id
|
||||||
|
3 * 24 * 60 * 60 * 1000 // expiration time
|
||||||
|
),
|
||||||
|
{
|
||||||
|
// expires:
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return ctx.status(200)
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
ServeError(res, 500, "internal server error")
|
return ServeError(ctx, 500, "internal server error")
|
||||||
})
|
})
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
router.post(
|
router.post("/logout", (ctx) => {
|
||||||
"/logout",
|
if (!Authentication.validate(getCookie(ctx, "auth")!)) {
|
||||||
(req, res) => {
|
return ServeError(ctx, 401, "not logged in")
|
||||||
if (!Authentication.validate(req.cookies.auth)) {
|
|
||||||
ServeError(res, 401, "not logged in")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Authentication.invalidate(req.cookies.auth)
|
|
||||||
res.send("logged out")
|
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
Authentication.invalidate(getCookie(ctx, "auth")!)
|
||||||
|
return ctx.text("logged out")
|
||||||
|
})
|
||||||
|
|
||||||
router.patch(
|
router.patch(
|
||||||
"/dfv",
|
"/dfv",
|
||||||
requiresAccount, requiresPermissions("manage"),
|
|
||||||
(req, res) => {
|
|
||||||
const Account = res.locals.acc as Accounts.Account
|
|
||||||
|
|
||||||
if (['public', 'private', 'anonymous'].includes(req.body.defaultFileVisibility)) {
|
|
||||||
Account.defaultFileVisibility = req.body.defaultFileVisibility
|
|
||||||
|
|
||||||
Accounts.save()
|
|
||||||
|
|
||||||
res.send(`dfv has been set to ${Account.defaultFileVisibility}`)
|
|
||||||
} else {
|
|
||||||
ServeError(res, 400, "invalid dfv")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
router.delete("/me",
|
|
||||||
requiresAccount, noAPIAccess,
|
|
||||||
parser,
|
|
||||||
(req, res) => {
|
|
||||||
const Account = res.locals.acc as Accounts.Account
|
|
||||||
|
|
||||||
const accountId = Account.id
|
|
||||||
|
|
||||||
Authentication.AuthTokens.filter(e => e.account == accountId).forEach((token) => {
|
|
||||||
Authentication.invalidate(token.token)
|
|
||||||
})
|
|
||||||
|
|
||||||
Accounts.deleteAccount(accountId).then(_ => res.send("account deleted"))
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
router.patch("/me/name",
|
|
||||||
requiresAccount,
|
requiresAccount,
|
||||||
noAPIAccess,
|
requiresPermissions("manage"),
|
||||||
parser,
|
async (ctx) => {
|
||||||
(req, res) => {
|
const body = await ctx.req.json()
|
||||||
const Account = res.locals.acc as Accounts.Account
|
const Account = ctx.get("account")! as Accounts.Account
|
||||||
|
|
||||||
const newUsername = req.body.username
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
typeof newUsername != "string"
|
["public", "private", "anonymous"].includes(
|
||||||
||
|
body.defaultFileVisibility
|
||||||
newUsername.length < 3
|
)
|
||||||
||
|
|
||||||
req.body.username.length > 20
|
|
||||||
) {
|
) {
|
||||||
ServeError(res, 400, "username must be between 3 and 20 characters in length")
|
Account.defaultFileVisibility = body.defaultFileVisibility
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Accounts.getFromUsername(newUsername)) {
|
Accounts.save()
|
||||||
ServeError(res, 400, "account with this username already exists")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
return ctx.text(
|
||||||
(
|
`dfv has been set to ${Account.defaultFileVisibility}`
|
||||||
newUsername.match(/[A-Za-z0-9_\-\.]+/)
|
)
|
||||||
||
|
} else {
|
||||||
[]
|
return ServeError(ctx, 400, "invalid dfv")
|
||||||
)[0] != req.body.username
|
|
||||||
) {
|
|
||||||
ServeError(res, 400, "username contains invalid characters")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Account.username = newUsername
|
|
||||||
Accounts.save()
|
|
||||||
|
|
||||||
if (Account.email) {
|
|
||||||
sendMail(
|
|
||||||
Account.email,
|
|
||||||
`Your login details have been updated`,
|
|
||||||
`<b>Hello there!</b> Your username has been updated to <span username>${newUsername}</span>. Please update your devices accordingly. Thank you for using monofile.`
|
|
||||||
).then(() => {
|
|
||||||
res.send("OK")
|
|
||||||
}).catch((err) => {})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
router.delete("/me", requiresAccount, noAPIAccess, async (ctx) => {
|
||||||
|
const Account = ctx.get("account") as Accounts.Account
|
||||||
|
const accountId = Account.id
|
||||||
|
|
||||||
|
Authentication.AuthTokens.filter((e) => e.account == accountId).forEach(
|
||||||
|
(token) => {
|
||||||
|
Authentication.invalidate(token.token)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
await Accounts.deleteAccount(accountId)
|
||||||
|
return ctx.text("account deleted")
|
||||||
|
})
|
||||||
|
|
||||||
|
router.patch("/me/name", requiresAccount, noAPIAccess, async (ctx) => {
|
||||||
|
const Account = ctx.get("account") as Accounts.Account
|
||||||
|
const body = await ctx.req.json()
|
||||||
|
const newUsername = body.username
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof newUsername != "string" ||
|
||||||
|
newUsername.length < 3 ||
|
||||||
|
newUsername.length > 20
|
||||||
|
) {
|
||||||
|
return ServeError(
|
||||||
|
ctx,
|
||||||
|
400,
|
||||||
|
"username must be between 3 and 20 characters in length"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Accounts.getFromUsername(newUsername)) {
|
||||||
|
return ServeError(
|
||||||
|
ctx,
|
||||||
|
400,
|
||||||
|
"account with this username already exists"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(newUsername.match(/[A-Za-z0-9_\-\.]+/) || [])[0] != body.username
|
||||||
|
) {
|
||||||
|
ServeError(ctx, 400, "username contains invalid characters")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Account.username = newUsername
|
||||||
|
Accounts.save()
|
||||||
|
|
||||||
|
if (Account.email) {
|
||||||
|
await sendMail(
|
||||||
|
Account.email,
|
||||||
|
`Your login details have been updated`,
|
||||||
|
`<b>Hello there!</b> Your username has been updated to <span username>${newUsername}</span>. Please update your devices accordingly. Thank you for using monofile.`
|
||||||
|
).catch()
|
||||||
|
return ctx.text("OK")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return router
|
return router
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,120 +1,119 @@
|
||||||
// Modules
|
// Modules
|
||||||
|
|
||||||
import { writeFile } from 'fs'
|
import { writeFile } from "fs/promises"
|
||||||
import { Router } from "express";
|
import { Hono } from "hono"
|
||||||
import bodyParser from "body-parser";
|
|
||||||
|
|
||||||
// Libs
|
// Libs
|
||||||
|
|
||||||
import Files, { id_check_regex } from "../../../lib/files";
|
import Files, { id_check_regex } from "../../../lib/files"
|
||||||
import * as Accounts from '../../../lib/accounts'
|
import * as Accounts from "../../../lib/accounts"
|
||||||
import * as Authentication from '../../../lib/auth'
|
import * as Authentication from "../../../lib/auth"
|
||||||
import { assertAPI, getAccount, noAPIAccess, requiresAccount, requiresAdmin, requiresPermissions } from "../../../lib/middleware";
|
import {
|
||||||
import ServeError from "../../../lib/errors";
|
getAccount,
|
||||||
import { sendMail } from '../../../lib/mail';
|
noAPIAccess,
|
||||||
|
requiresAccount,
|
||||||
|
requiresAdmin,
|
||||||
|
} from "../../../lib/middleware"
|
||||||
|
import ServeError from "../../../lib/errors"
|
||||||
|
import { sendMail } from "../../../lib/mail"
|
||||||
|
|
||||||
const Configuration = require(`${process.cwd()}/config.json`)
|
const Configuration = require(`${process.cwd()}/config.json`)
|
||||||
|
|
||||||
const parser = bodyParser.json({
|
const router = new Hono<{
|
||||||
type: [ "type/plain", "application/json" ]
|
Variables: {
|
||||||
})
|
account?: Accounts.Account
|
||||||
|
}
|
||||||
|
}>()
|
||||||
|
|
||||||
const router = Router()
|
router.use(getAccount, requiresAccount, requiresAdmin)
|
||||||
|
|
||||||
router.use(getAccount, requiresAccount, requiresAdmin, parser)
|
module.exports = function (files: Files) {
|
||||||
|
router.patch("/account/:username/password", async (ctx) => {
|
||||||
|
const Account = ctx.get("account") as Accounts.Account
|
||||||
|
const body = await ctx.req.json()
|
||||||
|
|
||||||
module.exports = function(files: Files) {
|
const targetUsername = ctx.req.param("username")
|
||||||
router.patch(
|
const password = body.password
|
||||||
"/account/:username/password",
|
|
||||||
(req, res) => {
|
|
||||||
const Account = res.locals.acc as Accounts.Account
|
|
||||||
|
|
||||||
const targetUsername = req.params.username
|
if (typeof password !== "string") return ServeError(ctx, 404, "")
|
||||||
const password = req.body.password
|
|
||||||
|
|
||||||
if (typeof password !== "string") {
|
const targetAccount = Accounts.getFromUsername(targetUsername)
|
||||||
ServeError(res, 404, "")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const targetAccount = Accounts.getFromUsername(targetUsername)
|
if (!targetAccount) return ServeError(ctx, 404, "")
|
||||||
|
|
||||||
if (!targetAccount) {
|
Accounts.password.set(targetAccount.id, password)
|
||||||
ServeError(res, 404, "")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Accounts.password.set( targetAccount.id, password )
|
Authentication.AuthTokens.filter(
|
||||||
|
(e) => e.account == targetAccount?.id
|
||||||
Authentication.AuthTokens.filter(e => e.account == targetAccount?.id).forEach((accountToken) => {
|
).forEach((accountToken) => {
|
||||||
Authentication.invalidate(accountToken.token)
|
Authentication.invalidate(accountToken.token)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (targetAccount.email) {
|
if (targetAccount.email) {
|
||||||
sendMail(targetAccount.email, `Your login details have been updated`, `<b>Hello there!</b> This email is to notify you of a password change that an administrator, <span username>${Account.username}</span>, has initiated. You have been logged out of your devices. Thank you for using monofile.`).then(() => {
|
await sendMail(
|
||||||
res.send("OK")
|
targetAccount.email,
|
||||||
}).catch((err) => {})
|
`Your login details have been updated`,
|
||||||
}
|
`<b>Hello there!</b> This email is to notify you of a password change that an administrator, <span username>${Account.username}</span>, has initiated. You have been logged out of your devices. Thank you for using monofile.`
|
||||||
|
).catch()
|
||||||
res.send()
|
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
router.patch(
|
return ctx.text("")
|
||||||
"/account/:username/elevate",
|
})
|
||||||
(req, res) => {
|
|
||||||
const targetUsername = req.params.username
|
|
||||||
const targetAccount = Accounts.getFromUsername(targetUsername)
|
|
||||||
|
|
||||||
if (!targetAccount) {
|
router.patch("/account/:username/elevate", (ctx) => {
|
||||||
ServeError(res, 404, "")
|
const targetUsername = ctx.req.param("username")
|
||||||
return
|
const targetAccount = Accounts.getFromUsername(targetUsername)
|
||||||
}
|
|
||||||
|
|
||||||
targetAccount.admin = true
|
if (!targetAccount) {
|
||||||
Accounts.save()
|
return ServeError(ctx, 404, "")
|
||||||
|
|
||||||
res.send()
|
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
router.delete("/account/:username/:deleteFiles",
|
targetAccount.admin = true
|
||||||
|
Accounts.save()
|
||||||
|
|
||||||
|
return ctx.text("")
|
||||||
|
})
|
||||||
|
|
||||||
|
router.delete(
|
||||||
|
"/account/:username/:deleteFiles",
|
||||||
requiresAccount,
|
requiresAccount,
|
||||||
noAPIAccess,
|
noAPIAccess,
|
||||||
parser,
|
async (ctx) => {
|
||||||
(req, res) => {
|
const targetUsername = ctx.req.param("username")
|
||||||
const targetUsername = req.params.username
|
const deleteFiles = ctx.req.param("deleteFiles")
|
||||||
const deleteFiles = req.params.deleteFiles
|
|
||||||
|
|
||||||
const targetAccount = Accounts.getFromUsername(targetUsername)
|
const targetAccount = Accounts.getFromUsername(targetUsername)
|
||||||
|
|
||||||
if (!targetAccount) {
|
if (!targetAccount) return ServeError(ctx, 404, "")
|
||||||
ServeError(res, 404, "")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const accountId = targetAccount.id
|
const accountId = targetAccount.id
|
||||||
|
|
||||||
Authentication.AuthTokens.filter(e => e.account == accountId).forEach((token) => {
|
Authentication.AuthTokens.filter(
|
||||||
|
(e) => e.account == accountId
|
||||||
|
).forEach((token) => {
|
||||||
Authentication.invalidate(token.token)
|
Authentication.invalidate(token.token)
|
||||||
})
|
})
|
||||||
|
|
||||||
const deleteAccount = () => Accounts.deleteAccount(accountId).then(_ => res.send("account deleted"))
|
const deleteAccount = () =>
|
||||||
|
Accounts.deleteAccount(accountId).then((_) =>
|
||||||
|
ctx.text("account deleted")
|
||||||
|
)
|
||||||
|
|
||||||
if (deleteFiles) {
|
if (deleteFiles) {
|
||||||
const Files = targetAccount.files.map(e => e)
|
const Files = targetAccount.files.map((e) => e)
|
||||||
|
|
||||||
for (let fileId of Files) {
|
for (let fileId of Files) {
|
||||||
files.unlink(fileId, true).catch(err => console.error)
|
files.unlink(fileId, true).catch((err) => console.error)
|
||||||
}
|
}
|
||||||
|
|
||||||
writeFile(process.cwd() + "/.data/files.json", JSON.stringify(files.files), (err) => {
|
await writeFile(
|
||||||
if (err) console.log(err)
|
process.cwd() + "/.data/files.json",
|
||||||
deleteAccount()
|
JSON.stringify(files.files)
|
||||||
})
|
)
|
||||||
} else deleteAccount()
|
return deleteAccount()
|
||||||
|
} else return deleteAccount()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
return router
|
return router
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,98 +1,97 @@
|
||||||
// Modules
|
import { Hono } from "hono"
|
||||||
|
import Files, { id_check_regex } from "../../../lib/files"
|
||||||
import { Router } from "express";
|
import * as Accounts from "../../../lib/accounts"
|
||||||
import bodyParser from "body-parser";
|
import {
|
||||||
|
getAccount,
|
||||||
// Libs
|
requiresAccount,
|
||||||
|
requiresPermissions,
|
||||||
import Files, { id_check_regex } from "../../../lib/files";
|
} from "../../../lib/middleware"
|
||||||
import * as Accounts from '../../../lib/accounts'
|
import ServeError from "../../../lib/errors"
|
||||||
import { getAccount, requiresAccount, requiresPermissions } from "../../../lib/middleware";
|
|
||||||
import ServeError from "../../../lib/errors";
|
|
||||||
|
|
||||||
const Configuration = require(`${process.cwd()}/config.json`)
|
const Configuration = require(`${process.cwd()}/config.json`)
|
||||||
|
|
||||||
const parser = bodyParser.json({
|
const router = new Hono<{
|
||||||
type: [ "type/plain", "application/json" ]
|
Variables: {
|
||||||
})
|
account?: Accounts.Account
|
||||||
|
}
|
||||||
|
}>()
|
||||||
|
|
||||||
const router = Router()
|
router.use(getAccount)
|
||||||
|
|
||||||
router.use(getAccount, parser)
|
module.exports = function (files: Files) {
|
||||||
|
|
||||||
module.exports = function(files: Files) {
|
|
||||||
router.put(
|
router.put(
|
||||||
"/css",
|
"/css",
|
||||||
requiresAccount, requiresPermissions("customize"),
|
|
||||||
async (req, res) => {
|
|
||||||
const Account = res.locals.acc as Accounts.Account
|
|
||||||
|
|
||||||
if (typeof req.body.fileId != "string") req.body.fileId = undefined;
|
|
||||||
|
|
||||||
if (
|
|
||||||
!req.body.fileId
|
|
||||||
||
|
|
||||||
(req.body.fileId.match(id_check_regex) == req.body.fileId
|
|
||||||
&& req.body.fileId.length <= Configuration.maxUploadIdLength)
|
|
||||||
) {
|
|
||||||
Account.customCSS = req.body.fileId || undefined
|
|
||||||
|
|
||||||
await Accounts.save()
|
|
||||||
res.send("custom css saved")
|
|
||||||
} else ServeError(res, 400, "invalid fileId")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
router.get('/css',
|
|
||||||
requiresAccount,
|
requiresAccount,
|
||||||
(req, res) => {
|
requiresPermissions("customize"),
|
||||||
const Account = res.locals.acc
|
async (ctx) => {
|
||||||
|
const Account = ctx.get("account") as Accounts.Account
|
||||||
|
const body = await ctx.req.json()
|
||||||
|
if (typeof body.fileId != "string") body.fileId = undefined
|
||||||
|
|
||||||
if (Account?.customCSS) res.redirect(`/file/${Account.customCSS}`)
|
|
||||||
else res.send("");
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
router.put("/embed/color",
|
|
||||||
requiresAccount, requiresPermissions("customize"),
|
|
||||||
async (req, res) => {
|
|
||||||
const Account = res.locals.acc as Accounts.Account
|
|
||||||
|
|
||||||
if (typeof req.body.color != "string") req.body.color = undefined;
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!req.body.color
|
!body.fileId ||
|
||||||
|| (req.body.color.toLowerCase().match(/[a-f0-9]+/) == req.body.color.toLowerCase())
|
(body.fileId.match(id_check_regex) == body.fileId &&
|
||||||
&& req.body.color.length == 6
|
body.fileId.length <= Configuration.maxUploadIdLength)
|
||||||
) {
|
) {
|
||||||
|
Account.customCSS = body.fileId || undefined
|
||||||
if (!Account.embed) Account.embed = {};
|
|
||||||
Account.embed.color = req.body.color || undefined
|
|
||||||
|
|
||||||
await Accounts.save()
|
await Accounts.save()
|
||||||
res.send("custom embed color saved")
|
return ctx.text("custom css saved")
|
||||||
|
} else return ServeError(ctx, 400, "invalid fileId")
|
||||||
} else ServeError(res,400,"invalid hex code")
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
router.put("/embed/size",
|
router.get("/css", requiresAccount, async (ctx) => {
|
||||||
requiresAccount, requiresPermissions("customize"),
|
const Account = ctx.get("account")
|
||||||
async (req, res) => {
|
|
||||||
const Account = res.locals.acc as Accounts.Account
|
|
||||||
|
|
||||||
if (typeof req.body.largeImage != "boolean") {
|
if (Account?.customCSS)
|
||||||
ServeError(res, 400, "largeImage must be bool");
|
return ctx.redirect(`/file/${Account.customCSS}`)
|
||||||
|
else return ctx.text("")
|
||||||
|
})
|
||||||
|
|
||||||
|
router.put(
|
||||||
|
"/embed/color",
|
||||||
|
requiresAccount,
|
||||||
|
requiresPermissions("customize"),
|
||||||
|
async (ctx) => {
|
||||||
|
const Account = ctx.get("account") as Accounts.Account
|
||||||
|
const body = await ctx.req.json()
|
||||||
|
if (typeof body.color != "string") body.color = undefined
|
||||||
|
|
||||||
|
if (
|
||||||
|
!body.color ||
|
||||||
|
(body.color.toLowerCase().match(/[a-f0-9]+/) ==
|
||||||
|
body.color.toLowerCase() &&
|
||||||
|
body.color.length == 6)
|
||||||
|
) {
|
||||||
|
if (!Account.embed) Account.embed = {}
|
||||||
|
Account.embed.color = body.color || undefined
|
||||||
|
|
||||||
|
await Accounts.save()
|
||||||
|
return ctx.text("custom embed color saved")
|
||||||
|
} else return ServeError(ctx, 400, "invalid hex code")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
router.put(
|
||||||
|
"/embed/size",
|
||||||
|
requiresAccount,
|
||||||
|
requiresPermissions("customize"),
|
||||||
|
async (ctx) => {
|
||||||
|
const Account = ctx.get("account") as Accounts.Account
|
||||||
|
const body = await ctx.req.json()
|
||||||
|
if (typeof body.largeImage != "boolean") {
|
||||||
|
ServeError(ctx, 400, "largeImage must be bool")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Account.embed) Account.embed = {};
|
if (!Account.embed) Account.embed = {}
|
||||||
Account.embed.largeImage = req.body.largeImage
|
Account.embed.largeImage = body.largeImage
|
||||||
|
|
||||||
await Accounts.save()
|
await Accounts.save()
|
||||||
res.send(`custom embed image size saved`)
|
return ctx.text(`custom embed image size saved`)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
return router
|
return router
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { Router } from "express";
|
import { Hono } from "hono";
|
||||||
import Files from "../../../lib/files";
|
import Files from "../../../lib/files";
|
||||||
|
|
||||||
let router = Router()
|
const router = new Hono()
|
||||||
|
|
||||||
module.exports = function(files: Files) {
|
module.exports = function(files: Files) {
|
||||||
return router
|
return router
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { Router } from "express";
|
import { Hono } from "hono"
|
||||||
import Files from "../../../lib/files";
|
import Files from "../../../lib/files"
|
||||||
|
|
||||||
let router = Router()
|
const router = new Hono()
|
||||||
|
|
||||||
module.exports = function(files: Files) {
|
module.exports = function (files: Files) {
|
||||||
return router
|
return router
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue