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
This commit is contained in:
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
20
apps/api/src/middleware/auth.ts
Normal file
20
apps/api/src/middleware/auth.ts
Normal file
@@ -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 };
|
||||
});
|
||||
@@ -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') {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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<string, unknown>) {
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user