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 = {
|
const invite = {
|
||||||
token: 'valid-token',
|
token: 'valid-token',
|
||||||
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
|
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
|
||||||
status: 'accepted' as const,
|
status: 'accepted' as string,
|
||||||
};
|
};
|
||||||
|
|
||||||
const canAccept = invite.status === 'pending';
|
const canAccept = invite.status === 'pending';
|
||||||
@@ -76,13 +76,13 @@ describe('Invite System', () => {
|
|||||||
|
|
||||||
describe('Admin Access', () => {
|
describe('Admin Access', () => {
|
||||||
test('should allow admin to create invites', () => {
|
test('should allow admin to create invites', () => {
|
||||||
const user = { role: 'admin' as const };
|
const user: { role: string } = { role: 'admin' };
|
||||||
const canInvite = user.role === 'admin';
|
const canInvite = user.role === 'admin';
|
||||||
expect(canInvite).toBe(true);
|
expect(canInvite).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should deny regular users from creating invites', () => {
|
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';
|
const canInvite = user.role === 'admin';
|
||||||
expect(canInvite).toBe(false);
|
expect(canInvite).toBe(false);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ import { taskRoutes } from './routes/tasks';
|
|||||||
import { labelRoutes } from './routes/labels';
|
import { labelRoutes } from './routes/labels';
|
||||||
import { commentRoutes } from './routes/comments';
|
import { commentRoutes } from './routes/comments';
|
||||||
import { hammerRoutes } from './routes/hammer';
|
import { hammerRoutes } from './routes/hammer';
|
||||||
import type { User } from './lib/auth';
|
|
||||||
|
|
||||||
const app = new Elysia()
|
const app = new Elysia()
|
||||||
// CORS
|
// CORS
|
||||||
.use(cors({
|
.use(cors({
|
||||||
@@ -39,21 +37,7 @@ const app = new Elysia()
|
|||||||
// Hammer API (uses separate API key auth)
|
// Hammer API (uses separate API key auth)
|
||||||
.group('/api', app => app.use(hammerRoutes))
|
.group('/api', app => app.use(hammerRoutes))
|
||||||
|
|
||||||
// Protected routes - require auth
|
// Authenticated API routes (auth middleware is in each route plugin)
|
||||||
.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
|
|
||||||
.group('/api', app => app
|
.group('/api', app => app
|
||||||
.use(adminRoutes)
|
.use(adminRoutes)
|
||||||
.use(projectRoutes)
|
.use(projectRoutes)
|
||||||
@@ -64,30 +48,33 @@ const app = new Elysia()
|
|||||||
|
|
||||||
// Error handler
|
// Error handler
|
||||||
.onError(({ code, error, set, path }) => {
|
.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}:`, {
|
console.error(`[${new Date().toISOString()}] ERROR on ${path}:`, {
|
||||||
code,
|
code,
|
||||||
message: error.message,
|
message,
|
||||||
stack: process.env.NODE_ENV !== 'production' ? error.stack : undefined,
|
stack: process.env.NODE_ENV !== 'production' ? stack : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (code === 'VALIDATION') {
|
if (code === 'VALIDATION') {
|
||||||
set.status = 400;
|
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;
|
set.status = 401;
|
||||||
return { error: 'Unauthorized' };
|
return { error: 'Unauthorized' };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error.message === 'Admin access required') {
|
if (message === 'Admin access required') {
|
||||||
set.status = 403;
|
set.status = 403;
|
||||||
return { error: 'Forbidden: Admin access required' };
|
return { error: 'Forbidden: Admin access required' };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error.message.includes('not found')) {
|
if (message.includes('not found')) {
|
||||||
set.status = 404;
|
set.status = 404;
|
||||||
return { error: error.message };
|
return { error: message };
|
||||||
}
|
}
|
||||||
|
|
||||||
set.status = 500;
|
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 { auth } from '../lib/auth';
|
||||||
import type { User } from '../lib/auth';
|
import type { User } from '../lib/auth';
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
|
import { authMiddleware } from '../middleware/auth';
|
||||||
|
|
||||||
export const adminRoutes = new Elysia({ prefix: '/admin' })
|
export const adminRoutes = new Elysia({ prefix: '/admin' })
|
||||||
|
.use(authMiddleware)
|
||||||
// Middleware: require admin role
|
// Middleware: require admin role
|
||||||
.derive(({ user, set }) => {
|
.derive(({ user, set }) => {
|
||||||
if ((user as User).role !== 'admin') {
|
if ((user as User).role !== 'admin') {
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ export const authRoutes = new Elysia({ prefix: '/auth' })
|
|||||||
if (newUser) {
|
if (newUser) {
|
||||||
// Set role from invite if specified
|
// Set role from invite if specified
|
||||||
if (invite.role && invite.role !== 'user') {
|
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
|
// Create default inbox project
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ import { db } from '../db';
|
|||||||
import { comments, tasks } from '../db/schema';
|
import { comments, tasks } from '../db/schema';
|
||||||
import { eq, and, asc } from 'drizzle-orm';
|
import { eq, and, asc } from 'drizzle-orm';
|
||||||
import type { User } from '../lib/auth';
|
import type { User } from '../lib/auth';
|
||||||
|
import { authMiddleware } from '../middleware/auth';
|
||||||
|
|
||||||
export const commentRoutes = new Elysia({ prefix: '/comments' })
|
export const commentRoutes = new Elysia({ prefix: '/comments' })
|
||||||
|
.use(authMiddleware)
|
||||||
// Get comments for a task
|
// Get comments for a task
|
||||||
.get('/task/:taskId', async ({ params, user, set }) => {
|
.get('/task/:taskId', async ({ params, user, set }) => {
|
||||||
// Verify task ownership
|
// Verify task ownership
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { auth } from '../lib/auth';
|
|||||||
// This route uses bearer token auth for Hammer (service account)
|
// This route uses bearer token auth for Hammer (service account)
|
||||||
// The token is set in HAMMER_API_KEY env var
|
// 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;
|
if (!authHeader) return false;
|
||||||
const token = authHeader.replace('Bearer ', '');
|
const token = authHeader.replace('Bearer ', '');
|
||||||
return token === process.env.HAMMER_API_KEY;
|
return token === process.env.HAMMER_API_KEY;
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ import { db } from '../db';
|
|||||||
import { labels, taskLabels } from '../db/schema';
|
import { labels, taskLabels } from '../db/schema';
|
||||||
import { eq, asc, sql } from 'drizzle-orm';
|
import { eq, asc, sql } from 'drizzle-orm';
|
||||||
import type { User } from '../lib/auth';
|
import type { User } from '../lib/auth';
|
||||||
|
import { authMiddleware } from '../middleware/auth';
|
||||||
|
|
||||||
export const labelRoutes = new Elysia({ prefix: '/labels' })
|
export const labelRoutes = new Elysia({ prefix: '/labels' })
|
||||||
|
.use(authMiddleware)
|
||||||
// List all labels for user
|
// List all labels for user
|
||||||
.get('/', async ({ user }) => {
|
.get('/', async ({ user }) => {
|
||||||
const userLabels = await db.query.labels.findMany({
|
const userLabels = await db.query.labels.findMany({
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ import { db } from '../db';
|
|||||||
import { projects, sections } from '../db/schema';
|
import { projects, sections } from '../db/schema';
|
||||||
import { eq, and, asc, desc } from 'drizzle-orm';
|
import { eq, and, asc, desc } from 'drizzle-orm';
|
||||||
import type { User } from '../lib/auth';
|
import type { User } from '../lib/auth';
|
||||||
|
import { authMiddleware } from '../middleware/auth';
|
||||||
|
|
||||||
export const projectRoutes = new Elysia({ prefix: '/projects' })
|
export const projectRoutes = new Elysia({ prefix: '/projects' })
|
||||||
|
.use(authMiddleware)
|
||||||
// List all projects for user
|
// List all projects for user
|
||||||
.get('/', async ({ user }) => {
|
.get('/', async ({ user }) => {
|
||||||
const userProjects = await db.query.projects.findMany({
|
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 { tasks, taskLabels, projects, activityLog, hammerWebhooks } from '../db/schema';
|
||||||
import { eq, and, or, asc, desc, isNull, gte, lte, sql } from 'drizzle-orm';
|
import { eq, and, or, asc, desc, isNull, gte, lte, sql } from 'drizzle-orm';
|
||||||
import type { User } from '../lib/auth';
|
import type { User } from '../lib/auth';
|
||||||
|
import { authMiddleware } from '../middleware/auth';
|
||||||
|
|
||||||
// Helper to trigger Hammer webhooks
|
// Helper to trigger Hammer webhooks
|
||||||
async function triggerHammerWebhooks(event: string, payload: Record<string, unknown>) {
|
async function triggerHammerWebhooks(event: string, payload: Record<string, unknown>) {
|
||||||
@@ -46,6 +47,7 @@ async function logActivity(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const taskRoutes = new Elysia({ prefix: '/tasks' })
|
export const taskRoutes = new Elysia({ prefix: '/tasks' })
|
||||||
|
.use(authMiddleware)
|
||||||
// List tasks with filters
|
// List tasks with filters
|
||||||
.get('/', async ({ user, query }) => {
|
.get('/', async ({ user, query }) => {
|
||||||
const userId = (user as User).id;
|
const userId = (user as User).id;
|
||||||
|
|||||||
Reference in New Issue
Block a user