FRE-588: Complete tRPC/Clerk integration with database schema updates

- Updated router.ts middleware for Clerk authentication
- Modified test contexts to use clerkUserId
- Added team tables to test schema
- Updated WaitlistForm and waitlist page
- Created src/server/trpc/ parallel structure

All 258 tests pass. Ready for Security Reviewer.
This commit is contained in:
2026-04-28 16:13:55 -04:00
parent 81d944b2ac
commit 0cdb2e96b1
15 changed files with 427 additions and 71 deletions

View File

@@ -11,7 +11,7 @@ describe('tRPC API Layer - Character System', () => {
beforeEach(async () => {
await resetTestDb();
const db = await getTestDb();
ctx = { userId: 1, db };
ctx = { clerkUserId: 'user_test', db };
caller = appRouter.createCaller(ctx);
const project = await caller.project.createProject({

View File

@@ -25,7 +25,7 @@ describe('tRPC API Layer', () => {
expect(project).toMatchObject({
name: 'Test Project',
description: 'A test project',
ownerId: ctx.userId,
ownerId: 1,
});
expect(project.id).toBeDefined();
expect(project.id).toBeGreaterThan(0);
@@ -173,7 +173,7 @@ describe('tRPC API Layer', () => {
sharedProjectId = project.id;
// Insert a second user
globalSqlite!.exec("INSERT INTO users (id, email, name) VALUES (2, 'user2@test.com', 'User Two');");
globalSqlite!.exec("INSERT INTO users (id, clerk_id, email, name) VALUES (2, 'user2_test', 'user2@test.com', 'User Two');");
});
it('should share a project with another user', async () => {
@@ -276,7 +276,7 @@ describe('tRPC API Layer', () => {
// Create caller for user 2
const db = await getTestDb();
const ctx2: TRPCContext = { userId: 2, db };
const ctx2: TRPCContext = { clerkUserId: 'user2_test', db };
const caller2 = appRouter.createCaller(ctx2);
const project = await caller2.project.getProject({ id: sharedProjectId });
@@ -291,7 +291,7 @@ describe('tRPC API Layer', () => {
});
const db = await getTestDb();
const ctx2: TRPCContext = { userId: 2, db };
const ctx2: TRPCContext = { clerkUserId: 'user2_test', db };
const caller2 = appRouter.createCaller(ctx2);
const projects = await caller2.project.listProjects();

View File

@@ -12,7 +12,7 @@ describe('revisionsRouter', () => {
await resetTestDb();
const db = await getTestDb();
await resetInMemoryState(db);
ctx = { userId: 1, db };
ctx = { clerkUserId: 'user_test', db };
caller = appRouter.createCaller(ctx);
});

View File

@@ -15,12 +15,24 @@ const isAuthenticated = t.middleware(({ ctx, next }) => {
return next({ ctx: { ...ctx, clerkUserId: ctx.clerkUserId } });
});
// Middleware for database access
const hasDb = t.middleware(({ ctx, next }) => {
// Middleware for database access and user lookup
const hasDb = t.middleware(async ({ ctx, next }) => {
if (!ctx.db) {
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Database not available' });
}
return next({ ctx: { ...ctx, db: ctx.db } });
let userId: number | undefined;
if (ctx.clerkUserId) {
const { users } = await import('../../src/db/schema');
const userRows = await ctx.db.select({ id: users.id })
.from(users)
.where(eq(users.clerkId, ctx.clerkUserId));
if (userRows.length > 0) {
userId = userRows[0].id;
}
}
return next({ ctx: { ...ctx, db: ctx.db, userId } });
});
// Middleware for project ownership verification
@@ -35,7 +47,7 @@ const hasProjectAccess = t.middleware(async ({ ctx, next }) => {
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Database not available' });
}
const { users } = await import('../../src/db/schema');
const userRows = await ctx.db.select({ dbId: users.id, clerkId: users.clerkId })
const userRows = await ctx.db.select({ id: users.id, clerkId: users.clerkId })
.from(users)
.where(eq(users.clerkId, ctx.clerkUserId));
const dbUser = userRows[0];
@@ -49,10 +61,10 @@ const hasProjectAccess = t.middleware(async ({ ctx, next }) => {
if (!project) {
throw new TRPCError({ code: 'NOT_FOUND', message: `Project ${ctx.projectId} not found` });
}
if (project.ownerId !== dbUser.dbId) {
if (project.ownerId !== dbUser.id) {
throw new TRPCError({ code: 'FORBIDDEN', message: `You do not have access to project ${ctx.projectId}` });
}
return next({ ctx: { ...ctx, projectId: ctx.projectId, userId: dbUser.dbId } });
return next({ ctx: { ...ctx, projectId: ctx.projectId, userId: dbUser.id } });
});
// Base router

View File

@@ -111,18 +111,34 @@ CREATE TABLE IF NOT EXISTS revisions (
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS revision_changes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
revision_id INTEGER NOT NULL REFERENCES revisions(id),
change_type TEXT NOT NULL CHECK(change_type IN ('addition', 'deletion', 'modification')),
element_type TEXT,
old_content TEXT,
new_content TEXT,
scene_number INTEGER,
line_number INTEGER,
page_number INTEGER,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS revision_changes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
revision_id INTEGER NOT NULL REFERENCES revisions(id),
change_type TEXT NOT NULL CHECK(change_type IN ('addition', 'deletion', 'modification')),
element_type TEXT,
old_content TEXT,
new_content TEXT,
scene_number INTEGER,
line_number INTEGER,
page_number INTEGER,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS teams (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
owner_id INTEGER NOT NULL REFERENCES users(id),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS team_members (
id INTEGER PRIMARY KEY AUTOINCREMENT,
team_id TEXT NOT NULL REFERENCES teams(id),
user_id INTEGER NOT NULL REFERENCES users(id),
role TEXT NOT NULL DEFAULT 'editor',
joined_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
`;
export let globalSqlite: Database.Database | null = null;

View File

@@ -162,5 +162,5 @@ export interface TRPCContext {
userId?: number;
clerkUserId?: string;
projectId?: number;
db?: typeof import('../../src/db/config/migrations').db;
db?: import('../../src/db/config/migrations').DrizzleDB;
}