alright that's enough for now i'm gonna go eat

This commit is contained in:
split / May 2024-03-08 16:42:23 -08:00
parent e9df285ef7
commit b04414aeb9
5 changed files with 89 additions and 332 deletions

View file

@ -28,7 +28,7 @@ const router = new Hono<{
} }
}>() }>()
type UserUpdateParameters = Partial<Accounts.Account & { password: string, newPassword?: string }> type UserUpdateParameters = Partial<Omit<Accounts.Account, "password"> & { password: string, currentPassword?: string }>
type Message = [200 | 400 | 401 | 403 | 501, string] 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>`
@ -52,6 +52,68 @@ const validators: {
email(actor, target, params) { email(actor, target, params) {
return [501, "not implemented"] return [501, "not implemented"]
}, },
password(actor, target, params) {
if (
!params.currentPassword
|| (params.currentPassword && Accounts.password.check(actor.id, params.currentPassword))
) return [401, "current password incorrect"]
if (
typeof params.password != "string"
|| params.password.length < 8
) return [400, "password must be 8 characters or longer"]
if (target.email) {
sendMail(
target.email,
`Your login details have been updated`,
`<b>Hello there!</b> Your password on your account, <span username>${target.username}</span>, has been updated`
+ `${actor != target ? ` by <span username>${actor.username}</span>` : ""}. `
+ `Please update your saved login details accordingly.`
).catch()
}
return Accounts.password.hash(params.password)
},
username(actor, target, params) {
if (!params.currentPassword
|| (params.currentPassword && Accounts.password.check(actor.id, params.currentPassword)))
return [401, "current password incorrect"]
if (
typeof params.username != "string"
|| params.username.length < 3
|| params.username.length > 20
) return [400, "username must be between 3 and 20 characters in length"]
if (Accounts.getFromUsername(params.username))
return [400, "account with this username already exists"]
if ((params.username.match(/[A-Za-z0-9_\-\.]+/) || [])[0] != params.username)
return [400, "username has invalid characters"]
if (target.email) {
sendMail(
target.email,
`Your login details have been updated`,
`<b>Hello there!</b> Your username on your account, <span username>${target.username}</span>, has been updated`
+ `${actor != target ? ` by <span username>${actor.username}</span>` : ""} to <span username>${params.username}</span>. `
+ `Please update your saved login details accordingly.`
).catch()
}
return params.username
},
customCSS(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"]
},
admin(actor, target, params) { admin(actor, target, params) {
if (actor.admin && !target.admin) return params.admin if (actor.admin && !target.admin) return params.admin
else if (!actor.admin) return [400, "cannot promote yourself"] else if (!actor.admin) return [400, "cannot promote yourself"]
@ -80,6 +142,13 @@ router.all("/:user", async (ctx, next) => {
return next() return next()
}) })
function isMessage(object: any): object is Message {
return Array.isArray(object)
&& object.length == 2
&& typeof object[0] == "number"
&& typeof object[1] == "string"
}
export default function (files: Files) { export default function (files: Files) {
router.post("/", async (ctx) => { router.post("/", async (ctx) => {
@ -137,16 +206,6 @@ export default function (files: Files) {
}) })
}) })
router.patch(
"/:user",
requiresAccount,
requiresPermissions("manage"),
async (ctx) => {
let body = await ctx.req.json() as UserUpdateParameters
}
)
router.patch( router.patch(
"/:user", "/:user",
requiresAccount, requiresAccount,
@ -158,11 +217,25 @@ 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")
Object.entries(body).filter(e => e[1]).map(([x]) => { let results: [keyof Accounts.Account, Accounts.Account[keyof Accounts.Account]|Message][] = Object.entries(body).filter(e => e[1] && e[0] !== "currentPassword").map(([x]) =>
if (x in validators) { [
validators[x](actor, target, body as any) 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 allMsgs = results.map(([x,v]) => {
if (isMessage(v))
return v
target[x] = v as never // lol
return [200, "OK"] as Message
}) })
if (allMsgs.length == 1)
return ctx.body(...allMsgs[0]!.reverse() as [Message[1], Message[0]]) // im sorry
else return ctx.json(allMsgs)
} }
) )
@ -193,94 +266,5 @@ export default function (files: Files) {
return ctx.text("account deleted") return ctx.text("account deleted")
}) })
router.put("/:user/password", requiresAccount, noAPIAccess, async (ctx) => {
let acc = ctx.req.param("user") == "me" ? ctx.get("account") : Accounts.getFromId(ctx.req.param("user"))
if (acc != ctx.get("account") && !ctx.get("account")?.admin) return ServeError(ctx, 403, "you are not an administrator")
if (!acc) return ServeError(ctx, 404, "account does not exist")
const body = await ctx.req.json()
const newPassword = body.newPassword
if (
typeof body.password != "string" ||
!Accounts.password.check(acc.id, body.password)
) {
return ServeError(
ctx,
403,
"previous password not supplied"
)
}
if (
typeof newPassword != "string" ||
newPassword.length < 8
) {
return ServeError(
ctx,
400,
"password must be 8 characters or longer"
)
}
Accounts.password.set(acc.id, newPassword)
Accounts.save()
if (acc.email) {
await sendMail(
acc.email,
`Your login details have been updated`,
`<b>Hello there!</b> Your password has been updated. Please update your saved login details accordingly.`
).catch()
return ctx.text("OK")
}
})
router.put("/:user/username", requiresAccount, noAPIAccess, async (ctx) => {
let acc = ctx.req.param("user") == "me" ? ctx.get("account") : Accounts.getFromId(ctx.req.param("user"))
if (acc != ctx.get("account") && !ctx.get("account")?.admin) return ServeError(ctx, 403, "you are not an administrator")
if (!acc) return ServeError(ctx, 404, "account does not exist")
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
}
acc.username = newUsername
Accounts.save()
if (acc.email) {
await sendMail(
acc.email,
`Your login details have been updated`,
`<b>Hello there!</b> Your username has been updated to <span username>${newUsername}</span>. Please update your saved login details accordingly.`
).catch()
return ctx.text("OK")
}
})
return router return router
} }

View file

@ -1,117 +0,0 @@
// Modules
import { writeFile } from "fs/promises"
import { Hono } from "hono"
// Libs
import Files, { id_check_regex } from "../../../lib/files.js"
import * as Accounts from "../../../lib/accounts.js"
import * as Authentication from "../../../lib/auth.js"
import {
getAccount,
noAPIAccess,
requiresAccount,
requiresAdmin,
} from "../../../lib/middleware.js"
import ServeError from "../../../lib/errors.js"
import { sendMail } from "../../../lib/mail.js"
const router = new Hono<{
Variables: {
account?: Accounts.Account
}
}>()
router.use(getAccount, requiresAccount, requiresAdmin)
export default function (files: Files) {
router.patch("/account/:username/password", async (ctx) => {
const Account = ctx.get("account") as Accounts.Account
const body = await ctx.req.json()
const targetUsername = ctx.req.param("username")
const password = body.password
if (typeof password !== "string") return ServeError(ctx, 404, "")
const targetAccount = Accounts.getFromUsername(targetUsername)
if (!targetAccount) return ServeError(ctx, 404, "")
Accounts.password.set(targetAccount.id, password)
Authentication.AuthTokens.filter(
(e) => e.account == targetAccount?.id
).forEach((accountToken) => {
Authentication.invalidate(accountToken.token)
})
if (targetAccount.email) {
await sendMail(
targetAccount.email,
`Your login details have been updated`,
`<b>Hello there!</b> This email is to notify you of a password change that an administrator, <span username>${Account.username}</span>, has initiated. You have been logged out of your devices. Thank you for using monofile.`
).catch()
}
return ctx.text("")
})
router.patch("/account/:username/elevate", (ctx) => {
const targetUsername = ctx.req.param("username")
const targetAccount = Accounts.getFromUsername(targetUsername)
if (!targetAccount) {
return ServeError(ctx, 404, "")
}
targetAccount.admin = true
Accounts.save()
return ctx.text("")
})
router.delete(
"/account/:username/:deleteFiles",
requiresAccount,
noAPIAccess,
async (ctx) => {
const targetUsername = ctx.req.param("username")
const deleteFiles = ctx.req.param("deleteFiles")
const targetAccount = Accounts.getFromUsername(targetUsername)
if (!targetAccount) return ServeError(ctx, 404, "")
const accountId = targetAccount.id
Authentication.AuthTokens.filter(
(e) => e.account == accountId
).forEach((token) => {
Authentication.invalidate(token.token)
})
const deleteAccount = () =>
Accounts.deleteAccount(accountId).then((_) =>
ctx.text("account deleted")
)
if (deleteFiles) {
const Files = targetAccount.files.map((e) => e)
for (let fileId of Files) {
files.unlink(fileId, true).catch((err) => console.error)
}
await writeFile(
process.cwd() + "/.data/files.json",
JSON.stringify(files.files)
)
return deleteAccount()
} else return deleteAccount()
}
)
return router
}

View file

@ -3,13 +3,7 @@
"baseURL": "/api/v1", "baseURL": "/api/v1",
"mount": [ "mount": [
"account", "account",
"admin",
"public",
"file", "file",
"session", "session"
{
"file": "customization",
"to": "/account/customization"
}
] ]
} }

View file

@ -1,96 +0,0 @@
import { Hono } from "hono"
import Files, { id_check_regex } from "../../../lib/files.js"
import * as Accounts from "../../../lib/accounts.js"
import {
getAccount,
requiresAccount,
requiresPermissions,
} from "../../../lib/middleware.js"
import ServeError from "../../../lib/errors.js"
import Configuration from "../../../../../config.json" assert {type:"json"}
const router = new Hono<{
Variables: {
account?: Accounts.Account
}
}>()
router.use(getAccount)
export default function (files: Files) {
router.put(
"/css",
requiresAccount,
requiresPermissions("customize"),
async (ctx) => {
const Account = ctx.get("account") as Accounts.Account
const body = await ctx.req.json()
if (typeof body.fileId != "string") body.fileId = undefined
if (
!body.fileId ||
(body.fileId.match(id_check_regex) == body.fileId &&
body.fileId.length <= Configuration.maxUploadIdLength)
) {
Account.customCSS = body.fileId || undefined
await Accounts.save()
return ctx.text("custom css saved")
} else return ServeError(ctx, 400, "invalid fileId")
}
)
router.get("/css", requiresAccount, async (ctx) => {
const Account = ctx.get("account")
if (Account?.customCSS)
return ctx.redirect(`/file/${Account.customCSS}`)
else return ctx.text("")
})
router.put(
"/embed/color",
requiresAccount,
requiresPermissions("customize"),
async (ctx) => {
const Account = ctx.get("account") as Accounts.Account
const body = await ctx.req.json()
if (typeof body.color != "string") body.color = undefined
if (
!body.color ||
(body.color.toLowerCase().match(/[a-f0-9]+/) ==
body.color.toLowerCase() &&
body.color.length == 6)
) {
if (!Account.embed) Account.embed = {}
Account.embed.color = body.color || undefined
await Accounts.save()
return ctx.text("custom embed color saved")
} else return ServeError(ctx, 400, "invalid hex code")
}
)
router.put(
"/embed/size",
requiresAccount,
requiresPermissions("customize"),
async (ctx) => {
const Account = ctx.get("account") as Accounts.Account
const body = await ctx.req.json()
if (typeof body.largeImage != "boolean") {
ServeError(ctx, 400, "largeImage must be bool")
return
}
if (!Account.embed) Account.embed = {}
Account.embed.largeImage = body.largeImage
await Accounts.save()
return ctx.text(`custom embed image size saved`)
}
)
return router
}

View file

@ -1,8 +0,0 @@
import { Hono } from "hono"
import Files from "../../../lib/files.js"
const router = new Hono()
export default function (files: Files) {
return router
}