i think we're done actually

This commit is contained in:
split / May 2024-04-30 23:40:42 -07:00
parent 8a26ace11f
commit f0a2450082
7 changed files with 107 additions and 138 deletions

View file

@ -9,7 +9,7 @@ import { fileURLToPath } from "url"
import { dirname } from "path" import { dirname } from "path"
import config from "./lib/config.js" import config from "./lib/config.js"
const app = new Hono() const app = new Hono({strict: false})
app.get( app.get(
"/static/assets/*", "/static/assets/*",
@ -78,7 +78,7 @@ apiRouter.loadAPIMethods().then(() => {
app.fetch( app.fetch(
new Request( new Request(
new URL( new URL(
"/api/v1/info", "/api/v1",
ctx.req.raw.url ctx.req.raw.url
).href, ).href,
ctx.req.raw ctx.req.raw

View file

@ -10,6 +10,7 @@ import "dotenv/config"
import * as Accounts from "./accounts.js" import * as Accounts from "./accounts.js"
import { z } from "zod" import { z } from "zod"
import * as schemas from "./schemas/files.js" import * as schemas from "./schemas/files.js"
import { issuesToMessage } from "./middleware.js"
export let alphanum = Array.from( export let alphanum = Array.from(
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890" "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
@ -504,7 +505,7 @@ export class UploadStream extends Writable {
let check = schemas.FileId.safeParse(id); let check = schemas.FileId.safeParse(id);
if (!check.success) if (!check.success)
return this.destroy(new WebError(400, check.error.message)) return this.destroy(new WebError(400, issuesToMessage(check.error.issues)))
if (this.files.files[id] && this.files.files[id].owner != this.owner) if (this.files.files[id] && this.files.files[id].owner != this.owner)
return this.destroy(new WebError(403, "you don't own this file")) return this.destroy(new WebError(403, "you don't own this file"))

View file

@ -3,7 +3,7 @@ import type { Context, Handler as RequestHandler } from "hono"
import ServeError from "../lib/errors.js" import ServeError from "../lib/errors.js"
import * as auth from "./auth.js" import * as auth from "./auth.js"
import { setCookie } from "hono/cookie" import { setCookie } from "hono/cookie"
import { ZodObject } from "zod" import { z } from "zod"
/** /**
* @description Middleware which adds an account, if any, to ctx.get("account") * @description Middleware which adds an account, if any, to ctx.get("account")
@ -38,7 +38,6 @@ export const requiresAdmin: RequestHandler = function (ctx, next) {
* @param tokenPermissions Permissions which your route requires. * @param tokenPermissions Permissions which your route requires.
* @returns Express middleware * @returns Express middleware
*/ */
export const requiresPermissions = function ( export const requiresPermissions = function (
...tokenPermissions: auth.TokenPermission[] ...tokenPermissions: auth.TokenPermission[]
): RequestHandler { ): RequestHandler {
@ -94,6 +93,18 @@ export const assertAPI = function (
} }
} }
export const issuesToMessage = function(issues: z.ZodIssue[]) {
return issues.map(e => `${e.path}: ${e.code} :: ${e.message}`).join("; ")
}
export const scheme = function(scheme: z.ZodTypeAny): RequestHandler {
return async function(ctx, next) {
let chk = scheme.safeParse(await ctx.req.json())
if (chk.success) return next()
else return ServeError(ctx, 400, issuesToMessage(chk.error.issues))
}
}
// Not really middleware but a utility // Not really middleware but a utility
export const login = (ctx: Context, account: string) => setCookie(ctx, "auth", auth.create(account, 3 * 24 * 60 * 60 * 1000), { export const login = (ctx: Context, account: string) => setCookie(ctx, "auth", auth.create(account, 3 * 24 * 60 * 60 * 1000), {
@ -101,12 +112,4 @@ export const login = (ctx: Context, account: string) => setCookie(ctx, "auth", a
sameSite: "Strict", sameSite: "Strict",
secure: true, secure: true,
httpOnly: true httpOnly: true
}) })
export const scheme = function(scheme: ZodObject<any>): RequestHandler {
return function(ctx, next) {
let chk = scheme.safeParse(ctx.req.json())
if (chk.success) next()
else ServeError(ctx, 400, chk.error.message)
}
}

View file

@ -11,6 +11,7 @@ import * as auth from "../../../lib/auth.js"
import { import {
assertAPI, assertAPI,
getAccount, getAccount,
issuesToMessage,
login, login,
noAPIAccess, noAPIAccess,
requiresAccount, requiresAccount,
@ -43,8 +44,7 @@ type Message = [200 | 400 | 401 | 403 | 429 | 501, string]
// @Jack5079 make typings better if possible // @Jack5079 make typings better if possible
type Validator< type Validator<
T extends keyof Partial<Accounts.Account>, T extends keyof Partial<Accounts.Account>
ValueNotNull extends boolean,
> = > =
/** /**
* @param actor The account performing this action * @param actor The account performing this action
@ -55,38 +55,33 @@ type Validator<
actor: Accounts.Account, actor: Accounts.Account,
target: Accounts.Account, target: Accounts.Account,
params: UserUpdateParameters & params: UserUpdateParameters &
(ValueNotNull extends true {
? { [K in keyof Pick<
[K in keyof Pick< UserUpdateParameters,
UserUpdateParameters, T
T >]-?: UserUpdateParameters[K]
>]-?: UserUpdateParameters[K] },
}
: {}),
ctx: Context ctx: Context
) => Accounts.Account[T] | Message ) => Accounts.Account[T] | Message
// this type is so stupid stg type SchemedValidator<
type ValidatorWithSettings<T extends keyof Partial<Accounts.Account>> = T extends keyof Partial<Accounts.Account>
| { > = {
acceptsNull: true validator: Validator<T>,
validator: Validator<T, false> schema: z.ZodTypeAny
} }
| {
acceptsNull?: false
validator: Validator<T, true>
}
const validators: { const validators: {
[T in keyof Partial<Accounts.Account>]: [T in keyof Partial<Accounts.Account>]: SchemedValidator<T>
| Validator<T, true>
| ValidatorWithSettings<T>
} = { } = {
defaultFileVisibility(actor, target, params) { defaultFileVisibility: {
return params.defaultFileVisibility schema: FileSchemas.FileVisibility,
validator: (actor, target, params) => {
return params.defaultFileVisibility
}
}, },
email: { email: {
acceptsNull: true, schema: AccountSchemas.Account.shape.email.optional(),
validator: (actor, target, params, ctx) => { validator: (actor, target, params, ctx) => {
if ( if (
!params.currentPassword || // actor on purpose here to allow admins !params.currentPassword || // actor on purpose here to allow admins
@ -139,82 +134,62 @@ const validators: {
return [200, "please check your inbox"] return [200, "please check your inbox"]
}, },
}, },
password(actor, target, params) { password: {
if ( schema: AccountSchemas.StringPassword,
!params.currentPassword || // actor on purpose here to allow admins
(params.currentPassword &&
Accounts.password.check(actor.id, params.currentPassword))
)
return [401, "current password incorrect"]
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 || // actor on purpose here to allow admins
(params.currentPassword &&
Accounts.password.check(actor.id, params.currentPassword))
)
return [401, "current password incorrect"]
if (Accounts.getFromUsername(params.username))
return [400, "account with this username already exists"]
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: {
acceptsNull: true,
validator: (actor, target, params) => { validator: (actor, target, params) => {
if (FileSchemas.FileId.safeParse(params.customCSS).success) if (
return params.customCSS !params.currentPassword || // actor on purpose here to allow admins
else return [400, "bad file id"] (params.currentPassword &&
}, Accounts.password.check(actor.id, params.currentPassword))
},
embed(actor, target, params) {
if (typeof params.embed !== "object")
return [400, "must use an object for embed"]
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 [401, "current password incorrect"]
return [400, "bad embed color"]
if (params.embed.largeImage === undefined) { if (target.email) {
params.embed.largeImage = target.embed?.largeImage sendMail(
} else params.embed.largeImage = Boolean(params.embed.largeImage) 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 params.embed return Accounts.password.hash(params.password)
}
}, },
admin(actor, target, params) { username: {
if (actor.admin && !target.admin) return params.admin schema: AccountSchemas.Username,
else if (!actor.admin) return [400, "cannot promote yourself"] validator: (actor, target, params) => {
else return [400, "cannot demote an admin"] 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 (Accounts.getFromUsername(params.username))
return [400, "account with this username already exists"]
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
}
},
admin: {
schema: z.boolean(),
validator: (actor, target, params) => {
if (actor.admin && !target.admin) return params.admin
else if (!actor.admin) return [400, "cannot promote yourself"]
else return [400, "cannot demote an admin"]
}
}, },
} }
@ -306,23 +281,11 @@ export default function (files: Files) {
`the ${x} parameter cannot be set or is not a valid parameter`, `the ${x} parameter cannot be set or is not a valid parameter`,
] as Message ] as Message
let validator = ( let validator = validators[x]!
typeof validators[x] == "object"
? validators[x]
: {
validator: validators[x] as Validator<
typeof x,
false
>,
acceptsNull: false,
}
) as ValidatorWithSettings<typeof x>
if (!validator.acceptsNull && !v) let check = validator.schema.safeParse(v)
return [ if (!check.success)
400, return [400, issuesToMessage(check.error.issues)]
`the ${x} validator does not accept null values`,
] as Message
return [ return [
x, x,
@ -391,7 +354,7 @@ export default function (files: Files) {
}) })
}) })
router.get("/css", async (ctx) => { router.get("/:user/css", async (ctx) => {
let acc = ctx.get("account") let acc = ctx.get("account")
if (acc?.customCSS) return ctx.redirect(`/file/${acc.customCSS}`) if (acc?.customCSS) return ctx.redirect(`/file/${acc.customCSS}`)
else return ctx.text("") else return ctx.text("")

View file

@ -4,7 +4,10 @@
"mount": [ "mount": [
"account", "account",
"session", "session",
"info", {
"file": "index",
"to": "/"
},
{ {
"file": "file/index", "file": "file/index",
"to": "/file" "to": "/file"

View file

@ -12,9 +12,12 @@ import * as auth from "../../../lib/auth.js"
import { import {
getAccount, getAccount,
login, login,
requiresAccount requiresAccount,
scheme
} from "../../../lib/middleware.js" } from "../../../lib/middleware.js"
import ServeError from "../../../lib/errors.js" import ServeError from "../../../lib/errors.js"
import { AccountSchemas } from "../../../lib/schemas/index.js"
import { z } from "zod"
const router = new Hono<{ const router = new Hono<{
Variables: { Variables: {
@ -25,15 +28,11 @@ const router = new Hono<{
router.use(getAccount) router.use(getAccount)
export default function (files: Files) { export default function (files: Files) {
router.post("/", async (ctx, res) => { router.post("/",scheme(z.object({
username: AccountSchemas.Username,
password: AccountSchemas.StringPassword
})), async (ctx) => {
const body = await ctx.req.json() const body = await ctx.req.json()
if (
typeof body.username != "string" ||
typeof body.password != "string"
) {
ServeError(ctx, 400, "please provide a username or password")
return
}
if (auth.validate(getCookie(ctx, "auth")!)) { if (auth.validate(getCookie(ctx, "auth")!)) {
ServeError(ctx, 400, "you are already logged in") ServeError(ctx, 400, "you are already logged in")