From c965fdd06fcb59420bc1876d7c4c4541ff1b1e4a Mon Sep 17 00:00:00 2001 From: Hammer Date: Fri, 30 Jan 2026 02:57:07 +0000 Subject: [PATCH] fix: resolve TypeScript errors for CI - add auth middleware plugin - Create shared authMiddleware plugin with scoped derive for proper type propagation - Each route file now uses authMiddleware instead of relying on parent derive - Fix error handler to use instanceof Error checks for message/stack access - Fix null vs undefined type mismatch in hammer route auth validation - Fix invite role type assertion for enum compatibility - Fix test type assertions to avoid impossible comparisons --- apps/api/src/__tests__/auth.test.ts | 6 ++--- apps/api/src/index.ts | 35 +++++++++-------------------- apps/api/src/middleware/auth.ts | 20 +++++++++++++++++ apps/api/src/routes/admin.ts | 2 ++ apps/api/src/routes/auth.ts | 2 +- apps/api/src/routes/comments.ts | 2 ++ apps/api/src/routes/hammer.ts | 2 +- apps/api/src/routes/labels.ts | 2 ++ apps/api/src/routes/projects.ts | 2 ++ apps/api/src/routes/tasks.ts | 2 ++ 10 files changed, 46 insertions(+), 29 deletions(-) create mode 100644 apps/api/src/middleware/auth.ts diff --git a/apps/api/src/__tests__/auth.test.ts b/apps/api/src/__tests__/auth.test.ts index c430413..fe0a313 100644 --- a/apps/api/src/__tests__/auth.test.ts +++ b/apps/api/src/__tests__/auth.test.ts @@ -35,7 +35,7 @@ describe('Invite System', () => { const invite = { token: 'valid-token', expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), - status: 'accepted' as const, + status: 'accepted' as string, }; const canAccept = invite.status === 'pending'; @@ -76,13 +76,13 @@ describe('Invite System', () => { describe('Admin Access', () => { test('should allow admin to create invites', () => { - const user = { role: 'admin' as const }; + const user: { role: string } = { role: 'admin' }; const canInvite = user.role === 'admin'; expect(canInvite).toBe(true); }); test('should deny regular users from creating invites', () => { - const user = { role: 'user' as const }; + const user: { role: string } = { role: 'user' }; const canInvite = user.role === 'admin'; expect(canInvite).toBe(false); }); diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index d48efa4..51ac531 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -8,8 +8,6 @@ import { taskRoutes } from './routes/tasks'; import { labelRoutes } from './routes/labels'; import { commentRoutes } from './routes/comments'; import { hammerRoutes } from './routes/hammer'; -import type { User } from './lib/auth'; - const app = new Elysia() // CORS .use(cors({ @@ -39,21 +37,7 @@ const app = new Elysia() // Hammer API (uses separate API key auth) .group('/api', app => app.use(hammerRoutes)) - // Protected routes - require auth - .derive(async ({ request, set }): Promise<{ user: User }> => { - const session = await auth.api.getSession({ - headers: request.headers, - }); - - if (!session?.user) { - set.status = 401; - throw new Error('Unauthorized'); - } - - return { user: session.user as User }; - }) - - // Authenticated API routes + // Authenticated API routes (auth middleware is in each route plugin) .group('/api', app => app .use(adminRoutes) .use(projectRoutes) @@ -64,30 +48,33 @@ const app = new Elysia() // Error handler .onError(({ code, error, set, path }) => { + const message = error instanceof Error ? error.message : String(error); + const stack = error instanceof Error ? error.stack : undefined; + console.error(`[${new Date().toISOString()}] ERROR on ${path}:`, { code, - message: error.message, - stack: process.env.NODE_ENV !== 'production' ? error.stack : undefined, + message, + stack: process.env.NODE_ENV !== 'production' ? stack : undefined, }); if (code === 'VALIDATION') { set.status = 400; - return { error: 'Validation error', details: error.message }; + return { error: 'Validation error', details: message }; } - if (error.message === 'Unauthorized') { + if (message === 'Unauthorized') { set.status = 401; return { error: 'Unauthorized' }; } - if (error.message === 'Admin access required') { + if (message === 'Admin access required') { set.status = 403; return { error: 'Forbidden: Admin access required' }; } - if (error.message.includes('not found')) { + if (message.includes('not found')) { set.status = 404; - return { error: error.message }; + return { error: message }; } set.status = 500; diff --git a/apps/api/src/middleware/auth.ts b/apps/api/src/middleware/auth.ts new file mode 100644 index 0000000..760cdc3 --- /dev/null +++ b/apps/api/src/middleware/auth.ts @@ -0,0 +1,20 @@ +import { Elysia } from 'elysia'; +import { auth, type User } from '../lib/auth'; + +/** + * Auth middleware plugin - adds `user` to the Elysia context. + * Import and `.use(authMiddleware)` in route files that need authentication. + */ +export const authMiddleware = new Elysia({ name: 'auth-middleware' }) + .derive({ as: 'scoped' }, async ({ request, set }): Promise<{ user: User }> => { + const session = await auth.api.getSession({ + headers: request.headers, + }); + + if (!session?.user) { + set.status = 401; + throw new Error('Unauthorized'); + } + + return { user: session.user as User }; + }); diff --git a/apps/api/src/routes/admin.ts b/apps/api/src/routes/admin.ts index b786ebc..1def7a1 100644 --- a/apps/api/src/routes/admin.ts +++ b/apps/api/src/routes/admin.ts @@ -6,8 +6,10 @@ import { sendInviteEmail } from '../lib/email'; import { auth } from '../lib/auth'; import type { User } from '../lib/auth'; import crypto from 'crypto'; +import { authMiddleware } from '../middleware/auth'; export const adminRoutes = new Elysia({ prefix: '/admin' }) + .use(authMiddleware) // Middleware: require admin role .derive(({ user, set }) => { if ((user as User).role !== 'admin') { diff --git a/apps/api/src/routes/auth.ts b/apps/api/src/routes/auth.ts index 17b12b0..c2648b3 100644 --- a/apps/api/src/routes/auth.ts +++ b/apps/api/src/routes/auth.ts @@ -78,7 +78,7 @@ export const authRoutes = new Elysia({ prefix: '/auth' }) if (newUser) { // Set role from invite if specified if (invite.role && invite.role !== 'user') { - await db.update(users).set({ role: invite.role }).where(eq(users.id, newUser.id)); + await db.update(users).set({ role: invite.role as 'admin' | 'user' | 'service' }).where(eq(users.id, newUser.id)); } // Create default inbox project diff --git a/apps/api/src/routes/comments.ts b/apps/api/src/routes/comments.ts index 67d4050..145b511 100644 --- a/apps/api/src/routes/comments.ts +++ b/apps/api/src/routes/comments.ts @@ -3,8 +3,10 @@ import { db } from '../db'; import { comments, tasks } from '../db/schema'; import { eq, and, asc } from 'drizzle-orm'; import type { User } from '../lib/auth'; +import { authMiddleware } from '../middleware/auth'; export const commentRoutes = new Elysia({ prefix: '/comments' }) + .use(authMiddleware) // Get comments for a task .get('/task/:taskId', async ({ params, user, set }) => { // Verify task ownership diff --git a/apps/api/src/routes/hammer.ts b/apps/api/src/routes/hammer.ts index b4fd2e3..2d75855 100644 --- a/apps/api/src/routes/hammer.ts +++ b/apps/api/src/routes/hammer.ts @@ -8,7 +8,7 @@ import { auth } from '../lib/auth'; // This route uses bearer token auth for Hammer (service account) // The token is set in HAMMER_API_KEY env var -const validateHammerAuth = (authHeader: string | undefined): boolean => { +const validateHammerAuth = (authHeader: string | null | undefined): boolean => { if (!authHeader) return false; const token = authHeader.replace('Bearer ', ''); return token === process.env.HAMMER_API_KEY; diff --git a/apps/api/src/routes/labels.ts b/apps/api/src/routes/labels.ts index fefe9f2..aa42e3a 100644 --- a/apps/api/src/routes/labels.ts +++ b/apps/api/src/routes/labels.ts @@ -3,8 +3,10 @@ import { db } from '../db'; import { labels, taskLabels } from '../db/schema'; import { eq, asc, sql } from 'drizzle-orm'; import type { User } from '../lib/auth'; +import { authMiddleware } from '../middleware/auth'; export const labelRoutes = new Elysia({ prefix: '/labels' }) + .use(authMiddleware) // List all labels for user .get('/', async ({ user }) => { const userLabels = await db.query.labels.findMany({ diff --git a/apps/api/src/routes/projects.ts b/apps/api/src/routes/projects.ts index 370637d..eea2d40 100644 --- a/apps/api/src/routes/projects.ts +++ b/apps/api/src/routes/projects.ts @@ -3,8 +3,10 @@ import { db } from '../db'; import { projects, sections } from '../db/schema'; import { eq, and, asc, desc } from 'drizzle-orm'; import type { User } from '../lib/auth'; +import { authMiddleware } from '../middleware/auth'; export const projectRoutes = new Elysia({ prefix: '/projects' }) + .use(authMiddleware) // List all projects for user .get('/', async ({ user }) => { const userProjects = await db.query.projects.findMany({ diff --git a/apps/api/src/routes/tasks.ts b/apps/api/src/routes/tasks.ts index 26ec1c7..dd5e2a4 100644 --- a/apps/api/src/routes/tasks.ts +++ b/apps/api/src/routes/tasks.ts @@ -3,6 +3,7 @@ import { db } from '../db'; import { tasks, taskLabels, projects, activityLog, hammerWebhooks } from '../db/schema'; import { eq, and, or, asc, desc, isNull, gte, lte, sql } from 'drizzle-orm'; import type { User } from '../lib/auth'; +import { authMiddleware } from '../middleware/auth'; // Helper to trigger Hammer webhooks async function triggerHammerWebhooks(event: string, payload: Record) { @@ -46,6 +47,7 @@ async function logActivity(params: { } export const taskRoutes = new Elysia({ prefix: '/tasks' }) + .use(authMiddleware) // List tasks with filters .get('/', async ({ user, query }) => { const userId = (user as User).id;