Skip to content

Commit

Permalink
feat: middleware 🎉
Browse files Browse the repository at this point in the history
  • Loading branch information
SeanCassiere committed Jun 23, 2024
1 parent 5aa3f51 commit d755e7e
Show file tree
Hide file tree
Showing 6 changed files with 109 additions and 33 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"dotenv": "^16.4.5",
"drizzle-orm": "^0.31.2",
"hono": "^4.4.7",
"hono-rate-limiter": "^0.3.0",
"postgres": "^3.4.4",
"zod": "^3.23.8"
}
Expand Down
12 changes: 12 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 28 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { Hono } from "hono";
import { cors } from "hono/cors";
import { csrf } from "hono/csrf";
import { etag } from "hono/etag";
import { secureHeaders } from "hono/secure-headers";
import { timeout } from "hono/timeout";
import { logger } from "hono/logger";
import { rateLimiter } from "hono-rate-limiter";
import { serve } from "@hono/node-server";

import v2Router from "@/v2";
Expand All @@ -9,6 +16,27 @@ import type { ServerContext } from "@/types/hono";
const packageJson = require("../package.json");

const app = new Hono<ServerContext>();
app.use(cors());
app.use(csrf());
app.use(etag());
app.use(logger());
app.use(secureHeaders());

const limiter = rateLimiter({
windowMs: 15 * 60 * 1000, // 15 minutes
limit: 100,
standardHeaders: "draft-6",
keyGenerator: (c) => c.req.header("x-service-id") ?? c.req.header("x-forwarded-for") ?? "",
});
app.use(limiter);

app.use("*", async (c, next) => {
c.set("service", null);

await next();
});

app.use("/api/", timeout(5000));

app.route("/api/v2", v2Router);

Expand Down
10 changes: 9 additions & 1 deletion src/types/hono.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import type { services as servicesTable } from "@/config/db/schema";

type Service = typeof servicesTable.$inferSelect;

type Variables = {
service: Service | null;
};

export type ServerContext = {
Bindings: {};
Variables: {};
Variables: Variables;
};
55 changes: 52 additions & 3 deletions src/utils/server-helpers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { db } from "@/config/db";
import { createFactory } from "hono/factory";
import type { Context } from "hono";

import { db } from "@/config/db";
import { ENDPOINT_MESSAGES } from "./messages";

/**
* Takes a URL and returns an object with the query string parameters, multiple of the same key will be an array
*/
Expand All @@ -24,7 +27,7 @@ export function parseSearchParams(url: string): Record<string, string | string[]
/**
* Get the service ID from the request headers
*/
export function getServiceId(c: Context): string | null {
function getServiceId(c: Context): string | null {
const header = c.req.header("x-service-id");
return header ?? null;
}
Expand All @@ -34,10 +37,56 @@ export function getServiceId(c: Context): string | null {
* @param serviceId The ID of the service to find
* @param mustBeAdmin If true, the service must be an admin service
*/
export async function getService(serviceId: string, opts = { mustBeAdmin: false }) {
async function getService(serviceId: string, opts = { mustBeAdmin: false }) {
const service = await db.query.services.findFirst({
where: (fields, { and, eq }) =>
and(eq(fields.id, serviceId), ...(opts.mustBeAdmin ? [eq(fields.isAdmin, true)] : [])),
});
return service ?? null;
}

const factory = createFactory();

/**
* Middleware to validate that a service ID is provided and that the service exists
*/
export const serviceValidation = factory.createMiddleware(async (c, next) => {
const serviceId = getServiceId(c);

if (!serviceId) {
c.status(401);
return c.json({ success: false, message: ENDPOINT_MESSAGES.ServiceIdHeaderNotProvided });
}

const service = await getService(serviceId);

if (!service) {
c.status(403);
return c.json({ success: false, message: ENDPOINT_MESSAGES.ServiceDoesNotExistOrDoesNotHaveNecessaryRights });
}

c.set("service", service);
await next();
});

/**
* Middleware to validate that a service ID is provided and that the service exists and is an admin service
*/
export const adminServiceValidation = factory.createMiddleware(async (c, next) => {
const serviceId = getServiceId(c);

if (!serviceId) {
c.status(401);
return c.json({ success: false, message: ENDPOINT_MESSAGES.ServiceIdHeaderNotProvided });
}

const service = await getService(serviceId, { mustBeAdmin: true });

if (!service) {
c.status(403);
return c.json({ success: false, message: ENDPOINT_MESSAGES.ServiceDoesNotExistOrDoesNotHaveNecessaryRights });
}

c.set("service", service);
await next();
});
36 changes: 7 additions & 29 deletions src/v2/logging/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Hono } from "hono";

import { parseSearchParams, getServiceId, getService } from "@/utils/server-helpers";
import { parseSearchParams, serviceValidation } from "@/utils/server-helpers";
import { db } from "@/config/db";
import { ENDPOINT_MESSAGES } from "@/utils/messages";
import type { ServerContext } from "@/types/hono";
Expand All @@ -16,20 +16,9 @@ const app = new Hono<ServerContext>();
* @public
* Get all log entries
*/
app.get("/", async (c) => {
const serviceId = getServiceId(c);

if (!serviceId) {
c.status(401);
return c.json({ success: false, message: ENDPOINT_MESSAGES.ServiceIdHeaderNotProvided });
}

const service = await getService(serviceId);

if (!service) {
c.status(403);
return c.json({ success: false, message: ENDPOINT_MESSAGES.ServiceDoesNotExistOrDoesNotHaveNecessaryRights });
}
app.get("/", serviceValidation, async (c) => {
const service = c.var.service!;
const serviceId = service.id;

const searchQuery = parseSearchParams(c.req.url);
const searchResult = getLogsFiltersSchema.safeParse(searchQuery);
Expand Down Expand Up @@ -63,26 +52,15 @@ app.get("/", async (c) => {
* @public
* Create a log entry
*/
app.post("/", async (c) => {
const serviceId = getServiceId(c);
app.post("/", serviceValidation, async (c) => {
const service = c.var.service!;
const serviceId = service.id;

if (env.FREEZE_DB_WRITES) {
c.status(503);
return c.json({ success: false, message: ENDPOINT_MESSAGES.DBWritesFrozen });
}

if (!serviceId) {
c.status(401);
return c.json({ success: false, message: ENDPOINT_MESSAGES.ServiceIdHeaderNotProvided });
}

const service = await getService(serviceId);

if (!service) {
c.status(403);
return c.json({ success: false, message: ENDPOINT_MESSAGES.ServiceDoesNotExistOrDoesNotHaveNecessaryRights });
}

const body = await c.req.json();
const bodyResult = createLogSchema.safeParse(body);

Expand Down

0 comments on commit d755e7e

Please sign in to comment.