Okay that's enough for now

This commit is contained in:
May 2024-03-08 20:02:27 -08:00
parent e624e19802
commit 86f5727d83
12 changed files with 173 additions and 71 deletions

View file

@ -19,7 +19,7 @@
<link <link
rel="stylesheet" rel="stylesheet"
href="/api/v1/account/customization/css" href="/api/v1/account/me/css"
> >
<link <link

View file

@ -17,7 +17,7 @@
<link <link
rel="stylesheet" rel="stylesheet"
href="/api/v1/account/customization/css" href="/api/v1/account/me/css"
> >
<meta <meta

View file

@ -15,7 +15,7 @@
href="/static/assets/icons/icon_temp.svg" 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 <meta
name="viewport" name="viewport"

View file

@ -6,7 +6,7 @@ import { readFile } from "fs/promises"
import Files from "./lib/files.js" import Files from "./lib/files.js"
import { getAccount } from "./lib/middleware.js" import { getAccount } from "./lib/middleware.js"
import APIRouter from "./routes/api.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 {fileURLToPath} from "url"
import {dirname} from "path" import {dirname} from "path"
import pkg from "../../package.json" assert {type:"json"} 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: routes should be in this order:

View file

@ -11,8 +11,6 @@ let mailConfig = config.mail,
}, },
}) })
// lazy but
/** /**
* @description Sends an email * @description Sends an email
* @param to Target email address * @param to Target email address
@ -35,4 +33,4 @@ export function sendMail(to: string, subject: string, content: string) {
`<span style="font-family:monospace;padding:3px 5px 3px 5px;border-radius:8px;background-color:#1C1C1C;color:#DDDDDD;">` `<span style="font-family:monospace;padding:3px 5px 3px 5px;border-radius:8px;background-color:#1C1C1C;color:#DDDDDD;">`
)}<br><br><span style="opacity:0.5">If you do not believe that you are the intended recipient of this email, please disregard this message.</span>`, )}<br><br><span style="opacity:0.5">If you do not believe that you are the intended recipient of this email, please disregard this message.</span>`,
}) })
} }

View file

@ -74,7 +74,6 @@ export default class APIRouter {
async loadAPIMethods() { async loadAPIMethods() {
let files = await readdir(APIDirectory) let files = await readdir(APIDirectory)
for (let version of files) { for (let version of files) {
/// temporary (hopefully). need to figure out something else for this
let def = JSON.parse( let def = JSON.parse(
( (
await readFile( await readFile(

View file

@ -31,12 +31,12 @@ export default function (files: Files) {
typeof body.target !== "string" || typeof body.target !== "string" ||
typeof body.password !== "string" typeof body.password !== "string"
) { ) {
return ctx.status(404) return ctx.text("not found", 404)
} }
let targetAccount = Accounts.getFromUsername(body.target) let targetAccount = Accounts.getFromUsername(body.target)
if (!targetAccount) { if (!targetAccount) {
return ctx.status(404) return ctx.text("not found", 404)
} }
Accounts.password.set(targetAccount.id, body.password) 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.` `<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")) .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 let acc = ctx.get("account") as Accounts.Account
if (typeof body.target !== "string") { if (typeof body.target !== "string") {
return ctx.status(404) return ctx.text("not found", 404)
} }
let targetAccount = Accounts.getFromUsername(body.target) let targetAccount = Accounts.getFromUsername(body.target)
if (!targetAccount) { if (!targetAccount) {
return ctx.status(404) return ctx.text("not found", 404)
} }
Accounts.save() Accounts.save()
@ -77,32 +77,31 @@ export default function (files: Files) {
adminRoutes.post("/delete", async (ctx) => { adminRoutes.post("/delete", async (ctx) => {
const body = await ctx.req.json() const body = await ctx.req.json()
if (typeof body.target !== "string") { if (typeof body.target !== "string") {
return ctx.status(404) return ctx.text("not found", 404)
} }
let targetFile = files.files[body.target] let targetFile = files.files[body.target]
if (!targetFile) { if (!targetFile) {
return ctx.status(404) return ctx.text("not found", 404)
} }
return files return files
.unlink(body.target) .unlink(body.target)
.then(() => ctx.status(200)) .then(() => ctx.text("ok", 200))
.catch(() => ctx.status(500)) .catch(() => ctx.text("err", 500))
.finally(() => ctx.status(200))
}) })
adminRoutes.post("/delete_account", async (ctx) => { adminRoutes.post("/delete_account", async (ctx) => {
let acc = ctx.get("account") as Accounts.Account let acc = ctx.get("account") as Accounts.Account
const body = await ctx.req.json() const body = await ctx.req.json()
if (typeof body.target !== "string") { if (typeof body.target !== "string") {
return ctx.status(404) return ctx.text("not found", 404)
} }
let targetAccount = Accounts.getFromUsername(body.target) let targetAccount = Accounts.getFromUsername(body.target)
if (!targetAccount) { if (!targetAccount) {
return ctx.status(404) return ctx.text("not found", 404)
} }
let accId = targetAccount.id let accId = targetAccount.id
@ -149,12 +148,12 @@ export default function (files: Files) {
adminRoutes.post("/transfer", async (ctx) => { adminRoutes.post("/transfer", async (ctx) => {
const body = await ctx.req.json() const body = await ctx.req.json()
if (typeof body.target !== "string" || typeof body.owner !== "string") { 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] let targetFile = files.files[body.target]
if (!targetFile) { if (!targetFile) {
return ctx.status(404) return ctx.text("not found", 404)
} }
let newOwner = Accounts.getFromUsername(body.owner || "") let newOwner = Accounts.getFromUsername(body.owner || "")
@ -173,21 +172,21 @@ export default function (files: Files) {
} }
targetFile.owner = newOwner ? newOwner.id : undefined targetFile.owner = newOwner ? newOwner.id : undefined
files return files
.write() .write()
.then(() => ctx.status(200)) .then(() => ctx.text("ok", 200))
.catch(() => ctx.status(500)) .catch(() => ctx.text("error", 500))
}) })
adminRoutes.post("/idchange", async (ctx) => { adminRoutes.post("/idchange", async (ctx) => {
const body = await ctx.req.json() const body = await ctx.req.json()
if (typeof body.target !== "string" || typeof body.new !== "string") { 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] let targetFile = files.files[body.target]
if (!targetFile) { if (!targetFile) {
return ctx.status(404) return ctx.text("not found", 404)
} }
if (files.files[body.new]) { if (files.files[body.new]) {

View file

@ -105,7 +105,6 @@ export default function (files: Files) {
} }
} }
) )
// upload handlers
primaryApi.post( primaryApi.post(
"/upload", "/upload",

View file

@ -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>` // there's probably a less stupid way to do this than `K in keyof Pick<UserUpdateParameters, T>`
// @Jack5079 make typings better if possible // @Jack5079 make typings better if possible
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 & (ValueNotNull extends true ? {
[K in keyof Pick<UserUpdateParameters, T>]-? : UserUpdateParameters[K]
} : {})) => 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: { const validators: {
[T in keyof Partial<Accounts.Account>]: [T in keyof Partial<Accounts.Account>]:
/** Validator<T, true> | ValidatorWithSettings<T>
* @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 & {
[K in keyof Pick<UserUpdateParameters, T>]-? : UserUpdateParameters[K]
}) => Accounts.Account[T] | Message
} = { } = {
defaultFileVisibility(actor, target, params) { defaultFileVisibility(actor, target, params) {
if (["public", "private", "anonymous"].includes(params.defaultFileVisibility)) if (["public", "private", "anonymous"].includes(params.defaultFileVisibility))
return params.defaultFileVisibility return params.defaultFileVisibility
else return [400, "invalid file visibility"] else return [400, "invalid file visibility"]
}, },
email(actor, target, params) { email: {
return [501, "not implemented"] 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) { password(actor, target, params) {
if ( if (
!params.currentPassword !params.currentPassword // actor on purpose here to allow admins
|| (params.currentPassword && Accounts.password.check(actor.id, params.currentPassword)) || (params.currentPassword && Accounts.password.check(actor.id, params.currentPassword))
) return [401, "current password incorrect"] ) return [401, "current password incorrect"]
@ -77,7 +103,7 @@ const validators: {
}, },
username(actor, target, params) { 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))) || (params.currentPassword && Accounts.password.check(actor.id, params.currentPassword)))
return [401, "current password incorrect"] return [401, "current password incorrect"]
@ -106,13 +132,30 @@ const validators: {
return params.username return params.username
}, },
customCSS(actor, target, params) { customCSS: {
if ( acceptsNull: true,
!params.customCSS || validator: (actor, target, params) => {
(params.customCSS.match(id_check_regex)?.[0] == params.customCSS && if (
params.customCSS.length <= Configuration.maxUploadIdLength) !params.customCSS ||
) return params.customCSS (params.customCSS.match(id_check_regex)?.[0] == params.customCSS &&
else return [400, "bad file id"] 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) { admin(actor, target, params) {
if (actor.admin && !target.admin) return params.admin if (actor.admin && !target.admin) return params.admin
@ -217,24 +260,41 @@ export default function (files: Files) {
if (Array.isArray(body)) if (Array.isArray(body))
return ServeError(ctx, 400, "invalid 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]) => let results: ([keyof Accounts.Account, Accounts.Account[keyof Accounts.Account]]|Message)[] =
[ (Object.entries(body)
x as keyof Accounts.Account, .filter(e => e[0] !== "currentPassword") as [keyof Accounts.Account, UserUpdateParameters[keyof Accounts.Account]][])
x in validators .map(([x, v]) => {
? validators[x as keyof Accounts.Account]!(actor, target, body as any) if (!validators[x])
: [400, `the ${x} parameter cannot be set or is not a valid parameter`] as Message 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)) if (isMessage(v))
return v return v
target[x] = v as never // lol target[v[0]] = v[1] as never // lol
return [200, "OK"] as Message return [200, "OK"] as Message
}) })
await Accounts.save()
if (allMsgs.length == 1) 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) else return ctx.json(allMsgs)
} }
) )
@ -264,7 +324,33 @@ export default function (files: Files) {
return ctx.text("account deleted") 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 return router
} }

View file

@ -11,6 +11,7 @@ import * as Accounts from "../../../lib/accounts.js"
import * as auth from "../../../lib/auth.js" import * as auth from "../../../lib/auth.js"
import { import {
getAccount, getAccount,
requiresAccount
} from "../../../lib/middleware.js" } from "../../../lib/middleware.js"
import ServeError from "../../../lib/errors.js" import ServeError from "../../../lib/errors.js"
@ -53,6 +54,15 @@ export default function (files: Files) {
ctx.status(200) 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) => { router.delete("/", (ctx) => {
if (!auth.validate(getCookie(ctx, "auth")!)) { if (!auth.validate(getCookie(ctx, "auth")!)) {
return ServeError(ctx, 401, "not logged in") return ServeError(ctx, 401, "not logged in")

View file

@ -0,0 +1,7 @@
{
"name": "web",
"baseURL": "/",
"mount": [
{ "file": "preview", "to": "/download" }
]
}

View file

@ -1,13 +1,18 @@
import fs from "fs/promises" import fs from "fs/promises"
import bytes from "bytes" import bytes from "bytes"
import ServeError from "../lib/errors.js" import ServeError from "../../../lib/errors.js"
import * as Accounts from "../lib/accounts.js" import * as Accounts from "../../../lib/accounts.js"
import type { Handler } from "hono" import type Files from "../../../lib/files.js"
import type Files from "../lib/files.js" import pkg from "../../../../../package.json" assert {type:"json"}
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 => export default function (files: Files) {
async (ctx) => { router.get("/:fileId", async (ctx) => {
let acc = ctx.get("account") as Accounts.Account let acc = ctx.get("account") as Accounts.Account
const fileId = ctx.req.param("fileId") const fileId = ctx.req.param("fileId")
const host = ctx.req.header("Host") const host = ctx.req.header("Host")
@ -104,4 +109,7 @@ export default (files: Files): Handler =>
} else { } else {
ServeError(ctx, 404, "file not found") ServeError(ctx, 404, "file not found")
} }
} })
return router
}