aboutsummaryrefslogtreecommitdiffstats
path: root/lib/app.ts
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-02-28 14:14:30 -0800
committerAdam Soltys <[email protected]>2026-02-28 14:14:30 -0800
commita1632ae402d11fdd7ffc360de97c55e0b4fad677 (patch)
treeaad5a1ea73f95dcd055c1d0b4ebf2708c36b4e01 /lib/app.ts
parent76c7e52172f3dc25ee109434157d4d982ad9d2bd (diff)
downloadcoinos-server-a1632ae402d11fdd7ffc360de97c55e0b4fad677.tar.gz
coinos-server-a1632ae402d11fdd7ffc360de97c55e0b4fad677.zip
migrate to hono
Diffstat (limited to 'lib/app.ts')
-rw-r--r--lib/app.ts225
1 files changed, 111 insertions, 114 deletions
diff --git a/lib/app.ts b/lib/app.ts
index da29d9fe..ec810288 100644
--- a/lib/app.ts
+++ b/lib/app.ts
@@ -1,34 +1,88 @@
-import config from "$config";
-import cors from "@fastify/cors";
-import fastifyProxy from "@fastify/http-proxy";
-import fastifyMultipart from "@fastify/multipart";
-import fastifyPassport from "@fastify/passport";
-import fastifyRateLimit from "@fastify/rate-limit";
-import fastifySecureSession from "@fastify/secure-session";
-import fastifyStatic from "@fastify/static";
-import fastify from "fastify";
+import { Hono } from "hono";
+import { cors } from "hono/cors";
+import { serveStatic } from "hono/bun";
import pino from "pino";
-import * as path from "path";
+const app = new Hono();
-import { jwtStrategy } from "$lib/auth";
+const reqLogger = pino(pino.destination("req"));
+const resLogger = pino(pino.destination("res"));
+
+// CORS
+app.use(
+ "*",
+ cors({
+ origin: (origin) => origin || "*",
+ credentials: true,
+ }),
+);
+
+// Static files
+app.use("/public/*", serveStatic({ root: "/home/bun/app/data/uploads", rewriteRequestPath: (p) => p.replace("/public", "") }));
+
+// Rate limiting
+const rateLimits = new Map<string, { count: number; reset: number }>();
+const strictLimits = new Map<string, { count: number; reset: number }>();
-const app = fastify({
- logger: true,
- disableRequestLogging: true,
- routerOptions: { maxParamLength: 500 },
+app.use("*", async (c, next) => {
+ const url = c.req.path;
+
+ // Skip rate limiting for public assets
+ if (url.includes("public")) return next();
+
+ const ip = (c.req.header("cf-connecting-ip") as string) || c.env?.ip || "unknown";
+ const ua = c.req.header("user-agent") || "unknown-ua";
+ const rateLimitBy = c.req.header("rate-limit-by");
+ const key = rateLimitBy === "ua" ? ua : ip;
+ const now = Date.now();
+
+ // General rate limit: 2000 req / 2s
+ const gen = rateLimits.get(key);
+ if (gen && now < gen.reset) {
+ gen.count++;
+ if (gen.count > 2000) {
+ return c.json(
+ { statusCode: 429, error: "Too Many Requests", message: "Rate limit exceeded, retry in 2 seconds" },
+ 429,
+ );
+ }
+ } else {
+ rateLimits.set(key, { count: 1, reset: now + 2000 });
+ }
+
+ // Strict rate limit for /login and /send: 10 req / 10s
+ const isStrict = url.includes("/login") || url.includes("/send");
+ if (isStrict) {
+ const strictKey = `strict:${ua}`;
+ const s = strictLimits.get(strictKey);
+ if (s && now < s.reset) {
+ s.count++;
+ if (s.count > 10) {
+ return c.json(
+ { statusCode: 429, error: "Too Many Requests", message: "Rate limit exceeded" },
+ 429,
+ );
+ }
+ } else {
+ strictLimits.set(strictKey, { count: 1, reset: now + 10000 });
+ }
+ }
+
+ return next();
});
-const reqLogger = pino(pino.destination("req"));
-const resLogger = pino(pino.destination("res"));
+// Clean up rate limit maps periodically
+setInterval(() => {
+ const now = Date.now();
+ for (const [k, v] of rateLimits) if (now >= v.reset) rateLimits.delete(k);
+ for (const [k, v] of strictLimits) if (now >= v.reset) strictLimits.delete(k);
+}, 5000);
-// app.addHook("onRequest", async (req) => {
-// reqLogger.info({ url: req.raw.url, id: req.id });
-// });
-//
+// Request logging
+app.use("*", async (c, next) => {
+ const start = Date.now();
+ const url = c.req.path;
-app.addHook("preHandler", async (req) => {
- const url = req.raw.url;
const ignore = [
"/ws",
"/me",
@@ -42,28 +96,36 @@ app.addHook("preHandler", async (req) => {
"/accounts",
"/contacts",
];
- if (ignore.some((path) => url.startsWith(path))) return;
- if (req.method === "GET" && url.startsWith("/users")) return;
- const xff = req.headers["x-forwarded-for"];
- const forwardedIp = Array.isArray(xff) ? xff[0] : xff?.split(",")[0]?.trim();
- const ip = req.headers["cf-connecting-ip"] || forwardedIp || req.ip;
+ const shouldLog = !ignore.some((path) => url.startsWith(path)) &&
+ !(c.req.method === "GET" && url.startsWith("/users"));
- reqLogger.info({
- method: req.method,
- url,
- ip,
- headers: req.headers,
- query: req.query,
- body: req.body,
- user: (req.user as any)?.username,
- id: req.id,
- });
-});
+ if (shouldLog) {
+ const xff = c.req.header("x-forwarded-for");
+ const forwardedIp = xff?.split(",")[0]?.trim();
+ const ip = c.req.header("cf-connecting-ip") || forwardedIp || c.env?.ip || "unknown";
-app.addHook("onResponse", async (req, reply) => {
- const rawCookies = req.raw.headers.cookie || "";
+ let body;
+ // Only parse body for non-GET requests
+ if (c.req.method !== "GET" && c.req.method !== "HEAD") {
+ try {
+ body = await c.req.raw.clone().json();
+ } catch {}
+ }
+
+ reqLogger.info({
+ method: c.req.method,
+ url,
+ ip,
+ query: c.req.query(),
+ body,
+ user: c.get("user")?.username,
+ });
+ }
+ await next();
+
+ const rawCookies = c.req.header("cookie") || "";
const cookies: any = rawCookies.split(";").reduce((acc, cookie) => {
const [key, value] = cookie.split("=").map((s) => s.trim());
if (key && value) acc[key] = value;
@@ -71,84 +133,19 @@ app.addHook("onResponse", async (req, reply) => {
}, {});
resLogger.info({
- id: req.id,
- url: req.raw.url,
- statusCode: reply.raw.statusCode,
- durationMs: reply.elapsedTime,
+ url,
+ statusCode: c.res.status,
+ durationMs: Date.now() - start,
username: cookies.username,
});
});
-app.register(fastifyRateLimit, {
- allowList: (req) => req.raw.url?.includes("public"),
- max: 2000,
- timeWindow: 2000,
- keyGenerator: (req) => {
- const ip = (req.headers["cf-connecting-ip"] as string) || req.ip;
- const ua = req.headers["user-agent"] || "unknown-ua";
- return req.headers["rate-limit-by"] === "ua" ? ua : ip;
- },
- errorResponseBuilder: () => {
- return {
- statusCode: 429,
- error: "Too Many Requests",
- message: "Rate limit exceeded, retry in 2 seconds",
- };
- },
-});
-
-app.register(fastifyRateLimit, {
- allowList: (req) => {
- const url = req.raw.url || "";
- const matches = url.includes("/login") || url.includes("/send");
- return !matches;
- },
- max: 10,
- timeWindow: 10000,
- keyGenerator: (req) => (req.headers["user-agent"] as string) || "unknown-ua",
- errorResponseBuilder: () => ({
- statusCode: 429,
- error: "Too Many Requests",
- message: "Rate limit exceeded",
- }),
-});
-
-app.register(fastifyProxy, {
- upstream: "http://localhost:3120",
- prefix: "/ws",
- rewritePrefix: "/ws",
- websocket: true,
-});
-
-app.register(fastifySecureSession, {
- key: Buffer.from(config.jwt, "hex"),
-});
-
-app.register(fastifyPassport.initialize());
-app.register(fastifyPassport.secureSession());
-
-fastifyPassport.use("jwt", jwtStrategy);
-
-app.register(fastifyMultipart, { limits: { fileSize: 10 ** 7 } });
-
-app.register(fastifyStatic, {
- root: path.join("/home/bun/app/data/uploads"),
- prefix: "/public/",
-});
-
-await app.register(cors, {
- origin: true,
- credentials: true,
-});
-
-app.setErrorHandler((error, _, reply) => {
- if (error instanceof fastify.errorCodes.FST_ERR_BAD_STATUS_CODE) {
- reply.status(500).send({ ok: false });
- } else {
- reply.send(error);
- }
+// Error handler
+app.onError((err, c) => {
+ return c.json({ ok: false }, 500);
});
-app.setNotFoundHandler((_, reply) => reply.code(404).type("text/html").send("Not Found"));
+// Not found handler
+app.notFound((c) => c.text("Not Found", 404));
export default app;