mirror of
https://github.com/mollersuite/monofile.git
synced 2024-11-21 21:36:26 -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]
|
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
"baseURL": "/api/v1",
|
||||||
"mount": [
|
"mount": [
|
||||||
"account",
|
"account",
|
||||||
"admin",
|
|
||||||
"public",
|
|
||||||
"file",
|
"file",
|
||||||
"session",
|
"session"
|
||||||
{
|
|
||||||
"file": "customization",
|
|
||||||
"to": "/account/customization"
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -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