chuni: error handling, response validation and utilities
This commit is contained in:
parent
176b2c3352
commit
f019f6575e
|
@ -1,19 +1,19 @@
|
|||
import { VERSIONS } from "./versions";
|
||||
import { ChunkLength, ToBuffer } from "../utils/buffer";
|
||||
import { GetClassMethods } from "../utils/reflection";
|
||||
import compression from "compression";
|
||||
import { TransformResponse } from "../utils/response";
|
||||
import { Router } from "express";
|
||||
import { ChunithmVersions } from "lib/constants/game-versions";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import { RequestLoggerMiddleware } from "lib/middleware/request-logger";
|
||||
import { Config, Environment } from "lib/setup/config";
|
||||
import onHeaders from "on-headers";
|
||||
import getRawBody from "raw-body";
|
||||
import { IsHexadecimalString } from "utils/misc";
|
||||
import { ZodError } from "zod";
|
||||
import { fromZodError } from "zod-validation-error";
|
||||
import { createCipheriv, createDecipheriv, pbkdf2Sync } from "crypto";
|
||||
import { promisify } from "util";
|
||||
import zlib, { createDeflate, inflate } from "zlib";
|
||||
import type { RequestHandler } from "express";
|
||||
import type { ErrorRequestHandler, RequestHandler } from "express";
|
||||
import type { Transform } from "stream";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
@ -30,11 +30,10 @@ router.use(async (req, res, next) => {
|
|||
|
||||
// /SDHD/210/UpsertUserAllApi
|
||||
const splitPath = req.path.split("/");
|
||||
const version = splitPath[2];
|
||||
const versionNum = Number(version);
|
||||
const version = Number(splitPath[2]);
|
||||
const endpoint = splitPath.at(-1);
|
||||
|
||||
if (!endpoint || !version || Number.isNaN(versionNum)) {
|
||||
if (!endpoint || Number.isNaN(version)) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
@ -44,7 +43,7 @@ router.use(async (req, res, next) => {
|
|||
if (
|
||||
!isEncryptedRequest &&
|
||||
Config.CHUNITHM_CONFIG.CRYPTO.ENCRYPTED_ONLY &&
|
||||
versionNum >= ChunithmVersions.CRYSTAL_PLUS
|
||||
version >= ChunithmVersions.CRYSTAL_PLUS
|
||||
) {
|
||||
logger.error(
|
||||
"Rejecting unencrypted request, because the server is configured to accept only encrypted requests.",
|
||||
|
@ -54,194 +53,81 @@ router.use(async (req, res, next) => {
|
|||
return res.status(200).send({ stat: "0" });
|
||||
}
|
||||
|
||||
const body = await getRawBody(req, { limit: "20mb" });
|
||||
const transformations: Array<Transform> = [
|
||||
createDeflate({ flush: zlib.constants.Z_SYNC_FLUSH }),
|
||||
];
|
||||
let decrypted: Buffer | null = null;
|
||||
|
||||
if (!isEncryptedRequest) {
|
||||
const decompressed = await inflateAsync(body);
|
||||
if (isEncryptedRequest) {
|
||||
const keys = Config.CHUNITHM_CONFIG.CRYPTO.KEYS?.[version];
|
||||
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
req.body = JSON.parse(decompressed.toString("utf-8"));
|
||||
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug("Received encrypted request.", { version, endpoint });
|
||||
res.locals.encrypted = true;
|
||||
|
||||
const keys = Config.CHUNITHM_CONFIG.CRYPTO.KEYS?.[version];
|
||||
|
||||
if (!keys) {
|
||||
logger.error("No keys were configured for the requested version.", {
|
||||
version,
|
||||
endpoint,
|
||||
});
|
||||
return res.status(200).send({ stat: "0" });
|
||||
}
|
||||
|
||||
// Mostly as a convenience, so versions PARADISE LOST and older don't have to provide
|
||||
// the path salt.
|
||||
const userAgent = req.headers["user-agent"];
|
||||
|
||||
if (versionNum <= ChunithmVersions.PARADISE && userAgent) {
|
||||
const unhashedEndpoint = userAgent.split("#")[0];
|
||||
|
||||
if (unhashedEndpoint) {
|
||||
logger.debug("Resolved to unhashed endpoint from User-Agent.", { unhashedEndpoint });
|
||||
req.url = req.url.replace(endpoint, unhashedEndpoint);
|
||||
} else {
|
||||
logger.warn(
|
||||
"User-Agent did not contain the unhashed endpoint for version PARADISE (LOST) or older?",
|
||||
{ version, userAgent }
|
||||
);
|
||||
if (!keys) {
|
||||
logger.error("No keys were configured for the requested version.", {
|
||||
version,
|
||||
endpoint,
|
||||
});
|
||||
return res.status(200).send({ stat: "0" });
|
||||
}
|
||||
|
||||
// Mostly as a convenience, so versions PARADISE LOST and older don't have to provide
|
||||
// the path salt.
|
||||
const userAgent = req.headers["user-agent"];
|
||||
|
||||
if (version <= ChunithmVersions.PARADISE && userAgent) {
|
||||
const unhashedEndpoint = userAgent.split("#")[0];
|
||||
|
||||
if (unhashedEndpoint) {
|
||||
logger.debug("Resolved to unhashed endpoint from User-Agent.", {
|
||||
unhashedEndpoint,
|
||||
});
|
||||
req.url = req.url.replace(endpoint, unhashedEndpoint);
|
||||
} else {
|
||||
logger.warn(
|
||||
"User-Agent did not contain the unhashed endpoint for version PARADISE (LOST) or older?",
|
||||
{ version, userAgent }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const keyString = keys[0];
|
||||
const ivString = keys[1];
|
||||
|
||||
if (!keyString || !ivString) {
|
||||
logger.error("Key or IV was not provided for the requested version.", {
|
||||
version,
|
||||
key: keyString,
|
||||
iv: ivString,
|
||||
});
|
||||
|
||||
return res.status(200).send({ stat: "0" });
|
||||
}
|
||||
|
||||
const key = Buffer.from(keyString, "hex");
|
||||
const iv = Buffer.from(ivString, "hex");
|
||||
const cipher = createDecipheriv("aes-256-cbc", key, iv);
|
||||
const body = await getRawBody(req, { limit: "20mb" });
|
||||
|
||||
res.locals.key = key;
|
||||
res.locals.iv = iv;
|
||||
decrypted = Buffer.concat([cipher.update(body), cipher.final()]);
|
||||
|
||||
transformations.push(createCipheriv("aes-256-cbc", key, iv));
|
||||
} else {
|
||||
decrypted = await getRawBody(req, { limit: "20mb" });
|
||||
}
|
||||
|
||||
const keyString = keys[0];
|
||||
const ivString = keys[1];
|
||||
|
||||
if (!keyString || !ivString) {
|
||||
logger.error("Key or IV was not provided for the requested version.", {
|
||||
version,
|
||||
key: keyString,
|
||||
iv: ivString,
|
||||
});
|
||||
|
||||
return res.status(200).send({ stat: "0" });
|
||||
}
|
||||
|
||||
const key = Buffer.from(keyString, "hex");
|
||||
const iv = Buffer.from(ivString, "hex");
|
||||
|
||||
const cipher = createDecipheriv("aes-256-cbc", key, iv);
|
||||
const decrypted = Buffer.concat([cipher.update(body), cipher.final()]);
|
||||
const decompressed = await inflateAsync(decrypted);
|
||||
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
req.body = JSON.parse(decompressed.toString("utf-8"));
|
||||
|
||||
// Set up response necryption
|
||||
// Eldritch horrors beyond my understanding down here.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const listeners: Array<[string | symbol, any]> = [];
|
||||
let stream: Transform | null = null;
|
||||
let ended = false;
|
||||
|
||||
/* eslint-disable @typescript-eslint/unbound-method */
|
||||
const _on = res.on;
|
||||
const _write = res.write;
|
||||
const _end = res.end;
|
||||
/* eslint-enable @typescript-eslint/unbound-method */
|
||||
|
||||
/* eslint-disable prefer-arrow-callback */
|
||||
res.write = function write(chunk, encoding, ...rest) {
|
||||
if (ended) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// @ts-expect-error monkey noises
|
||||
if (!this._header && !this.headersSent) {
|
||||
this.writeHead(this.statusCode);
|
||||
}
|
||||
|
||||
const enc = typeof encoding === "function" ? undefined : encoding;
|
||||
|
||||
return stream
|
||||
? stream.write(ToBuffer(chunk, enc))
|
||||
: // @ts-expect-error reeeee
|
||||
_write.call(this, chunk, encoding, ...rest);
|
||||
};
|
||||
|
||||
// @ts-expect-error idk
|
||||
res.end = function end(chunk, encoding, ...rest) {
|
||||
if (ended) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const enc = typeof encoding === "function" ? undefined : encoding;
|
||||
|
||||
// @ts-expect-error how do express people live like this?
|
||||
if (!this._header && !this.headersSent) {
|
||||
this.writeHead(this.statusCode);
|
||||
|
||||
// estimate the length
|
||||
if (!this.getHeader("Content-Length")) {
|
||||
this.setHeader("Content-Length", ChunkLength(chunk, enc));
|
||||
}
|
||||
}
|
||||
|
||||
if (!stream) {
|
||||
// @ts-expect-error bruh
|
||||
return _end.call(this, chunk, encoding, ...rest);
|
||||
}
|
||||
|
||||
// mark ended
|
||||
ended = true;
|
||||
|
||||
// write Buffer for Node.js 0.8
|
||||
// @ts-expect-error bruh
|
||||
return chunk ? stream.end(ToBuffer(chunk, encoding)) : stream.end();
|
||||
};
|
||||
|
||||
// @ts-expect-error zzz
|
||||
res.on = function on(event, listener) {
|
||||
if (event !== "drain") {
|
||||
return _on.call(this, event, listener);
|
||||
}
|
||||
|
||||
if (stream) {
|
||||
return stream.on(event, listener);
|
||||
}
|
||||
|
||||
// buffer listeners for future stream
|
||||
listeners.push([event, listener]);
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
onHeaders(res, function onResponseHeaders() {
|
||||
const cipher = createCipheriv("aes-256-cbc", key, iv);
|
||||
|
||||
stream = createDeflate({ flush: zlib.constants.Z_SYNC_FLUSH });
|
||||
stream.pipe(cipher);
|
||||
|
||||
for (const listener of listeners) {
|
||||
stream.on(listener[0], listener[1]);
|
||||
}
|
||||
|
||||
res.removeHeader("Content-Length");
|
||||
|
||||
cipher.on("data", (chunk) => {
|
||||
// @ts-expect-error ugggh i hate monkeys
|
||||
if (!_write.call(res, chunk)) {
|
||||
stream?.pause();
|
||||
}
|
||||
});
|
||||
|
||||
cipher.on("end", () => {
|
||||
// @ts-expect-error ugggh i hate monkeys
|
||||
_end.call(res);
|
||||
});
|
||||
|
||||
_on.call(res, "drain", () => {
|
||||
stream?.resume();
|
||||
});
|
||||
});
|
||||
/* eslint-enable prefer-arrow-callback */
|
||||
TransformResponse(res, ...transformations);
|
||||
res.locals.transformationsSet = true;
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
// you WILL compress the response and you WILL be happy about it.
|
||||
// chunithm assumes all responses are deflated.
|
||||
router.use(
|
||||
compression({
|
||||
threshold: 0,
|
||||
filter: (req, res) => !res.locals.encrypted,
|
||||
})
|
||||
);
|
||||
|
||||
router.use((req, res, next) => {
|
||||
router.use((req, _, next) => {
|
||||
if (Environment.nodeEnv === "dev" && req.header("x-debug") !== undefined) {
|
||||
next();
|
||||
return;
|
||||
|
@ -324,8 +210,9 @@ for (const Title of VERSIONS) {
|
|||
}
|
||||
|
||||
const impl = (inst as unknown as Record<string, unknown>)[method] as RequestHandler;
|
||||
const implWrapper: RequestHandler = (req, res, next) => {
|
||||
impl.apply(inst, [req, res, next]);
|
||||
const implWrapper: RequestHandler = async (req, res, next) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-confusing-void-expression
|
||||
await Promise.resolve(impl.call(inst, req, res, next));
|
||||
};
|
||||
|
||||
subrouter.post(`/${methodName}`, implWrapper);
|
||||
|
@ -344,4 +231,30 @@ for (const Title of VERSIONS) {
|
|||
// TODO: Add to a title registry
|
||||
}
|
||||
|
||||
const ERR_HANDLER: ErrorRequestHandler = (err, req, res, _next) => {
|
||||
// eslint-disable-next-line cadence/no-instanceof
|
||||
const reportedErr = err instanceof ZodError ? fromZodError(err).message : err;
|
||||
|
||||
logger.error("Error propagated to title router.", {
|
||||
err: reportedErr,
|
||||
url: req.originalUrl,
|
||||
});
|
||||
|
||||
if (!res.locals.transformationsSet) {
|
||||
const transformations: Array<Transform> = [
|
||||
createDeflate({ flush: zlib.constants.Z_SYNC_FLUSH }),
|
||||
];
|
||||
|
||||
if (res.locals.key && res.locals.iv) {
|
||||
transformations.push(createCipheriv("aes-256-cbc", res.locals.key, res.locals.iv));
|
||||
}
|
||||
|
||||
TransformResponse(res, ...transformations);
|
||||
}
|
||||
|
||||
return res.status(200).send({ returnCode: "0" });
|
||||
};
|
||||
|
||||
router.use(ERR_HANDLER);
|
||||
|
||||
export default router;
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
import { z } from "zod";
|
||||
|
||||
export const UserRequest = z.object({
|
||||
userId: z.coerce.number(),
|
||||
});
|
||||
|
||||
export const UserPaginatedRequest = UserRequest.extend({
|
||||
nextIndex: z.coerce.number(),
|
||||
maxCount: z.coerce.number(),
|
||||
});
|
||||
|
||||
export const UserRequestWithKind = UserRequest.extend({
|
||||
kind: z.coerce.number(),
|
||||
});
|
|
@ -1,4 +1,5 @@
|
|||
import { GetByCardIdAndVersion } from "../utils";
|
||||
import { UserPaginatedRequest, UserRequest, UserRequestWithKind } from "../validators";
|
||||
import { ChunithmUserCharacter, ChunithmUserItem } from "external/db/entity/chunithm/item";
|
||||
import {
|
||||
ChunithmUserActivity,
|
||||
|
@ -13,7 +14,6 @@ import CreateLogCtx from "lib/logger/logger";
|
|||
import { Config } from "lib/setup/config";
|
||||
import { DateTime } from "luxon";
|
||||
import { BaseTitle } from "servers/titles/types/titles";
|
||||
import { ParseStrInt } from "servers/titles/utils/string-checks";
|
||||
import { LessThanOrEqual } from "typeorm";
|
||||
import type { Request, Response } from "express";
|
||||
|
||||
|
@ -141,15 +141,8 @@ export class Chunithm extends BaseTitle {
|
|||
}
|
||||
|
||||
async handle_GetUserPreviewApi(req: Request, res: Response) {
|
||||
const userId = ParseStrInt(req.safeBody.userId);
|
||||
|
||||
if (!userId) {
|
||||
logger.error("Invalid request: received userId is not a number.", {
|
||||
userId: req.safeBody.userId,
|
||||
});
|
||||
|
||||
return res.send({ returnCode: "1" });
|
||||
}
|
||||
const body = UserRequest.parse(req.safeBody);
|
||||
const userId = body.userId;
|
||||
|
||||
const profile = await GetByCardIdAndVersion(ChunithmUserData, userId, this.numericVersion);
|
||||
|
||||
|
@ -217,15 +210,8 @@ export class Chunithm extends BaseTitle {
|
|||
}
|
||||
|
||||
async handle_GetUserDataApi(req: Request, res: Response) {
|
||||
const userId = ParseStrInt(req.safeBody.userId);
|
||||
|
||||
if (!userId) {
|
||||
logger.error("Invalid request: received userId is not a number.", {
|
||||
userId: req.safeBody.userId,
|
||||
});
|
||||
|
||||
return res.send({ returnCode: "1" });
|
||||
}
|
||||
const body = UserRequest.parse(req.safeBody);
|
||||
const userId = body.userId;
|
||||
|
||||
const profile = await GetByCardIdAndVersion(ChunithmUserData, userId, this.numericVersion);
|
||||
|
||||
|
@ -244,15 +230,8 @@ export class Chunithm extends BaseTitle {
|
|||
}
|
||||
|
||||
async handle_GetUserDataExApi(req: Request, res: Response) {
|
||||
const userId = ParseStrInt(req.safeBody.userId);
|
||||
|
||||
if (!userId) {
|
||||
logger.error("Invalid request: received userId is not a number.", {
|
||||
userId: req.safeBody.userId,
|
||||
});
|
||||
|
||||
return res.send({ returnCode: "1" });
|
||||
}
|
||||
const body = UserRequest.parse(req.safeBody);
|
||||
const userId = body.userId;
|
||||
|
||||
const profile = await GetByCardIdAndVersion(
|
||||
ChunithmUserDataEx,
|
||||
|
@ -275,15 +254,8 @@ export class Chunithm extends BaseTitle {
|
|||
}
|
||||
|
||||
async handle_GetUserOptionApi(req: Request, res: Response) {
|
||||
const userId = ParseStrInt(req.safeBody.userId);
|
||||
|
||||
if (!userId) {
|
||||
logger.error("Invalid request: received userId is not a number.", {
|
||||
userId: req.safeBody.userId,
|
||||
});
|
||||
|
||||
return res.send({ returnCode: "1" });
|
||||
}
|
||||
const body = UserRequest.parse(req.safeBody);
|
||||
const userId = body.userId;
|
||||
|
||||
const options = await GetByCardIdAndVersion(
|
||||
ChunithmUserOption,
|
||||
|
@ -306,15 +278,8 @@ export class Chunithm extends BaseTitle {
|
|||
}
|
||||
|
||||
async handle_GetUserOptionExApi(req: Request, res: Response) {
|
||||
const userId = ParseStrInt(req.safeBody.userId);
|
||||
|
||||
if (!userId) {
|
||||
logger.error("Invalid request: received userId is not a number.", {
|
||||
userId: req.safeBody.userId,
|
||||
});
|
||||
|
||||
return res.send({ returnCode: "1" });
|
||||
}
|
||||
const body = UserRequest.parse(req.safeBody);
|
||||
const userId = body.userId;
|
||||
|
||||
const options = await GetByCardIdAndVersion(
|
||||
ChunithmUserOptionEx,
|
||||
|
@ -337,19 +302,8 @@ export class Chunithm extends BaseTitle {
|
|||
}
|
||||
|
||||
async handle_GetUserCharacterApi(req: Request, res: Response) {
|
||||
const userId = ParseStrInt(req.safeBody.userId);
|
||||
const nextIndex = ParseStrInt(req.safeBody.nextIndex);
|
||||
const maxCount = ParseStrInt(req.safeBody.maxCount);
|
||||
|
||||
if (!userId || !nextIndex || !maxCount) {
|
||||
logger.error("Invalid request: received userId/nextIndex/maxCount is not a number.", {
|
||||
userId: req.safeBody.userId,
|
||||
nextIndex: req.safeBody.nextIndex,
|
||||
maxCount: req.safeBody.maxCount,
|
||||
});
|
||||
|
||||
return res.send({ returnCode: "1" });
|
||||
}
|
||||
const body = UserPaginatedRequest.parse(req.safeBody);
|
||||
const { userId, nextIndex, maxCount } = body;
|
||||
|
||||
const [characters, characterCount] = await ChunithmUserCharacter.findAndCount({
|
||||
where: {
|
||||
|
@ -372,17 +326,8 @@ export class Chunithm extends BaseTitle {
|
|||
}
|
||||
|
||||
async handle_GetUserActivityApi(req: Request, res: Response) {
|
||||
const userId = ParseStrInt(req.safeBody.userId);
|
||||
const kind = ParseStrInt(req.safeBody.kind);
|
||||
|
||||
if (!userId || !kind) {
|
||||
logger.error("Invalid request: received userId/kind is not a number.", {
|
||||
userId: req.safeBody.userId,
|
||||
kind: req.safeBody.kind,
|
||||
});
|
||||
|
||||
return res.send({ returnCode: "1" });
|
||||
}
|
||||
const body = UserRequestWithKind.parse(req.safeBody);
|
||||
const { userId, kind } = body;
|
||||
|
||||
const activities = await ChunithmUserActivity.find({
|
||||
where: {
|
||||
|
@ -406,19 +351,8 @@ export class Chunithm extends BaseTitle {
|
|||
}
|
||||
|
||||
async handle_GetUserItemApi(req: Request, res: Response) {
|
||||
const userId = ParseStrInt(req.safeBody.userId);
|
||||
const nextIndex = ParseStrInt(req.safeBody.nextIndex);
|
||||
const maxCount = ParseStrInt(req.safeBody.maxCount);
|
||||
|
||||
if (!userId || !nextIndex || !maxCount) {
|
||||
logger.error("Invalid request: received userId/nextIndex/maxCount is not a number.", {
|
||||
userId: req.safeBody.userId,
|
||||
nextIndex: req.safeBody.nextIndex,
|
||||
maxCount: req.safeBody.maxCount,
|
||||
});
|
||||
|
||||
return res.send({ returnCode: "1" });
|
||||
}
|
||||
const body = UserPaginatedRequest.parse(req.safeBody);
|
||||
const { userId, nextIndex, maxCount } = body;
|
||||
|
||||
const itemKind = Math.trunc(nextIndex / 1e10);
|
||||
const skip = nextIndex % 1e10;
|
||||
|
@ -446,15 +380,8 @@ export class Chunithm extends BaseTitle {
|
|||
}
|
||||
|
||||
async handle_GetUserRecentRatingApi(req: Request, res: Response) {
|
||||
const userId = ParseStrInt(req.safeBody.userId);
|
||||
|
||||
if (!userId) {
|
||||
logger.error("Invalid request: received userId is not a number.", {
|
||||
userId: req.safeBody.userId,
|
||||
});
|
||||
|
||||
return res.send({ returnCode: "1" });
|
||||
}
|
||||
const body = UserRequest.parse(req.safeBody);
|
||||
const userId = body.userId;
|
||||
|
||||
const recentRatings = await ChunithmUserRecentRating.find({
|
||||
where: {
|
||||
|
|
|
@ -1,12 +1,9 @@
|
|||
import { ChunithmAmazon } from "./130-amazon";
|
||||
import { UserRequestWithKind } from "../validators";
|
||||
import { ChunithmUserFavoriteItem } from "external/db/entity/chunithm/item";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import { ParseStrInt } from "servers/titles/utils/string-checks";
|
||||
import { LessThanOrEqual } from "typeorm";
|
||||
import type { Request, Response } from "express";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
export class ChunithmAmazonPlus extends ChunithmAmazon {
|
||||
constructor(gameCode?: string, version?: string, servletName?: string) {
|
||||
super(gameCode ?? "SDBT", version ?? "135", servletName ?? "ChuniServlet");
|
||||
|
@ -34,17 +31,8 @@ export class ChunithmAmazonPlus extends ChunithmAmazon {
|
|||
}
|
||||
|
||||
async handle_GetUserFavoriteItemApi(req: Request, res: Response) {
|
||||
const userId = ParseStrInt(req.safeBody.userId);
|
||||
const kind = ParseStrInt(req.safeBody.kind);
|
||||
|
||||
if (!userId || !kind) {
|
||||
logger.error("Invalid request: received userId/kind is not a number.", {
|
||||
userId: req.safeBody.userId,
|
||||
kind: req.safeBody.kind,
|
||||
});
|
||||
|
||||
return res.send({ returnCode: "1" });
|
||||
}
|
||||
const body = UserRequestWithKind.parse(req.safeBody);
|
||||
const { userId, kind } = body;
|
||||
|
||||
const favorites = await ChunithmUserFavoriteItem.find({
|
||||
where: {
|
||||
|
|
|
@ -0,0 +1,113 @@
|
|||
import { ChunkLength, ToBuffer } from "./buffer";
|
||||
import onHeaders from "on-headers";
|
||||
import type { Response } from "express";
|
||||
import type { Transform } from "stream";
|
||||
|
||||
/**
|
||||
* Pipe the response through a series of transformations.
|
||||
*
|
||||
* Inspired by [node.js compression middleware](https://www.npmjs.com/package/compression).
|
||||
* @param res The response to transform.
|
||||
* @param transformers stream.Transform items.
|
||||
*/
|
||||
export function TransformResponse(res: Response, ...transformers: Array<Transform>) {
|
||||
const head = transformers[0];
|
||||
const tail = transformers.at(-1);
|
||||
|
||||
if (!head || !tail) {
|
||||
throw new Error("At least one stream.Transform must be provided");
|
||||
}
|
||||
|
||||
if (transformers.length > 1) {
|
||||
for (let i = 0; i < transformers.length - 1; i++) {
|
||||
const previous = transformers[i];
|
||||
const next = transformers[i + 1];
|
||||
|
||||
if (!previous || !next) {
|
||||
throw new Error("The provided transformers array have an undefined item.");
|
||||
}
|
||||
|
||||
previous.pipe(next);
|
||||
}
|
||||
}
|
||||
|
||||
let ended = false;
|
||||
|
||||
/* eslint-disable @typescript-eslint/unbound-method */
|
||||
const _on = res.on;
|
||||
const _write = res.write;
|
||||
const _end = res.end;
|
||||
/* eslint-enable @typescript-eslint/unbound-method */
|
||||
|
||||
/* eslint-disable prefer-arrow-callback */
|
||||
res.write = function write(chunk, encoding) {
|
||||
if (ended) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// @ts-expect-error Typical JavaScript monkey patching.
|
||||
if (!this._header && !this.headersSent) {
|
||||
this.writeHead(this.statusCode);
|
||||
}
|
||||
|
||||
const enc = typeof encoding === "function" ? undefined : encoding;
|
||||
|
||||
return head.write(ToBuffer(chunk, enc));
|
||||
};
|
||||
|
||||
// @ts-expect-error More monkey patching shenanigans. It works, we'll live.
|
||||
res.end = function end(chunk, encoding) {
|
||||
if (ended) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const enc = typeof encoding === "function" ? undefined : encoding;
|
||||
|
||||
// @ts-expect-error how do express people live like this?
|
||||
if (!this._header && !this.headersSent) {
|
||||
// estimate the length
|
||||
if (!this.getHeader("Content-Length")) {
|
||||
this.setHeader("Content-Length", ChunkLength(chunk, enc));
|
||||
}
|
||||
|
||||
this.writeHead(this.statusCode);
|
||||
}
|
||||
|
||||
// mark ended
|
||||
ended = true;
|
||||
|
||||
// write Buffer for Node.js 0.8
|
||||
// @ts-expect-error bruh
|
||||
return chunk ? head.end(ToBuffer(chunk, encoding)) : head.end();
|
||||
};
|
||||
|
||||
// @ts-expect-error zzz
|
||||
res.on = function on(event, listener) {
|
||||
if (event !== "drain") {
|
||||
return _on.call(this, event, listener);
|
||||
}
|
||||
|
||||
return head.on(event, listener);
|
||||
};
|
||||
/* eslint-enable prefer-arrow-callback */
|
||||
|
||||
onHeaders(res, () => {
|
||||
res.removeHeader("Content-Length");
|
||||
|
||||
tail.on("data", (chunk) => {
|
||||
// @ts-expect-error ugggh i hate monkeys
|
||||
if (!_write.call(res, chunk)) {
|
||||
head.pause();
|
||||
}
|
||||
});
|
||||
|
||||
tail.on("end", () => {
|
||||
// @ts-expect-error ugggh i hate monkeys
|
||||
_end.call(res);
|
||||
});
|
||||
|
||||
_on.call(res, "drain", () => {
|
||||
head.resume();
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Reference in New Issue