mirror of
https://github.com/mollersuite/monofile.git
synced 2024-11-21 13:36:25 -08:00
alright that's enough for now i'm gonna go eat
This commit is contained in:
parent
e9df285ef7
commit
b04414aeb9
|
@ -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]
|
||||
|
||||
// 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) {
|
||||
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) {
|
||||
if (actor.admin && !target.admin) return params.admin
|
||||
else if (!actor.admin) return [400, "cannot promote yourself"]
|
||||
|
@ -80,6 +142,13 @@ router.all("/:user", async (ctx, 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) {
|
||||
|
||||
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(
|
||||
"/:user",
|
||||
requiresAccount,
|
||||
|
@ -158,11 +217,25 @@ export default function (files: Files) {
|
|||
if (Array.isArray(body))
|
||||
return ServeError(ctx, 400, "invalid body")
|
||||
|
||||
Object.entries(body).filter(e => e[1]).map(([x]) => {
|
||||
if (x in validators) {
|
||||
validators[x](actor, target, body as any)
|
||||
}
|
||||
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 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")
|
||||
})
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -3,13 +3,7 @@
|
|||
"baseURL": "/api/v1",
|
||||
"mount": [
|
||||
"account",
|
||||
"admin",
|
||||
"public",
|
||||
"file",
|
||||
"session",
|
||||
{
|
||||
"file": "customization",
|
||||
"to": "/account/customization"
|
||||
}
|
||||
"session"
|
||||
]
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
Loading…
Reference in a new issue