mirror of
https://github.com/mollersuite/monofile.git
synced 2024-11-21 21:36:26 -08:00
Okay that's enough for now
This commit is contained in:
parent
e624e19802
commit
86f5727d83
|
@ -19,7 +19,7 @@
|
|||
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="/api/v1/account/customization/css"
|
||||
href="/api/v1/account/me/css"
|
||||
>
|
||||
|
||||
<link
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="/api/v1/account/customization/css"
|
||||
href="/api/v1/account/me/css"
|
||||
>
|
||||
|
||||
<meta
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
href="/static/assets/icons/icon_temp.svg"
|
||||
/>
|
||||
|
||||
<link rel="stylesheet" href="/api/v1/account/customization/css" />
|
||||
<link rel="stylesheet" href="/api/v1/account/me/css" />
|
||||
|
||||
<meta
|
||||
name="viewport"
|
||||
|
|
|
@ -6,7 +6,7 @@ import { readFile } from "fs/promises"
|
|||
import Files from "./lib/files.js"
|
||||
import { getAccount } from "./lib/middleware.js"
|
||||
import APIRouter from "./routes/api.js"
|
||||
import preview from "./routes/preview.js"
|
||||
import preview from "./routes/api/web/preview.js"
|
||||
import {fileURLToPath} from "url"
|
||||
import {dirname} from "path"
|
||||
import pkg from "../../package.json" assert {type:"json"}
|
||||
|
@ -95,10 +95,6 @@ app.get("/", async (ctx) =>
|
|||
)
|
||||
)
|
||||
|
||||
// serve download page
|
||||
|
||||
app.get("/download/:fileId", getAccount, preview(files))
|
||||
|
||||
/*
|
||||
routes should be in this order:
|
||||
|
||||
|
|
|
@ -11,8 +11,6 @@ let mailConfig = config.mail,
|
|||
},
|
||||
})
|
||||
|
||||
// lazy but
|
||||
|
||||
/**
|
||||
* @description Sends an email
|
||||
* @param to Target email address
|
||||
|
|
|
@ -74,7 +74,6 @@ export default class APIRouter {
|
|||
async loadAPIMethods() {
|
||||
let files = await readdir(APIDirectory)
|
||||
for (let version of files) {
|
||||
/// temporary (hopefully). need to figure out something else for this
|
||||
let def = JSON.parse(
|
||||
(
|
||||
await readFile(
|
||||
|
|
|
@ -31,12 +31,12 @@ export default function (files: Files) {
|
|||
typeof body.target !== "string" ||
|
||||
typeof body.password !== "string"
|
||||
) {
|
||||
return ctx.status(404)
|
||||
return ctx.text("not found", 404)
|
||||
}
|
||||
|
||||
let targetAccount = Accounts.getFromUsername(body.target)
|
||||
if (!targetAccount) {
|
||||
return ctx.status(404)
|
||||
return ctx.text("not found", 404)
|
||||
}
|
||||
|
||||
Accounts.password.set(targetAccount.id, body.password)
|
||||
|
@ -53,7 +53,7 @@ export default function (files: Files) {
|
|||
`<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))
|
||||
.catch(() => ctx.text("err while sending email", 500))
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -62,12 +62,12 @@ export default function (files: Files) {
|
|||
let acc = ctx.get("account") as Accounts.Account
|
||||
|
||||
if (typeof body.target !== "string") {
|
||||
return ctx.status(404)
|
||||
return ctx.text("not found", 404)
|
||||
}
|
||||
|
||||
let targetAccount = Accounts.getFromUsername(body.target)
|
||||
if (!targetAccount) {
|
||||
return ctx.status(404)
|
||||
return ctx.text("not found", 404)
|
||||
}
|
||||
|
||||
Accounts.save()
|
||||
|
@ -77,32 +77,31 @@ export default function (files: Files) {
|
|||
adminRoutes.post("/delete", async (ctx) => {
|
||||
const body = await ctx.req.json()
|
||||
if (typeof body.target !== "string") {
|
||||
return ctx.status(404)
|
||||
return ctx.text("not found", 404)
|
||||
}
|
||||
|
||||
let targetFile = files.files[body.target]
|
||||
|
||||
if (!targetFile) {
|
||||
return ctx.status(404)
|
||||
return ctx.text("not found", 404)
|
||||
}
|
||||
|
||||
return files
|
||||
.unlink(body.target)
|
||||
.then(() => ctx.status(200))
|
||||
.catch(() => ctx.status(500))
|
||||
.finally(() => ctx.status(200))
|
||||
.then(() => ctx.text("ok", 200))
|
||||
.catch(() => ctx.text("err", 500))
|
||||
})
|
||||
|
||||
adminRoutes.post("/delete_account", async (ctx) => {
|
||||
let acc = ctx.get("account") as Accounts.Account
|
||||
const body = await ctx.req.json()
|
||||
if (typeof body.target !== "string") {
|
||||
return ctx.status(404)
|
||||
return ctx.text("not found", 404)
|
||||
}
|
||||
|
||||
let targetAccount = Accounts.getFromUsername(body.target)
|
||||
if (!targetAccount) {
|
||||
return ctx.status(404)
|
||||
return ctx.text("not found", 404)
|
||||
}
|
||||
|
||||
let accId = targetAccount.id
|
||||
|
@ -149,12 +148,12 @@ export default function (files: Files) {
|
|||
adminRoutes.post("/transfer", async (ctx) => {
|
||||
const body = await ctx.req.json()
|
||||
if (typeof body.target !== "string" || typeof body.owner !== "string") {
|
||||
return ctx.status(404)
|
||||
return ctx.text("not found", 404)
|
||||
}
|
||||
|
||||
let targetFile = files.files[body.target]
|
||||
if (!targetFile) {
|
||||
return ctx.status(404)
|
||||
return ctx.text("not found", 404)
|
||||
}
|
||||
|
||||
let newOwner = Accounts.getFromUsername(body.owner || "")
|
||||
|
@ -173,21 +172,21 @@ export default function (files: Files) {
|
|||
}
|
||||
targetFile.owner = newOwner ? newOwner.id : undefined
|
||||
|
||||
files
|
||||
return files
|
||||
.write()
|
||||
.then(() => ctx.status(200))
|
||||
.catch(() => ctx.status(500))
|
||||
.then(() => ctx.text("ok", 200))
|
||||
.catch(() => ctx.text("error", 500))
|
||||
})
|
||||
|
||||
adminRoutes.post("/idchange", async (ctx) => {
|
||||
const body = await ctx.req.json()
|
||||
if (typeof body.target !== "string" || typeof body.new !== "string") {
|
||||
return ctx.status(400)
|
||||
return ctx.text("inappropriate body", 400)
|
||||
}
|
||||
|
||||
let targetFile = files.files[body.target]
|
||||
if (!targetFile) {
|
||||
return ctx.status(404)
|
||||
return ctx.text("not found", 404)
|
||||
}
|
||||
|
||||
if (files.files[body.new]) {
|
||||
|
|
|
@ -105,7 +105,6 @@ export default function (files: Files) {
|
|||
}
|
||||
}
|
||||
)
|
||||
// upload handlers
|
||||
|
||||
primaryApi.post(
|
||||
"/upload",
|
||||
|
|
|
@ -33,28 +33,54 @@ type Message = [200 | 400 | 401 | 403 | 501, string]
|
|||
|
||||
// there's probably a less stupid way to do this than `K in keyof Pick<UserUpdateParameters, T>`
|
||||
// @Jack5079 make typings better if possible
|
||||
const validators: {
|
||||
[T in keyof Partial<Accounts.Account>]:
|
||||
|
||||
type Validator<T extends keyof Partial<Accounts.Account>, ValueNotNull extends boolean> =
|
||||
/**
|
||||
* @param actor The account performing this action
|
||||
* @param target The target account for this action
|
||||
* @param params Changes being patched in by the user
|
||||
*/
|
||||
(actor: Accounts.Account, target: Accounts.Account, params: UserUpdateParameters & {
|
||||
(actor: Accounts.Account, target: Accounts.Account, params: UserUpdateParameters & (ValueNotNull extends true ? {
|
||||
[K in keyof Pick<UserUpdateParameters, T>]-? : UserUpdateParameters[K]
|
||||
}) => Accounts.Account[T] | Message
|
||||
} : {})) => Accounts.Account[T] | Message
|
||||
|
||||
// this type is so stupid stg
|
||||
interface ValidatorWithSettings<T extends keyof Partial<Accounts.Account>> {
|
||||
acceptsNull?: boolean,
|
||||
validator: Validator<T, this["acceptsNull"] extends true ? true : false> // i give upp ill fix this later
|
||||
}
|
||||
|
||||
const validators: {
|
||||
[T in keyof Partial<Accounts.Account>]:
|
||||
Validator<T, true> | ValidatorWithSettings<T>
|
||||
} = {
|
||||
defaultFileVisibility(actor, target, params) {
|
||||
if (["public", "private", "anonymous"].includes(params.defaultFileVisibility))
|
||||
return params.defaultFileVisibility
|
||||
else return [400, "invalid file visibility"]
|
||||
},
|
||||
email(actor, target, params) {
|
||||
return [501, "not implemented"]
|
||||
email: {
|
||||
acceptsNull: true,
|
||||
validator: (actor, target, params) => {
|
||||
if (!params.currentPassword // actor on purpose here to allow admins
|
||||
|| (params.currentPassword && Accounts.password.check(actor.id, params.currentPassword)))
|
||||
return [401, "current password incorrect"]
|
||||
|
||||
if (!params.email) {
|
||||
if (target.email) {
|
||||
sendMail(
|
||||
target.email,
|
||||
`Email disconnected`,
|
||||
`<b>Hello there!</b> Your email address (<span code>${target.email}</span>) has been disconnected from the monofile account <span username>${target.username}</span>. Thank you for using monofile.`
|
||||
).catch()
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
},
|
||||
password(actor, target, params) {
|
||||
if (
|
||||
!params.currentPassword
|
||||
!params.currentPassword // actor on purpose here to allow admins
|
||||
|| (params.currentPassword && Accounts.password.check(actor.id, params.currentPassword))
|
||||
) return [401, "current password incorrect"]
|
||||
|
||||
|
@ -77,7 +103,7 @@ const validators: {
|
|||
|
||||
},
|
||||
username(actor, target, params) {
|
||||
if (!params.currentPassword
|
||||
if (!params.currentPassword // actor on purpose here to allow admins
|
||||
|| (params.currentPassword && Accounts.password.check(actor.id, params.currentPassword)))
|
||||
return [401, "current password incorrect"]
|
||||
|
||||
|
@ -106,13 +132,30 @@ const validators: {
|
|||
return params.username
|
||||
|
||||
},
|
||||
customCSS(actor, target, params) {
|
||||
customCSS: {
|
||||
acceptsNull: true,
|
||||
validator: (actor, target, params) => {
|
||||
if (
|
||||
!params.customCSS ||
|
||||
(params.customCSS.match(id_check_regex)?.[0] == params.customCSS &&
|
||||
params.customCSS.length <= Configuration.maxUploadIdLength)
|
||||
) return params.customCSS
|
||||
else return [400, "bad file id"]
|
||||
}
|
||||
},
|
||||
embed(actor, target, params) {
|
||||
if (params.embed.color === undefined) {
|
||||
params.embed.color = target.embed?.color
|
||||
} else if (!((params.embed.color.toLowerCase().match(/[a-f0-9]+/)?.[0] ==
|
||||
params.embed.color.toLowerCase() &&
|
||||
params.embed.color.length == 6) || params.embed.color == null)) return [400, "bad embed color"]
|
||||
|
||||
|
||||
if (params.embed.largeImage === undefined) {
|
||||
params.embed.largeImage = target.embed?.largeImage
|
||||
} else params.embed.largeImage = Boolean(params.embed.largeImage)
|
||||
|
||||
return params.embed
|
||||
},
|
||||
admin(actor, target, params) {
|
||||
if (actor.admin && !target.admin) return params.admin
|
||||
|
@ -217,24 +260,41 @@ export default function (files: Files) {
|
|||
if (Array.isArray(body))
|
||||
return ServeError(ctx, 400, "invalid body")
|
||||
|
||||
let results: [keyof Accounts.Account, Accounts.Account[keyof Accounts.Account]|Message][] = Object.entries(body).filter(e => e[1] && e[0] !== "currentPassword").map(([x]) =>
|
||||
[
|
||||
x as keyof Accounts.Account,
|
||||
x in validators
|
||||
? validators[x as keyof Accounts.Account]!(actor, target, body as any)
|
||||
: [400, `the ${x} parameter cannot be set or is not a valid parameter`] as Message
|
||||
]
|
||||
)
|
||||
let results: ([keyof Accounts.Account, Accounts.Account[keyof Accounts.Account]]|Message)[] =
|
||||
(Object.entries(body)
|
||||
.filter(e => e[0] !== "currentPassword") as [keyof Accounts.Account, UserUpdateParameters[keyof Accounts.Account]][])
|
||||
.map(([x, v]) => {
|
||||
if (!validators[x])
|
||||
return [400, `the ${x} parameter cannot be set or is not a valid parameter`] as Message
|
||||
|
||||
let allMsgs = results.map(([x,v]) => {
|
||||
let validator =
|
||||
(typeof validators[x] == "object"
|
||||
? validators[x]
|
||||
: {
|
||||
validator: validators[x] as Validator<typeof x, false>,
|
||||
acceptsNull: false
|
||||
}) as ValidatorWithSettings<typeof x>
|
||||
|
||||
if (!validator.acceptsNull && !v)
|
||||
return [400, `the ${x} validator does not accept null values`] as Message
|
||||
|
||||
return [
|
||||
x,
|
||||
validator.validator(actor, target, body)
|
||||
] as [keyof Accounts.Account, Accounts.Account[keyof Accounts.Account]]
|
||||
})
|
||||
|
||||
let allMsgs = results.map((v) => {
|
||||
if (isMessage(v))
|
||||
return v
|
||||
target[x] = v as never // lol
|
||||
target[v[0]] = v[1] as never // lol
|
||||
return [200, "OK"] as Message
|
||||
})
|
||||
|
||||
await Accounts.save()
|
||||
|
||||
if (allMsgs.length == 1)
|
||||
return ctx.body(...allMsgs[0]!.reverse() as [Message[1], Message[0]]) // im sorry
|
||||
return ctx.text(...allMsgs[0]!.reverse() as [Message[1], Message[0]]) // im sorry
|
||||
else return ctx.json(allMsgs)
|
||||
}
|
||||
)
|
||||
|
@ -264,7 +324,33 @@ export default function (files: Files) {
|
|||
return ctx.text("account deleted")
|
||||
})
|
||||
|
||||
router.get("/:user")
|
||||
router.get("/:user", requiresAccount, async (ctx) => {
|
||||
let acc = ctx.get("target")
|
||||
let sessionToken = auth.tokenFor(ctx)!
|
||||
|
||||
return ctx.json({
|
||||
...acc,
|
||||
password: undefined,
|
||||
email:
|
||||
auth.getType(sessionToken) == "User" ||
|
||||
auth.getPermissions(sessionToken)?.includes("email")
|
||||
? acc.email
|
||||
: undefined,
|
||||
activeSessions: auth.AuthTokens.filter(
|
||||
(e) =>
|
||||
e.type != "App" &&
|
||||
e.account == acc.id &&
|
||||
(e.expire > Date.now() || !e.expire)
|
||||
).length,
|
||||
})
|
||||
})
|
||||
|
||||
router.get("/css", async (ctx) => {
|
||||
let acc = ctx.get('account')
|
||||
if (acc?.customCSS)
|
||||
return ctx.redirect(`/file/${acc.customCSS}`)
|
||||
else return ctx.text("")
|
||||
})
|
||||
|
||||
return router
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import * as Accounts from "../../../lib/accounts.js"
|
|||
import * as auth from "../../../lib/auth.js"
|
||||
import {
|
||||
getAccount,
|
||||
requiresAccount
|
||||
} from "../../../lib/middleware.js"
|
||||
import ServeError from "../../../lib/errors.js"
|
||||
|
||||
|
@ -53,6 +54,15 @@ export default function (files: Files) {
|
|||
ctx.status(200)
|
||||
})
|
||||
|
||||
router.get("/", requiresAccount, ctx => {
|
||||
let sessionToken = auth.tokenFor(ctx)
|
||||
return ctx.json({
|
||||
expiry: auth.AuthTokens.find(
|
||||
(e) => e.token == sessionToken
|
||||
)?.expire,
|
||||
})
|
||||
})
|
||||
|
||||
router.delete("/", (ctx) => {
|
||||
if (!auth.validate(getCookie(ctx, "auth")!)) {
|
||||
return ServeError(ctx, 401, "not logged in")
|
||||
|
|
7
src/server/routes/api/web/api.json
Normal file
7
src/server/routes/api/web/api.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"name": "web",
|
||||
"baseURL": "/",
|
||||
"mount": [
|
||||
{ "file": "preview", "to": "/download" }
|
||||
]
|
||||
}
|
|
@ -1,13 +1,18 @@
|
|||
import fs from "fs/promises"
|
||||
import bytes from "bytes"
|
||||
import ServeError from "../lib/errors.js"
|
||||
import * as Accounts from "../lib/accounts.js"
|
||||
import type { Handler } from "hono"
|
||||
import type Files from "../lib/files.js"
|
||||
import pkg from "../../../package.json" assert {type:"json"}
|
||||
import ServeError from "../../../lib/errors.js"
|
||||
import * as Accounts from "../../../lib/accounts.js"
|
||||
import type Files from "../../../lib/files.js"
|
||||
import pkg from "../../../../../package.json" assert {type:"json"}
|
||||
import { Hono } from "hono"
|
||||
export let router = new Hono<{
|
||||
Variables: {
|
||||
account: Accounts.Account
|
||||
}
|
||||
}>()
|
||||
|
||||
export default (files: Files): Handler =>
|
||||
async (ctx) => {
|
||||
export default function (files: Files) {
|
||||
router.get("/:fileId", async (ctx) => {
|
||||
let acc = ctx.get("account") as Accounts.Account
|
||||
const fileId = ctx.req.param("fileId")
|
||||
const host = ctx.req.header("Host")
|
||||
|
@ -104,4 +109,7 @@ export default (files: Files): Handler =>
|
|||
} else {
|
||||
ServeError(ctx, 404, "file not found")
|
||||
}
|
||||
})
|
||||
|
||||
return router
|
||||
}
|
Loading…
Reference in a new issue