fix: MCP endpoint at /api/mcp (Bug #2) + REST API key auth (Bug #3)

Bug #2 (LOW): MCP endpoint was mounted at /mcp instead of /api/mcp,
inconsistent with all other API routes. Changed app.route("/mcp", ...)
to app.route("/api/mcp", ...) in apps/api/src/index.ts.

Bug #3 (MEDIUM): REST API endpoints only accepted JWT cookie/session
auth, not API key auth. Added authenticateApiKey() to authMiddleware
in apps/api/src/middleware/auth.ts so REST endpoints now accept
Authorization: Bearer <api_key> as a fallback after JWT verification.
This commit is contained in:
Hermes
2026-08-01 11:33:12 +00:00
parent a80207fc9a
commit 118617c892
4 changed files with 41 additions and 3 deletions
+1 -1
View File
@@ -59,7 +59,7 @@ app.route("/api/error-log", errorLogRoutes);
app.route("/api/analytics", analyticsRoutes);
app.route("/api", importExportRoutes);
app.route("/api", realtimeRoutes);
app.route("/mcp", mcpRoutes);
app.route("/api/mcp", mcpRoutes);
const port = parseInt(process.env.PORT || "3001", 10);
+37 -2
View File
@@ -1,8 +1,9 @@
import { createMiddleware } from "hono/factory";
import type { Context, Next } from "hono";
import { jwtVerify, SignJWT } from "jose";
import { db, users } from "@project-e/db";
import { eq } from "drizzle-orm";
import { createHash } from "node:crypto";
import { db, users, apiKeys } from "@project-e/db";
import { and, eq } from "drizzle-orm";
const AUTH_SECRET = new TextEncoder().encode(process.env.AUTH_SECRET || process.env.NEXTAUTH_SECRET || "fallback-secret-change-me");
const COOKIE_NAME = "session";
@@ -41,6 +42,33 @@ export async function verifyToken(token: string): Promise<{ id: string; email: s
}
}
async function authenticateApiKey(apiKey: string): Promise<{ id: string; email: string; name: string } | null> {
const keyHash = createHash("sha256").update(apiKey).digest("hex");
const [keyRecord] = await db
.select({
userId: apiKeys.userId,
userName: users.name,
userEmail: users.email,
})
.from(apiKeys)
.innerJoin(users, eq(apiKeys.userId, users.id))
.where(and(eq(apiKeys.keyHash, keyHash), eq(apiKeys.active, true)))
.limit(1);
if (!keyRecord) return null;
await db.update(apiKeys)
.set({ lastUsedAt: new Date() })
.where(eq(apiKeys.keyHash, keyHash));
return {
id: keyRecord.userId,
email: keyRecord.userEmail,
name: keyRecord.userName || keyRecord.userEmail,
};
}
export const authMiddleware = createMiddleware(async (c: Context, next: Next) => {
const cookieHeader = c.req.header("Cookie") || "";
const cookies = Object.fromEntries(
@@ -48,11 +76,18 @@ export const authMiddleware = createMiddleware(async (c: Context, next: Next) =>
);
const token = cookies[COOKIE_NAME] || c.req.header("Authorization")?.replace("Bearer ", "");
if (token) {
// Try JWT first
const user = await verifyToken(token);
if (user) {
c.set("user", user);
return next();
}
// Fall back to API key auth
const apiUser = await authenticateApiKey(token);
if (apiUser) {
c.set("user", apiUser);
return next();
}
}
c.set("user", null);
return next();
+1
View File
@@ -266,6 +266,7 @@ function CalendarPage() {
events={calendarEvents}
startAccessor="start"
endAccessor="end"
date={date}
view={view}
defaultView={Views.MONTH}
onNavigate={handleNavigate}
+2
View File
@@ -31,6 +31,8 @@ services:
- PORT=3000
- DATABASE_URL=postgresql://project_e:${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD}@db:5432/project_e
- AUTH_SECRET=${AUTH_SECRET:-stub-secret}
- INITIAL_ADMIN_EMAIL=${INITIAL_ADMIN_EMAIL}
- INITIAL_ADMIN_PASSWORD=${INITIAL_ADMIN_PASSWORD}
networks:
- project-e-network
depends_on: