aboutsummaryrefslogtreecommitdiffhomepage
path: root/pkgs/server/src/middleware
diff options
context:
space:
mode:
Diffstat (limited to 'pkgs/server/src/middleware')
-rw-r--r--pkgs/server/src/middleware/auth.test.ts190
-rw-r--r--pkgs/server/src/middleware/auth.ts65
-rw-r--r--pkgs/server/src/middleware/error-handler.test.ts173
-rw-r--r--pkgs/server/src/middleware/error-handler.ts95
-rw-r--r--pkgs/server/src/middleware/index.ts2
5 files changed, 0 insertions, 525 deletions
diff --git a/pkgs/server/src/middleware/auth.test.ts b/pkgs/server/src/middleware/auth.test.ts
deleted file mode 100644
index 8c4286b..0000000
--- a/pkgs/server/src/middleware/auth.test.ts
+++ /dev/null
@@ -1,190 +0,0 @@
-import { Hono } from "hono";
-import { sign } from "hono/jwt";
-import { beforeEach, describe, expect, it } from "vitest";
-import { authMiddleware, getAuthUser } from "./auth";
-import { errorHandler } from "./error-handler";
-
-const JWT_SECRET = process.env.JWT_SECRET || "test-secret";
-
-interface ErrorResponse {
- error: {
- code: string;
- message: string;
- };
-}
-
-interface SuccessResponse {
- userId: string;
-}
-
-describe("authMiddleware", () => {
- let app: Hono;
-
- beforeEach(() => {
- app = new Hono();
- app.onError(errorHandler);
- app.use("/protected/*", authMiddleware);
- app.get("/protected/resource", (c) => {
- const user = getAuthUser(c);
- return c.json({ userId: user.id });
- });
- });
-
- it("allows access with valid token", async () => {
- const now = Math.floor(Date.now() / 1000);
- const token = await sign(
- {
- sub: "user-123",
- iat: now,
- exp: now + 3600,
- },
- JWT_SECRET,
- );
-
- const res = await app.request("/protected/resource", {
- headers: {
- Authorization: `Bearer ${token}`,
- },
- });
-
- expect(res.status).toBe(200);
- const body = (await res.json()) as SuccessResponse;
- expect(body.userId).toBe("user-123");
- });
-
- it("returns 401 when Authorization header is missing", async () => {
- const res = await app.request("/protected/resource");
-
- expect(res.status).toBe(401);
- const body = (await res.json()) as ErrorResponse;
- expect(body.error.code).toBe("MISSING_AUTH");
- });
-
- it("returns 401 for invalid Authorization format", async () => {
- const res = await app.request("/protected/resource", {
- headers: {
- Authorization: "Basic sometoken",
- },
- });
-
- expect(res.status).toBe(401);
- const body = (await res.json()) as ErrorResponse;
- expect(body.error.code).toBe("INVALID_AUTH_FORMAT");
- });
-
- it("returns 401 for empty Bearer token", async () => {
- const res = await app.request("/protected/resource", {
- headers: {
- Authorization: "Bearer",
- },
- });
-
- expect(res.status).toBe(401);
- const body = (await res.json()) as ErrorResponse;
- expect(body.error.code).toBe("INVALID_AUTH_FORMAT");
- });
-
- it("returns 401 for expired token", async () => {
- const now = Math.floor(Date.now() / 1000);
- const token = await sign(
- {
- sub: "user-123",
- iat: now - 7200,
- exp: now - 3600, // expired 1 hour ago
- },
- JWT_SECRET,
- );
-
- const res = await app.request("/protected/resource", {
- headers: {
- Authorization: `Bearer ${token}`,
- },
- });
-
- expect(res.status).toBe(401);
- const body = (await res.json()) as ErrorResponse;
- expect(body.error.code).toBe("INVALID_TOKEN");
- });
-
- it("returns 401 for invalid token", async () => {
- const res = await app.request("/protected/resource", {
- headers: {
- Authorization: "Bearer invalid.token.here",
- },
- });
-
- expect(res.status).toBe(401);
- const body = (await res.json()) as ErrorResponse;
- expect(body.error.code).toBe("INVALID_TOKEN");
- });
-
- it("returns 401 for token signed with wrong secret", async () => {
- const now = Math.floor(Date.now() / 1000);
- const token = await sign(
- {
- sub: "user-123",
- iat: now,
- exp: now + 3600,
- },
- "wrong-secret",
- );
-
- const res = await app.request("/protected/resource", {
- headers: {
- Authorization: `Bearer ${token}`,
- },
- });
-
- expect(res.status).toBe(401);
- const body = (await res.json()) as ErrorResponse;
- expect(body.error.code).toBe("INVALID_TOKEN");
- });
-});
-
-describe("getAuthUser", () => {
- it("returns user from context when authenticated", async () => {
- const app = new Hono();
- app.onError(errorHandler);
- app.use("/test", authMiddleware);
- app.get("/test", (c) => {
- const user = getAuthUser(c);
- return c.json({ id: user.id });
- });
-
- const now = Math.floor(Date.now() / 1000);
- const token = await sign(
- {
- sub: "test-user-456",
- iat: now,
- exp: now + 3600,
- },
- JWT_SECRET,
- );
-
- const res = await app.request("/test", {
- headers: {
- Authorization: `Bearer ${token}`,
- },
- });
-
- expect(res.status).toBe(200);
- const body = (await res.json()) as { id: string };
- expect(body.id).toBe("test-user-456");
- });
-
- it("throws when user is not in context", async () => {
- const app = new Hono();
- app.onError(errorHandler);
- // Note: no authMiddleware applied
- app.get("/unprotected", (c) => {
- const user = getAuthUser(c);
- return c.json({ id: user.id });
- });
-
- const res = await app.request("/unprotected");
-
- expect(res.status).toBe(401);
- const body = (await res.json()) as ErrorResponse;
- expect(body.error.code).toBe("NOT_AUTHENTICATED");
- });
-});
diff --git a/pkgs/server/src/middleware/auth.ts b/pkgs/server/src/middleware/auth.ts
deleted file mode 100644
index c295834..0000000
--- a/pkgs/server/src/middleware/auth.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-import type { Context, Next } from "hono";
-import { verify } from "hono/jwt";
-import { Errors } from "./error-handler";
-
-const JWT_SECRET = process.env.JWT_SECRET;
-if (!JWT_SECRET) {
- throw new Error("JWT_SECRET environment variable is required");
-}
-
-export interface AuthUser {
- id: string;
-}
-
-interface JWTPayload {
- sub: string;
- iat: number;
- exp: number;
-}
-
-/**
- * Auth middleware that validates JWT tokens from Authorization header
- * Sets the authenticated user in context variables
- */
-export async function authMiddleware(c: Context, next: Next) {
- const authHeader = c.req.header("Authorization");
-
- if (!authHeader) {
- throw Errors.unauthorized("Missing Authorization header", "MISSING_AUTH");
- }
-
- if (!authHeader.startsWith("Bearer ")) {
- throw Errors.unauthorized(
- "Invalid Authorization header format",
- "INVALID_AUTH_FORMAT",
- );
- }
-
- const token = authHeader.slice(7);
-
- try {
- const payload = (await verify(token, JWT_SECRET)) as unknown as JWTPayload;
-
- const user: AuthUser = {
- id: payload.sub,
- };
-
- c.set("user", user);
-
- await next();
- } catch {
- throw Errors.unauthorized("Invalid or expired token", "INVALID_TOKEN");
- }
-}
-
-/**
- * Helper function to get the authenticated user from context
- * Throws if user is not authenticated
- */
-export function getAuthUser(c: Context): AuthUser {
- const user = c.get("user") as AuthUser | undefined;
- if (!user) {
- throw Errors.unauthorized("Not authenticated", "NOT_AUTHENTICATED");
- }
- return user;
-}
diff --git a/pkgs/server/src/middleware/error-handler.test.ts b/pkgs/server/src/middleware/error-handler.test.ts
deleted file mode 100644
index 21d6fc1..0000000
--- a/pkgs/server/src/middleware/error-handler.test.ts
+++ /dev/null
@@ -1,173 +0,0 @@
-import { Hono } from "hono";
-import { describe, expect, it } from "vitest";
-import { AppError, Errors, errorHandler } from "./error-handler";
-
-function createTestApp() {
- const app = new Hono();
- app.onError(errorHandler);
- return app;
-}
-
-describe("errorHandler", () => {
- describe("AppError handling", () => {
- it("returns correct status and message for AppError", async () => {
- const app = createTestApp();
- app.get("/test", () => {
- throw new AppError("Custom error", 400, "CUSTOM_ERROR");
- });
-
- const res = await app.request("/test");
- expect(res.status).toBe(400);
- expect(await res.json()).toEqual({
- error: {
- message: "Custom error",
- code: "CUSTOM_ERROR",
- },
- });
- });
-
- it("uses default values for AppError", async () => {
- const app = createTestApp();
- app.get("/test", () => {
- throw new AppError("Something went wrong");
- });
-
- const res = await app.request("/test");
- expect(res.status).toBe(500);
- expect(await res.json()).toEqual({
- error: {
- message: "Something went wrong",
- code: "INTERNAL_ERROR",
- },
- });
- });
- });
-
- describe("Errors factory functions", () => {
- it("handles badRequest error", async () => {
- const app = createTestApp();
- app.get("/test", () => {
- throw Errors.badRequest("Invalid input");
- });
-
- const res = await app.request("/test");
- expect(res.status).toBe(400);
- expect(await res.json()).toEqual({
- error: {
- message: "Invalid input",
- code: "BAD_REQUEST",
- },
- });
- });
-
- it("handles unauthorized error", async () => {
- const app = createTestApp();
- app.get("/test", () => {
- throw Errors.unauthorized();
- });
-
- const res = await app.request("/test");
- expect(res.status).toBe(401);
- expect(await res.json()).toEqual({
- error: {
- message: "Unauthorized",
- code: "UNAUTHORIZED",
- },
- });
- });
-
- it("handles forbidden error", async () => {
- const app = createTestApp();
- app.get("/test", () => {
- throw Errors.forbidden("Access denied");
- });
-
- const res = await app.request("/test");
- expect(res.status).toBe(403);
- expect(await res.json()).toEqual({
- error: {
- message: "Access denied",
- code: "FORBIDDEN",
- },
- });
- });
-
- it("handles notFound error", async () => {
- const app = createTestApp();
- app.get("/test", () => {
- throw Errors.notFound("Resource not found");
- });
-
- const res = await app.request("/test");
- expect(res.status).toBe(404);
- expect(await res.json()).toEqual({
- error: {
- message: "Resource not found",
- code: "NOT_FOUND",
- },
- });
- });
-
- it("handles conflict error", async () => {
- const app = createTestApp();
- app.get("/test", () => {
- throw Errors.conflict("Already exists");
- });
-
- const res = await app.request("/test");
- expect(res.status).toBe(409);
- expect(await res.json()).toEqual({
- error: {
- message: "Already exists",
- code: "CONFLICT",
- },
- });
- });
-
- it("handles validationError", async () => {
- const app = createTestApp();
- app.get("/test", () => {
- throw Errors.validationError("Invalid data");
- });
-
- const res = await app.request("/test");
- expect(res.status).toBe(422);
- expect(await res.json()).toEqual({
- error: {
- message: "Invalid data",
- code: "VALIDATION_ERROR",
- },
- });
- });
-
- it("handles internal error", async () => {
- const app = createTestApp();
- app.get("/test", () => {
- throw Errors.internal("Database connection failed");
- });
-
- const res = await app.request("/test");
- expect(res.status).toBe(500);
- expect(await res.json()).toEqual({
- error: {
- message: "Database connection failed",
- code: "INTERNAL_ERROR",
- },
- });
- });
- });
-
- describe("unknown error handling", () => {
- it("handles generic Error with 500 status", async () => {
- const app = createTestApp();
- app.get("/test", () => {
- throw new Error("Unexpected error");
- });
-
- const res = await app.request("/test");
- expect(res.status).toBe(500);
- const body = (await res.json()) as { error: { code: string } };
- expect(body.error.code).toBe("INTERNAL_ERROR");
- });
- });
-});
diff --git a/pkgs/server/src/middleware/error-handler.ts b/pkgs/server/src/middleware/error-handler.ts
deleted file mode 100644
index 7b92940..0000000
--- a/pkgs/server/src/middleware/error-handler.ts
+++ /dev/null
@@ -1,95 +0,0 @@
-import type { Context } from "hono";
-import { HTTPException } from "hono/http-exception";
-import type { ContentfulStatusCode } from "hono/utils/http-status";
-
-/**
- * Application-specific error with status code and optional details
- */
-export class AppError extends Error {
- readonly statusCode: ContentfulStatusCode;
- readonly code: string;
-
- constructor(
- message: string,
- statusCode: ContentfulStatusCode = 500,
- code = "INTERNAL_ERROR",
- ) {
- super(message);
- this.name = "AppError";
- this.statusCode = statusCode;
- this.code = code;
- }
-}
-
-/**
- * Common error factory functions
- */
-export const Errors = {
- badRequest: (message = "Bad request", code = "BAD_REQUEST") =>
- new AppError(message, 400, code),
-
- unauthorized: (message = "Unauthorized", code = "UNAUTHORIZED") =>
- new AppError(message, 401, code),
-
- forbidden: (message = "Forbidden", code = "FORBIDDEN") =>
- new AppError(message, 403, code),
-
- notFound: (message = "Not found", code = "NOT_FOUND") =>
- new AppError(message, 404, code),
-
- conflict: (message = "Conflict", code = "CONFLICT") =>
- new AppError(message, 409, code),
-
- validationError: (message = "Validation failed", code = "VALIDATION_ERROR") =>
- new AppError(message, 422, code),
-
- internal: (message = "Internal server error", code = "INTERNAL_ERROR") =>
- new AppError(message, 500, code),
-};
-
-interface ErrorResponse {
- error: {
- message: string;
- code: string;
- };
-}
-
-/**
- * Global error handler middleware for Hono
- */
-export function errorHandler(err: Error, c: Context): Response {
- // Handle AppError
- if (err instanceof AppError) {
- const response: ErrorResponse = {
- error: {
- message: err.message,
- code: err.code,
- },
- };
- return c.json(response, err.statusCode);
- }
-
- // Handle Hono's HTTPException
- if (err instanceof HTTPException) {
- const response: ErrorResponse = {
- error: {
- message: err.message,
- code: "HTTP_ERROR",
- },
- };
- return c.json(response, err.status as ContentfulStatusCode);
- }
-
- // Handle unknown errors
- console.error("Unhandled error:", err);
- const response: ErrorResponse = {
- error: {
- message:
- process.env.NODE_ENV === "production"
- ? "Internal server error"
- : err.message,
- code: "INTERNAL_ERROR",
- },
- };
- return c.json(response, 500);
-}
diff --git a/pkgs/server/src/middleware/index.ts b/pkgs/server/src/middleware/index.ts
deleted file mode 100644
index 57de4dd..0000000
--- a/pkgs/server/src/middleware/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { type AuthUser, authMiddleware, getAuthUser } from "./auth";
-export { AppError, Errors, errorHandler } from "./error-handler";