nessa: retire the API copy now served by nessa-api.freno.me

The Nessa API (nessaDb.*, nessa.community.*, Clerk auth and the Clerk user
webhook) lives in the Nessa repo's server/ on pan. This copy never served
the app: RemoteAPIClient wraps every input in {"input": …}, which the stock
tRPC adapter here reads as the input itself (queries 404, writes no-op).
Clerk's webhook already targets nessa-api.freno.me.

Drops the routers, nessa-auth, the webhook, NessaConnectionFactory, the
NESSA_* env and the now-unused @clerk/backend and svix. Nessa's marketing,
privacy and deletion pages stay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-13 13:54:14 -04:00
parent 9ff095a82c
commit 6898186df5
21 changed files with 12 additions and 5978 deletions

View File

@@ -2,15 +2,12 @@ import { initTRPC, TRPCError } from "@trpc/server";
import type { APIEvent } from "@solidjs/start/server";
import { logVisit, enrichAnalyticsEntry } from "~/server/analytics";
import { getRequestIP } from "vinxi/http";
import { verifyNessaToken } from "~/server/nessa-auth";
import { getAuthPayloadFromEvent } from "~/server/auth";
import { NessaConnectionFactory } from "~/server/database";
export type Context = {
event: APIEvent;
userId: string | null;
isAdmin: boolean;
nessaUserId: string | null;
};
/** Safely get a header value from either Fetch API Headers or Node.js IncomingHttpHeaders */
@@ -55,44 +52,6 @@ async function createContextInner(event: APIEvent): Promise<Context> {
getHeader(req.headers, "referrer") ||
getHeader(event.request?.headers, "referer");
const ipAddress = getRequestIP(event.nativeEvent) || undefined;
const authHeader =
getHeader(req.headers, "authorization") ||
getHeader(event.request?.headers, "authorization") ||
null;
let nessaUserId: string | null = null;
if (authHeader && authHeader.startsWith("Bearer ")) {
const token = authHeader.replace("Bearer ", "").trim();
try {
// Verify the Clerk session JWT — `sub` is the Clerk user id.
const clerkPayload = await verifyNessaToken(token);
// Resolve the Clerk user id to the local users.id via the indexed
// clerkUserId column. One indexed query per request is acceptable;
// no premature caching (the row is created by the Clerk webhook).
const conn = NessaConnectionFactory();
const result = await conn.execute({
sql: "SELECT id FROM users WHERE clerkUserId = ?",
args: [clerkPayload.sub]
});
if (result.rows.length === 0) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Nessa user not found — Clerk account not linked"
});
}
// `nessaUserId` is the LOCAL users.id — router bodies reference it
// exactly as before (club ownership, membership, row scoping).
nessaUserId = (result.rows[0] as { id: string }).id;
} catch (error) {
// Re-throw typed TRPCError (lookup miss) so the caller gets UNAUTHORIZED;
// swallow Clerk verification failures (expired/invalid token) the same
// way the legacy path did — the enforceNessaUser middleware rejects
// null nessaUserId with UNAUTHORIZED.
if (error instanceof TRPCError) throw error;
console.error("Nessa JWT verification failed:", error);
}
}
// Don't log the performance logging endpoint itself to avoid circular tracking
if (!path.includes("analytics.logPerformance")) {
@@ -111,8 +70,7 @@ async function createContextInner(event: APIEvent): Promise<Context> {
return {
event,
userId,
isAdmin,
nessaUserId
isAdmin
};
}
@@ -152,24 +110,8 @@ const enforceUserIsAdmin = t.middleware(({ ctx, next }) => {
});
});
const enforceNessaUser = t.middleware(({ ctx, next }) => {
if (!ctx.nessaUserId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Nessa authentication required"
});
}
return next({
ctx: {
...ctx,
nessaUserId: ctx.nessaUserId
}
});
});
export const protectedProcedure = t.procedure.use(enforceUserIsAuthed);
export const adminProcedure = t.procedure.use(enforceUserIsAdmin);
export const nessaProcedure = t.procedure.use(enforceNessaUser);
// CSRF protection middleware - defined here to avoid circular dependency
const csrfProtection = t.middleware(async ({ ctx, next }) => {