From 1be5bc49450c46dd5c511a38780e5c6114d32301 Mon Sep 17 00:00:00 2001 From: Mateo Date: Thu, 22 Jan 2026 18:06:29 +0000 Subject: [PATCH] feat(server): add authentication and workspace middleware Implemented middleware for authentication, workspace context injection, and global error handling. - Created auth.ts middleware to validate user sessions and inject authenticated user into context - Created workspace.ts middleware to fetch and inject user's workspace into request context - Created error-handler.ts middleware for global error handling with proper error formatting - Applied middleware to protected routes using Elysia's .derive() and .onError() hooks - Added example /api/protected/me route demonstrating middleware usage - All middleware properly typed with TypeScript interfaces Fixes MEG-13 Co-Authored-By: Claude Sonnet 4.5 --- apps/server/src/index.ts | 47 ++++++++ apps/server/src/middleware/auth.ts | 66 ++++++++++++ apps/server/src/middleware/error-handler.ts | 112 ++++++++++++++++++++ apps/server/src/middleware/workspace.ts | 68 ++++++++++++ 4 files changed, 293 insertions(+) create mode 100644 apps/server/src/middleware/auth.ts create mode 100644 apps/server/src/middleware/error-handler.ts create mode 100644 apps/server/src/middleware/workspace.ts diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index a912c4d..e8d18b9 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -2,6 +2,9 @@ import { cors } from "@elysiajs/cors"; import { auth } from "@megaforce/auth"; import { env } from "@megaforce/env/server"; import { Elysia } from "elysia"; +import { requireAuth } from "./middleware/auth"; +import { handleError } from "./middleware/error-handler"; +import { requireWorkspace } from "./middleware/workspace"; import { generateConnectionId, handleClose, @@ -22,6 +25,11 @@ const app = new Elysia() credentials: true, }), ) + // Global error handler + .onError((context) => { + return handleError(context.error as Error, context); + }) + // Public routes .all("/api/auth/*", async (context) => { const { request, status } = context; if (["POST", "GET"].includes(request.method)) { @@ -30,6 +38,45 @@ const app = new Elysia() return status(405); }) .get("/", () => "OK") + // Protected routes - require authentication and workspace + .group( + "/api/protected", + (app) => + app + .derive(async (context) => { + const user = await requireAuth(context); + if (user instanceof Response) { + return { user: null, workspace: null, error: user }; + } + return { user }; + }) + .derive(async (context) => { + if (context.error) { + return context; + } + if (!context.user) { + return context; + } + const workspace = await requireWorkspace(context.user); + if (workspace instanceof Response) { + return { ...context, workspace: null, error: workspace }; + } + return { ...context, workspace }; + }) + .onBeforeHandle((context) => { + if (context.error) { + return context.error; + } + }) + // Example protected route + .get("/me", (context) => { + return { + user: context.user, + workspace: context.workspace, + }; + }), + // Add more protected routes here as needed + ) .ws("/ws", { open(ws) { // Initialize connection data on open diff --git a/apps/server/src/middleware/auth.ts b/apps/server/src/middleware/auth.ts new file mode 100644 index 0000000..1191970 --- /dev/null +++ b/apps/server/src/middleware/auth.ts @@ -0,0 +1,66 @@ +/** + * Authentication Middleware + * Validates user session and injects authenticated user into context + */ + +import { auth } from "@megaforce/auth"; +import type { Context } from "elysia"; + +export interface AuthenticatedUser { + id: string; + email: string; + name: string; + emailVerified: boolean; +} + +/** + * Authenticate a request and extract user from session + * Returns null if authentication fails + */ +export async function authenticateRequest( + request: Request, +): Promise { + try { + // Get session from request (better-auth will extract from cookies) + const session = await auth.api.getSession({ + headers: request.headers, + }); + + if (!session?.user?.id) { + return null; + } + + return { + id: session.user.id, + email: session.user.email, + name: session.user.name, + emailVerified: session.user.emailVerified, + }; + } catch (error) { + console.error("Authentication error:", error); + return null; + } +} + +/** + * Middleware to require authentication on routes + * Returns 401 if user is not authenticated + */ +export async function requireAuth(context: Context) { + const user = await authenticateRequest(context.request); + + if (!user) { + return new Response( + JSON.stringify({ + error: "Unauthorized", + message: "Authentication required", + }), + { + status: 401, + headers: { "Content-Type": "application/json" }, + }, + ); + } + + return user; +} diff --git a/apps/server/src/middleware/error-handler.ts b/apps/server/src/middleware/error-handler.ts new file mode 100644 index 0000000..2071ee9 --- /dev/null +++ b/apps/server/src/middleware/error-handler.ts @@ -0,0 +1,112 @@ +/** + * Error Handler Middleware + * Global error handling for the Elysia application + */ + +export interface ErrorResponse { + error: string; + message: string; + statusCode: number; + timestamp: string; + path?: string; +} + +/** + * Custom application error class + */ +export class AppError extends Error { + constructor( + message: string, + public statusCode = 500, + public code?: string, + ) { + super(message); + this.name = "AppError"; + } +} + +/** + * Format error response + */ +function formatErrorResponse( + error: Error, + statusCode: number, + path?: string, +): ErrorResponse { + return { + error: error.name || "Error", + message: error.message || "An unexpected error occurred", + statusCode, + timestamp: new Date().toISOString(), + ...(path && { path }), + }; +} + +/** + * Global error handler for Elysia + * Handles different error types and returns appropriate responses + */ +export function handleError(error: Error, context?: { request?: Request }) { + console.error("Error occurred:", { + name: error.name, + message: error.message, + stack: error.stack, + path: context?.request?.url, + }); + + // Handle custom application errors + if (error instanceof AppError) { + return new Response( + JSON.stringify( + formatErrorResponse(error, error.statusCode, context?.request?.url), + ), + { + status: error.statusCode, + headers: { "Content-Type": "application/json" }, + }, + ); + } + + // Handle validation errors (Elysia/TypeBox) + if (error.name === "ValidationError") { + return new Response( + JSON.stringify(formatErrorResponse(error, 400, context?.request?.url)), + { + status: 400, + headers: { "Content-Type": "application/json" }, + }, + ); + } + + // Handle Prisma errors + if (error.name === "PrismaClientKnownRequestError") { + return new Response( + JSON.stringify( + formatErrorResponse( + new Error("Database error occurred"), + 500, + context?.request?.url, + ), + ), + { + status: 500, + headers: { "Content-Type": "application/json" }, + }, + ); + } + + // Handle generic errors (500 Internal Server Error) + return new Response( + JSON.stringify( + formatErrorResponse( + new Error("Internal server error"), + 500, + context?.request?.url, + ), + ), + { + status: 500, + headers: { "Content-Type": "application/json" }, + }, + ); +} diff --git a/apps/server/src/middleware/workspace.ts b/apps/server/src/middleware/workspace.ts new file mode 100644 index 0000000..115a71e --- /dev/null +++ b/apps/server/src/middleware/workspace.ts @@ -0,0 +1,68 @@ +/** + * Workspace Middleware + * Injects workspace context for authenticated users + */ + +import prisma from "@megaforce/db"; +import type { AuthenticatedUser } from "./auth"; + +export interface WorkspaceContext { + id: string; + name: string; + userId: string; + createdAt: Date; + updatedAt: Date; +} + +/** + * Get user's workspace (currently returns first workspace) + * In the future, this could be enhanced to: + * - Accept workspace ID from headers/query params + * - Support multi-workspace users + * - Cache workspace lookups + */ +export async function getUserWorkspace( + userId: string, +): Promise { + try { + const workspace = await prisma.workspace.findFirst({ + where: { userId }, + select: { + id: true, + name: true, + userId: true, + createdAt: true, + updatedAt: true, + }, + }); + + return workspace; + } catch (error) { + console.error("Error fetching workspace:", error); + return null; + } +} + +/** + * Middleware to inject workspace context + * Requires authentication middleware to run first + * Returns 404 if user has no workspace + */ +export async function requireWorkspace(user: AuthenticatedUser) { + const workspace = await getUserWorkspace(user.id); + + if (!workspace) { + return new Response( + JSON.stringify({ + error: "Not Found", + message: "No workspace found for user", + }), + { + status: 404, + headers: { "Content-Type": "application/json" }, + }, + ); + } + + return workspace; +}