- Add Clerk token verification to tRPC context (server/trpc/index.ts) - Remove client-controlled authorId/reviewedById from revisions router - Require JWT_SECRET environment variable, remove hardcoded fallback - Add table name validation to prevent SQL injection in backup logic - Fix TRPCContext type to use better-sqlite3 instead of LibSQL - Update revisions router tests to use proper tRPC v11+ API - Add resetInMemoryState function for test isolation Security fixes address: - Critical: Authentication bypass via missing token verification - Critical: User impersonation via client-controlled IDs - High: Insecure WebSocket defaults with hardcoded secrets - High: SQL injection vulnerability in backup logic All tests passing (24/24).
71 lines
2.2 KiB
TypeScript
71 lines
2.2 KiB
TypeScript
import { createHTTPServer } from '@trpc/server/adapters/standalone';
|
|
import { projectRouter } from './project-router';
|
|
import { revisionsRouter } from './revisions-router';
|
|
import type { TRPCContext } from './types';
|
|
import type { TRPCError } from '@trpc/server';
|
|
import { t } from './router';
|
|
import { drizzle } from 'drizzle-orm/better-sqlite3';
|
|
import Database from 'better-sqlite3';
|
|
import { projects, characters, scenes, characterRelationships, sceneCharacters } from '../../src/db/schema';
|
|
import { verifyToken } from '@clerk/backend';
|
|
|
|
// App router combining all routers
|
|
export const appRouter = t.router({
|
|
project: projectRouter,
|
|
revisions: revisionsRouter,
|
|
} as const);
|
|
|
|
export type AppRouter = typeof appRouter;
|
|
|
|
// Database instance (shared for now - should come from config)
|
|
let dbInstance: ReturnType<typeof drizzle> | null = null;
|
|
|
|
function getDb() {
|
|
if (dbInstance) return dbInstance;
|
|
const sqlite = new Database('./data/frenocorp.db');
|
|
dbInstance = drizzle(sqlite);
|
|
return dbInstance;
|
|
}
|
|
|
|
// Create tRPC HTTP server
|
|
export function createTRPCServer(port: number = 8080) {
|
|
const server = createHTTPServer({
|
|
router: appRouter,
|
|
createContext: async ({ req }): Promise<TRPCContext> => {
|
|
const authHeader = req.headers.authorization;
|
|
let userId: number | undefined = undefined;
|
|
|
|
if (authHeader && authHeader.startsWith('Bearer ')) {
|
|
const token = authHeader.substring(7);
|
|
try {
|
|
const clerkSecretKey = process.env.CLERK_SECRET_KEY;
|
|
if (!clerkSecretKey) {
|
|
console.warn('CLERK_SECRET_KEY not set, skipping token verification');
|
|
} else {
|
|
const payload = await verifyToken(token, { secretKey: clerkSecretKey });
|
|
userId = payload.sub ? parseInt(payload.sub, 10) : undefined;
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to verify Clerk token:', error);
|
|
}
|
|
}
|
|
|
|
return {
|
|
userId,
|
|
db: getDb(),
|
|
};
|
|
},
|
|
onError: ({ error, path }: { error: TRPCError; path: string | undefined }) => {
|
|
console.error(`tRPC error on ${path}:`, error.message);
|
|
},
|
|
});
|
|
|
|
server.listen(port, () => {
|
|
console.log(`tRPC server listening on port ${port}`);
|
|
});
|
|
|
|
return server;
|
|
}
|
|
|
|
export default appRouter;
|