2025-06-04 16:18:49 +02:00
|
|
|
import type { Hook } from "@hono/zod-validator";
|
2024-06-27 01:11:39 +02:00
|
|
|
import { getLogger } from "@logtape/logtape";
|
2025-03-22 18:04:47 +01:00
|
|
|
import type { RolePermission } from "@versia/client/schemas";
|
2025-06-15 04:38:20 +02:00
|
|
|
import { ApiError } from "@versia/kit";
|
2025-04-10 19:15:31 +02:00
|
|
|
import { Application, db, Emoji, Note, Token, User } from "@versia/kit/db";
|
2025-03-22 18:04:47 +01:00
|
|
|
import { Challenges } from "@versia/kit/tables";
|
2025-06-15 04:38:20 +02:00
|
|
|
import { config } from "@versia-server/config";
|
2024-06-14 10:03:51 +02:00
|
|
|
import { extractParams, verifySolution } from "altcha-lib";
|
2025-03-30 23:06:34 +02:00
|
|
|
import { SHA256 } from "bun";
|
2024-05-22 02:59:03 +02:00
|
|
|
import chalk from "chalk";
|
2025-04-10 19:15:31 +02:00
|
|
|
import { eq, type SQL } from "drizzle-orm";
|
2025-06-04 16:18:49 +02:00
|
|
|
import type { Context, Hono, MiddlewareHandler, ValidationTargets } from "hono";
|
2024-12-30 21:30:10 +01:00
|
|
|
import { every } from "hono/combine";
|
2024-12-18 20:42:40 +01:00
|
|
|
import { createMiddleware } from "hono/factory";
|
2025-04-10 19:15:31 +02:00
|
|
|
import { validator } from "hono-openapi/zod";
|
2024-04-14 12:36:25 +02:00
|
|
|
import {
|
|
|
|
|
anyOf,
|
|
|
|
|
caseInsensitive,
|
|
|
|
|
charIn,
|
|
|
|
|
createRegExp,
|
|
|
|
|
digit,
|
|
|
|
|
exactly,
|
2024-05-13 04:27:40 +02:00
|
|
|
global,
|
2024-05-12 03:27:28 +02:00
|
|
|
letter,
|
2024-05-13 04:27:40 +02:00
|
|
|
maybe,
|
2024-05-12 03:27:28 +02:00
|
|
|
oneOrMore,
|
2024-04-14 12:36:25 +02:00
|
|
|
} from "magic-regexp";
|
2024-11-02 00:43:33 +01:00
|
|
|
import { type ParsedQs, parse } from "qs";
|
2025-06-04 16:18:49 +02:00
|
|
|
import { type ZodAny, type ZodError, z } from "zod";
|
2024-05-06 09:16:33 +02:00
|
|
|
import { fromZodError } from "zod-validation-error";
|
2024-11-03 17:45:21 +01:00
|
|
|
import type { AuthData } from "~/classes/functions/user";
|
2025-03-24 14:42:09 +01:00
|
|
|
import type { HonoEnv } from "~/types/api";
|
2024-03-10 23:48:14 +01:00
|
|
|
|
2025-03-29 03:30:06 +01:00
|
|
|
export const apiRoute = (fn: (app: Hono<HonoEnv>) => void): typeof fn => fn;
|
2024-08-19 20:06:38 +02:00
|
|
|
|
2024-04-14 12:36:25 +02:00
|
|
|
export const idValidator = createRegExp(
|
|
|
|
|
anyOf(digit, charIn("ABCDEF")).times(8),
|
|
|
|
|
exactly("-"),
|
|
|
|
|
anyOf(digit, charIn("ABCDEF")).times(4),
|
|
|
|
|
exactly("-"),
|
|
|
|
|
exactly("7"),
|
|
|
|
|
anyOf(digit, charIn("ABCDEF")).times(3),
|
|
|
|
|
exactly("-"),
|
|
|
|
|
anyOf("8", "9", "A", "B").times(1),
|
|
|
|
|
anyOf(digit, charIn("ABCDEF")).times(3),
|
|
|
|
|
exactly("-"),
|
|
|
|
|
anyOf(digit, charIn("ABCDEF")).times(12),
|
|
|
|
|
[caseInsensitive],
|
|
|
|
|
);
|
2024-05-06 09:16:33 +02:00
|
|
|
|
2024-05-13 04:27:40 +02:00
|
|
|
export const mentionValidator = createRegExp(
|
|
|
|
|
exactly("@"),
|
2024-11-22 14:51:11 +01:00
|
|
|
oneOrMore(anyOf(letter.lowercase, digit, charIn("-_"))).groupedAs(
|
2024-05-13 04:27:40 +02:00
|
|
|
"username",
|
|
|
|
|
),
|
|
|
|
|
maybe(
|
|
|
|
|
exactly("@"),
|
|
|
|
|
oneOrMore(anyOf(letter, digit, charIn("_-.:"))).groupedAs("domain"),
|
|
|
|
|
),
|
|
|
|
|
[global],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
export const webfingerMention = createRegExp(
|
|
|
|
|
exactly("acct:"),
|
2024-11-22 14:51:11 +01:00
|
|
|
oneOrMore(anyOf(letter, digit, charIn("-_"))).groupedAs("username"),
|
2024-05-13 04:27:40 +02:00
|
|
|
maybe(
|
|
|
|
|
exactly("@"),
|
|
|
|
|
oneOrMore(anyOf(letter, digit, charIn("_-.:"))).groupedAs("domain"),
|
|
|
|
|
),
|
|
|
|
|
[],
|
|
|
|
|
);
|
|
|
|
|
|
2024-11-02 00:43:33 +01:00
|
|
|
export const parseUserAddress = (
|
|
|
|
|
address: string,
|
|
|
|
|
): {
|
|
|
|
|
username: string;
|
2024-12-02 15:40:20 +01:00
|
|
|
domain?: string;
|
2024-11-02 00:43:33 +01:00
|
|
|
} => {
|
2024-07-16 23:29:20 +02:00
|
|
|
let output = address;
|
|
|
|
|
// Remove leading @ if it exists
|
|
|
|
|
if (output.startsWith("@")) {
|
|
|
|
|
output = output.slice(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const [username, domain] = output.split("@");
|
|
|
|
|
return { username, domain };
|
|
|
|
|
};
|
|
|
|
|
|
2025-06-04 16:18:49 +02:00
|
|
|
export const handleZodError: Hook<
|
|
|
|
|
z.infer<ZodAny>,
|
|
|
|
|
HonoEnv,
|
|
|
|
|
string,
|
|
|
|
|
keyof ValidationTargets
|
|
|
|
|
> = (result, context): Response | undefined => {
|
2024-05-06 09:16:33 +02:00
|
|
|
if (!result.success) {
|
2024-08-19 21:03:59 +02:00
|
|
|
return context.json(
|
|
|
|
|
{
|
2025-06-04 16:18:49 +02:00
|
|
|
error: fromZodError(result.error as ZodError).message,
|
2024-08-19 21:03:59 +02:00
|
|
|
},
|
|
|
|
|
422,
|
|
|
|
|
);
|
2024-05-06 09:16:33 +02:00
|
|
|
}
|
2025-05-23 17:29:27 +02:00
|
|
|
|
|
|
|
|
return undefined;
|
2024-05-06 09:16:33 +02:00
|
|
|
};
|
|
|
|
|
|
2024-06-13 04:26:43 +02:00
|
|
|
const checkPermissions = (
|
|
|
|
|
auth: AuthData | null,
|
2025-03-22 18:04:47 +01:00
|
|
|
required: RolePermission[],
|
2024-12-30 18:00:23 +01:00
|
|
|
): void => {
|
2024-06-13 04:26:43 +02:00
|
|
|
const userPerms = auth?.user
|
|
|
|
|
? auth.user.getAllPermissions()
|
|
|
|
|
: config.permissions.anonymous;
|
|
|
|
|
|
2024-12-30 19:18:31 +01:00
|
|
|
if (!required.every((perm) => userPerms.includes(perm))) {
|
|
|
|
|
const missingPerms = required.filter(
|
2024-06-13 04:26:43 +02:00
|
|
|
(perm) => !userPerms.includes(perm),
|
|
|
|
|
);
|
2024-12-30 18:00:23 +01:00
|
|
|
throw new ApiError(
|
2024-06-13 04:26:43 +02:00
|
|
|
403,
|
2024-12-30 18:00:23 +01:00
|
|
|
"Missing permissions",
|
|
|
|
|
`Missing: ${missingPerms.join(", ")}`,
|
2024-06-13 04:26:43 +02:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const checkRouteNeedsAuth = (
|
|
|
|
|
auth: AuthData | null,
|
2024-12-30 19:18:31 +01:00
|
|
|
required: boolean,
|
2024-12-30 18:00:23 +01:00
|
|
|
): AuthData => {
|
2024-11-03 17:45:21 +01:00
|
|
|
if (auth?.user && auth?.token) {
|
2024-06-13 04:26:43 +02:00
|
|
|
return {
|
2024-11-03 17:45:21 +01:00
|
|
|
user: auth.user,
|
|
|
|
|
token: auth.token,
|
|
|
|
|
application: auth.application,
|
2024-06-13 04:26:43 +02:00
|
|
|
};
|
|
|
|
|
}
|
2024-12-30 19:18:31 +01:00
|
|
|
if (required) {
|
2024-12-30 18:00:23 +01:00
|
|
|
throw new ApiError(401, "This route requires authentication");
|
2024-06-13 04:26:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
user: null,
|
|
|
|
|
token: null,
|
|
|
|
|
application: null,
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
2024-06-14 10:03:51 +02:00
|
|
|
export const checkRouteNeedsChallenge = async (
|
2024-12-30 19:18:31 +01:00
|
|
|
required: boolean,
|
2024-06-14 10:03:51 +02:00
|
|
|
context: Context,
|
2024-12-30 18:00:23 +01:00
|
|
|
): Promise<void> => {
|
2025-02-15 02:47:29 +01:00
|
|
|
if (!(required && config.validation.challenges)) {
|
2024-12-30 18:00:23 +01:00
|
|
|
return;
|
2024-06-14 10:03:51 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const challengeSolution = context.req.header("X-Challenge-Solution");
|
|
|
|
|
|
|
|
|
|
if (!challengeSolution) {
|
2024-12-30 18:00:23 +01:00
|
|
|
throw new ApiError(
|
2024-06-14 10:03:51 +02:00
|
|
|
401,
|
2024-12-30 18:00:23 +01:00
|
|
|
"Challenge required",
|
|
|
|
|
"This route requires a challenge solution to be sent to it via the X-Challenge-Solution header. Please check the documentation for more information.",
|
2024-06-14 10:03:51 +02:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { challenge_id } = extractParams(challengeSolution);
|
|
|
|
|
|
|
|
|
|
if (!challenge_id) {
|
2024-12-30 18:00:23 +01:00
|
|
|
throw new ApiError(401, "The challenge solution provided is invalid.");
|
2024-06-14 10:03:51 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const challenge = await db.query.Challenges.findFirst({
|
2025-05-26 19:00:24 +02:00
|
|
|
where: (c): SQL | undefined => eq(c.id, challenge_id),
|
2024-06-14 10:03:51 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!challenge) {
|
2024-12-30 18:00:23 +01:00
|
|
|
throw new ApiError(401, "The challenge solution provided is invalid.");
|
2024-06-14 10:03:51 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (new Date(challenge.expiresAt) < new Date()) {
|
2024-12-30 18:00:23 +01:00
|
|
|
throw new ApiError(401, "The challenge provided has expired.");
|
2024-06-14 10:03:51 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const isValid = await verifySolution(
|
|
|
|
|
challengeSolution,
|
|
|
|
|
config.validation.challenges.key,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (!isValid) {
|
2024-12-30 18:00:23 +01:00
|
|
|
throw new ApiError(
|
2024-06-14 10:03:51 +02:00
|
|
|
401,
|
2024-12-30 18:00:23 +01:00
|
|
|
"The challenge solution provided is incorrect.",
|
2024-06-14 10:03:51 +02:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Expire the challenge
|
|
|
|
|
await db
|
|
|
|
|
.update(Challenges)
|
|
|
|
|
.set({ expiresAt: new Date().toISOString() })
|
|
|
|
|
.where(eq(Challenges.id, challenge_id));
|
|
|
|
|
};
|
|
|
|
|
|
2025-03-27 20:12:00 +01:00
|
|
|
export type HonoEnvWithAuth = HonoEnv & {
|
2024-12-30 19:18:31 +01:00
|
|
|
Variables: {
|
2025-01-02 01:29:33 +01:00
|
|
|
auth: AuthData & {
|
|
|
|
|
user: NonNullable<AuthData["user"]>;
|
|
|
|
|
token: NonNullable<AuthData["token"]>;
|
|
|
|
|
};
|
2024-12-30 19:18:31 +01:00
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const auth = <AuthRequired extends boolean>(options: {
|
|
|
|
|
auth: AuthRequired;
|
2025-03-22 18:04:47 +01:00
|
|
|
permissions?: RolePermission[];
|
2024-12-30 19:18:31 +01:00
|
|
|
challenge?: boolean;
|
|
|
|
|
scopes?: string[];
|
|
|
|
|
// If authRequired is true, HonoEnv.Variables.auth.user will never be null
|
|
|
|
|
}): MiddlewareHandler<
|
|
|
|
|
AuthRequired extends true ? HonoEnvWithAuth : HonoEnv
|
|
|
|
|
> => {
|
|
|
|
|
return createMiddleware(async (context, next) => {
|
2024-08-27 17:20:36 +02:00
|
|
|
const header = context.req.header("Authorization");
|
2024-11-03 17:45:21 +01:00
|
|
|
const tokenString = header?.split(" ")[1];
|
|
|
|
|
|
|
|
|
|
const token = tokenString
|
|
|
|
|
? await Token.fromAccessToken(tokenString)
|
|
|
|
|
: null;
|
2024-08-27 17:20:36 +02:00
|
|
|
|
2024-11-03 17:45:21 +01:00
|
|
|
const auth: AuthData = {
|
|
|
|
|
token,
|
|
|
|
|
application: token?.data.application
|
|
|
|
|
? new Application(token?.data.application)
|
|
|
|
|
: null,
|
|
|
|
|
user: (await token?.getUser()) ?? null,
|
|
|
|
|
};
|
2024-06-13 04:26:43 +02:00
|
|
|
|
2024-11-26 15:27:39 +01:00
|
|
|
// Authentication check
|
2024-12-30 19:18:31 +01:00
|
|
|
const authCheck = checkRouteNeedsAuth(auth, options.auth);
|
2024-11-26 15:27:39 +01:00
|
|
|
|
|
|
|
|
context.set("auth", authCheck);
|
|
|
|
|
|
2024-06-13 04:26:43 +02:00
|
|
|
// Permissions check
|
2024-12-30 19:18:31 +01:00
|
|
|
if (options.permissions) {
|
|
|
|
|
checkPermissions(auth, options.permissions);
|
2024-06-13 04:26:43 +02:00
|
|
|
}
|
|
|
|
|
|
2024-11-26 15:27:39 +01:00
|
|
|
// Challenge check
|
2025-02-15 02:47:29 +01:00
|
|
|
if (options.challenge && config.validation.challenges) {
|
2024-12-30 19:18:31 +01:00
|
|
|
await checkRouteNeedsChallenge(options.challenge, context);
|
2024-06-14 10:03:51 +02:00
|
|
|
}
|
|
|
|
|
|
2024-08-27 17:20:36 +02:00
|
|
|
await next();
|
2024-06-13 04:26:43 +02:00
|
|
|
});
|
2024-12-30 19:18:31 +01:00
|
|
|
};
|
2024-06-13 04:26:43 +02:00
|
|
|
|
2024-12-30 21:30:10 +01:00
|
|
|
type WithIdParam = {
|
|
|
|
|
in: { param: { id: string } };
|
|
|
|
|
out: { param: { id: string } };
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Middleware to check if a note exists and is viewable by the user.
|
|
|
|
|
*
|
|
|
|
|
* Useful in /api/v1/statuses/:id/* routes
|
|
|
|
|
* @returns MiddlewareHandler
|
|
|
|
|
*/
|
|
|
|
|
export const withNoteParam = every(
|
2025-03-29 03:30:06 +01:00
|
|
|
validator("param", z.object({ id: z.string().uuid() }), handleZodError),
|
2024-12-30 21:30:10 +01:00
|
|
|
createMiddleware<
|
|
|
|
|
HonoEnv & {
|
|
|
|
|
Variables: {
|
|
|
|
|
note: Note;
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
string,
|
|
|
|
|
WithIdParam
|
|
|
|
|
>(async (context, next) => {
|
|
|
|
|
const { id } = context.req.valid("param");
|
|
|
|
|
const { user } = context.get("auth");
|
|
|
|
|
|
|
|
|
|
const note = await Note.fromId(id, user?.id);
|
|
|
|
|
|
|
|
|
|
if (!(note && (await note.isViewableByUser(user)))) {
|
2025-03-24 14:42:09 +01:00
|
|
|
throw ApiError.noteNotFound();
|
2024-12-30 21:30:10 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
context.set("note", note);
|
|
|
|
|
|
|
|
|
|
await next();
|
|
|
|
|
}),
|
|
|
|
|
) as MiddlewareHandler<
|
|
|
|
|
HonoEnv & {
|
|
|
|
|
Variables: {
|
|
|
|
|
note: Note;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
>;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Middleware to check if a user exists
|
|
|
|
|
*
|
|
|
|
|
* Useful in /api/v1/accounts/:id/* routes
|
|
|
|
|
* @returns MiddlewareHandler
|
|
|
|
|
*/
|
|
|
|
|
export const withUserParam = every(
|
2025-03-29 03:30:06 +01:00
|
|
|
validator("param", z.object({ id: z.string().uuid() }), handleZodError),
|
2024-12-30 21:30:10 +01:00
|
|
|
createMiddleware<
|
|
|
|
|
HonoEnv & {
|
|
|
|
|
Variables: {
|
|
|
|
|
user: User;
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
string,
|
|
|
|
|
WithIdParam
|
|
|
|
|
>(async (context, next) => {
|
|
|
|
|
const { id } = context.req.valid("param");
|
|
|
|
|
const user = await User.fromId(id);
|
|
|
|
|
|
|
|
|
|
if (!user) {
|
|
|
|
|
throw new ApiError(404, "User not found");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
context.set("user", user);
|
|
|
|
|
|
|
|
|
|
await next();
|
|
|
|
|
}),
|
|
|
|
|
) as MiddlewareHandler<
|
|
|
|
|
HonoEnv & {
|
|
|
|
|
Variables: {
|
|
|
|
|
user: User;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
>;
|
|
|
|
|
|
2025-02-13 02:34:44 +01:00
|
|
|
/**
|
|
|
|
|
* Middleware to check if an emoji exists and is viewable by the user
|
|
|
|
|
*
|
|
|
|
|
* Useful in /api/v1/emojis/:id/* routes
|
|
|
|
|
* @returns
|
|
|
|
|
*/
|
|
|
|
|
export const withEmojiParam = every(
|
2025-03-29 03:30:06 +01:00
|
|
|
validator("param", z.object({ id: z.string().uuid() }), handleZodError),
|
2025-02-13 02:34:44 +01:00
|
|
|
createMiddleware<
|
|
|
|
|
HonoEnv & {
|
|
|
|
|
Variables: {
|
|
|
|
|
emoji: Emoji;
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
string,
|
|
|
|
|
WithIdParam
|
|
|
|
|
>(async (context, next) => {
|
|
|
|
|
const { id } = context.req.valid("param");
|
|
|
|
|
|
|
|
|
|
const emoji = await Emoji.fromId(id);
|
|
|
|
|
|
|
|
|
|
if (!emoji) {
|
2025-03-24 14:42:09 +01:00
|
|
|
throw ApiError.emojiNotFound();
|
2025-02-13 02:34:44 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
context.set("emoji", emoji);
|
|
|
|
|
|
|
|
|
|
await next();
|
|
|
|
|
}),
|
|
|
|
|
) as MiddlewareHandler<
|
|
|
|
|
HonoEnv & {
|
|
|
|
|
Variables: {
|
|
|
|
|
emoji: Emoji;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
>;
|
|
|
|
|
|
2024-06-13 06:16:59 +02:00
|
|
|
// Helper function to parse form data
|
2024-11-02 00:43:33 +01:00
|
|
|
async function parseFormData(context: Context): Promise<{
|
|
|
|
|
parsed: ParsedQs;
|
|
|
|
|
files: Map<string, File>;
|
|
|
|
|
}> {
|
2024-06-13 06:16:59 +02:00
|
|
|
const formData = await context.req.formData();
|
|
|
|
|
const urlparams = new URLSearchParams();
|
|
|
|
|
const files = new Map<string, File>();
|
2025-04-10 18:50:41 +02:00
|
|
|
for (const [key, value] of [
|
|
|
|
|
...(formData.entries() as IterableIterator<[string, string | File]>),
|
|
|
|
|
]) {
|
2024-06-13 06:16:59 +02:00
|
|
|
if (Array.isArray(value)) {
|
|
|
|
|
for (const val of value) {
|
|
|
|
|
urlparams.append(key, val);
|
2024-06-08 06:57:29 +02:00
|
|
|
}
|
2024-06-13 06:16:59 +02:00
|
|
|
} else if (value instanceof File) {
|
|
|
|
|
if (!files.has(key)) {
|
|
|
|
|
files.set(key, value);
|
2024-05-08 13:16:16 +02:00
|
|
|
}
|
2024-06-13 06:16:59 +02:00
|
|
|
} else {
|
|
|
|
|
urlparams.append(key, String(value));
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-05-08 13:16:16 +02:00
|
|
|
|
2024-06-13 06:16:59 +02:00
|
|
|
const parsed = parse(urlparams.toString(), {
|
|
|
|
|
parseArrays: true,
|
|
|
|
|
interpretNumericEntities: true,
|
|
|
|
|
});
|
2024-05-08 13:16:16 +02:00
|
|
|
|
2024-06-13 06:16:59 +02:00
|
|
|
return {
|
|
|
|
|
parsed,
|
|
|
|
|
files,
|
|
|
|
|
};
|
|
|
|
|
}
|
2024-05-08 13:16:16 +02:00
|
|
|
|
2024-06-13 06:16:59 +02:00
|
|
|
// Helper function to parse urlencoded data
|
2024-11-02 00:43:33 +01:00
|
|
|
async function parseUrlEncoded(context: Context): Promise<ParsedQs> {
|
2024-06-13 06:16:59 +02:00
|
|
|
const parsed = parse(await context.req.text(), {
|
|
|
|
|
parseArrays: true,
|
|
|
|
|
interpretNumericEntities: true,
|
2024-05-06 09:16:33 +02:00
|
|
|
});
|
2024-06-13 06:16:59 +02:00
|
|
|
|
|
|
|
|
return parsed;
|
|
|
|
|
}
|
2024-05-06 09:16:33 +02:00
|
|
|
|
2024-12-30 19:38:41 +01:00
|
|
|
export const qsQuery = (): MiddlewareHandler<HonoEnv> => {
|
|
|
|
|
return createMiddleware<HonoEnv>(async (context, next) => {
|
2024-11-03 17:45:21 +01:00
|
|
|
const parsed = parse(new URL(context.req.url).searchParams.toString(), {
|
2024-05-06 09:16:33 +02:00
|
|
|
parseArrays: true,
|
|
|
|
|
interpretNumericEntities: true,
|
|
|
|
|
});
|
|
|
|
|
|
2024-06-29 09:33:19 +02:00
|
|
|
// @ts-expect-error Very bad hack
|
2024-11-02 00:43:33 +01:00
|
|
|
context.req.query = (): typeof parsed => parsed;
|
2024-06-13 06:16:59 +02:00
|
|
|
|
2024-06-29 09:33:19 +02:00
|
|
|
// @ts-expect-error I'm so sorry for this
|
2024-11-02 00:43:33 +01:00
|
|
|
context.req.queries = (): typeof parsed => parsed;
|
2024-05-06 09:16:33 +02:00
|
|
|
await next();
|
|
|
|
|
});
|
|
|
|
|
};
|
2024-05-06 10:31:12 +02:00
|
|
|
|
2024-06-13 06:16:59 +02:00
|
|
|
export const setContextFormDataToObject = (
|
|
|
|
|
context: Context,
|
|
|
|
|
setTo: object,
|
|
|
|
|
): Context => {
|
2024-07-11 12:56:28 +02:00
|
|
|
context.req.bodyCache.json = setTo;
|
2024-11-02 00:43:33 +01:00
|
|
|
context.req.parseBody = (): Promise<unknown> =>
|
|
|
|
|
Promise.resolve(context.req.bodyCache.json);
|
2025-05-01 16:27:34 +02:00
|
|
|
// biome-ignore lint/suspicious/noExplicitAny: Monkeypatching
|
2024-11-02 00:43:33 +01:00
|
|
|
context.req.json = (): Promise<any> =>
|
|
|
|
|
Promise.resolve(context.req.bodyCache.json);
|
2024-06-13 06:16:59 +02:00
|
|
|
|
|
|
|
|
return context;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* Middleware to magically unfuck forms
|
|
|
|
|
* Add it to random Hono routes and hope it works
|
|
|
|
|
* @returns
|
|
|
|
|
*/
|
2024-12-18 20:42:40 +01:00
|
|
|
export const jsonOrForm = (): MiddlewareHandler<HonoEnv> => {
|
2024-05-06 10:31:12 +02:00
|
|
|
return createMiddleware(async (context, next) => {
|
|
|
|
|
const contentType = context.req.header("content-type");
|
|
|
|
|
|
|
|
|
|
if (contentType?.includes("application/json")) {
|
2024-06-13 06:16:59 +02:00
|
|
|
setContextFormDataToObject(context, await context.req.json());
|
2024-05-06 10:31:12 +02:00
|
|
|
} else if (contentType?.includes("application/x-www-form-urlencoded")) {
|
2024-06-13 06:16:59 +02:00
|
|
|
const parsed = await parseUrlEncoded(context);
|
2024-05-06 10:31:12 +02:00
|
|
|
|
2024-06-13 06:16:59 +02:00
|
|
|
setContextFormDataToObject(context, parsed);
|
2024-07-11 12:56:28 +02:00
|
|
|
context.req.raw.headers.set("Content-Type", "application/json");
|
2024-05-12 03:27:28 +02:00
|
|
|
} else if (contentType?.includes("multipart/form-data")) {
|
2024-06-13 06:16:59 +02:00
|
|
|
const { parsed, files } = await parseFormData(context);
|
2024-05-12 03:27:28 +02:00
|
|
|
|
2024-06-13 06:16:59 +02:00
|
|
|
setContextFormDataToObject(context, {
|
2024-05-12 03:27:28 +02:00
|
|
|
...parsed,
|
|
|
|
|
...Object.fromEntries(files),
|
2024-06-13 06:16:59 +02:00
|
|
|
});
|
2024-07-11 12:56:28 +02:00
|
|
|
context.req.raw.headers.set("Content-Type", "application/json");
|
|
|
|
|
} else if (!contentType) {
|
|
|
|
|
setContextFormDataToObject(context, {});
|
|
|
|
|
context.req.raw.headers.set("Content-Type", "application/json");
|
2024-05-06 10:31:12 +02:00
|
|
|
}
|
2024-06-08 05:31:17 +02:00
|
|
|
|
2024-05-06 10:31:12 +02:00
|
|
|
await next();
|
|
|
|
|
});
|
|
|
|
|
};
|
2024-05-22 02:59:03 +02:00
|
|
|
|
2024-11-02 00:43:33 +01:00
|
|
|
export const debugRequest = async (req: Request): Promise<void> => {
|
2024-09-04 22:59:39 +02:00
|
|
|
const body = await req.text();
|
2024-06-27 01:11:39 +02:00
|
|
|
const logger = getLogger("server");
|
|
|
|
|
|
|
|
|
|
const urlAndMethod = `${chalk.green(req.method)} ${chalk.blue(req.url)}`;
|
|
|
|
|
|
|
|
|
|
const hash = `${chalk.bold("Hash")}: ${chalk.yellow(
|
2025-03-30 23:06:34 +02:00
|
|
|
new SHA256().update(body).digest("hex"),
|
2024-06-27 01:11:39 +02:00
|
|
|
)}`;
|
|
|
|
|
|
|
|
|
|
const headers = `${chalk.bold("Headers")}:\n${Array.from(
|
|
|
|
|
req.headers.entries(),
|
|
|
|
|
)
|
|
|
|
|
.map(([key, value]) => ` - ${chalk.cyan(key)}: ${chalk.white(value)}`)
|
|
|
|
|
.join("\n")}`;
|
|
|
|
|
|
|
|
|
|
const bodyLog = `${chalk.bold("Body")}: ${chalk.gray(body)}`;
|
|
|
|
|
|
2025-02-15 02:47:29 +01:00
|
|
|
if (config.logging.types.requests_content) {
|
2024-06-27 01:11:39 +02:00
|
|
|
logger.debug`${urlAndMethod}\n${hash}\n${headers}\n${bodyLog}`;
|
|
|
|
|
} else {
|
|
|
|
|
logger.debug`${urlAndMethod}`;
|
|
|
|
|
}
|
2024-05-22 02:59:03 +02:00
|
|
|
};
|
2024-09-04 22:52:43 +02:00
|
|
|
|
2024-11-02 00:43:33 +01:00
|
|
|
export const debugResponse = async (res: Response): Promise<void> => {
|
2024-09-04 22:52:43 +02:00
|
|
|
const body = await res.clone().text();
|
|
|
|
|
const logger = getLogger("server");
|
|
|
|
|
|
|
|
|
|
const status = `${chalk.bold("Status")}: ${chalk.green(res.status)}`;
|
|
|
|
|
|
|
|
|
|
const headers = `${chalk.bold("Headers")}:\n${Array.from(
|
|
|
|
|
res.headers.entries(),
|
|
|
|
|
)
|
|
|
|
|
.map(([key, value]) => ` - ${chalk.cyan(key)}: ${chalk.white(value)}`)
|
|
|
|
|
.join("\n")}`;
|
|
|
|
|
|
|
|
|
|
const bodyLog = `${chalk.bold("Body")}: ${chalk.gray(body)}`;
|
|
|
|
|
|
2025-02-15 02:47:29 +01:00
|
|
|
if (config.logging.types.requests_content) {
|
2024-09-04 22:52:43 +02:00
|
|
|
logger.debug`${status}\n${headers}\n${bodyLog}`;
|
|
|
|
|
} else {
|
|
|
|
|
logger.debug`${status}`;
|
|
|
|
|
}
|
|
|
|
|
};
|