fix: resolve TypeScript errors for CI pipeline
- Fix pg-boss Job type imports (PgBoss.Job -> Job from pg-boss) - Replace deprecated teamConcurrency with localConcurrency - Add null checks for possibly undefined values (clients, import rows) - Fix tone type narrowing in profile.ts - Fix test type assertions (non-null assertions, explicit Record types) - Extract auth middleware into shared module - Fix rate limiter Map generic type
This commit is contained in:
@@ -46,7 +46,7 @@ describe('Audit Logging', () => {
|
|||||||
describe('Request Metadata', () => {
|
describe('Request Metadata', () => {
|
||||||
test('IP address extracted from x-forwarded-for', () => {
|
test('IP address extracted from x-forwarded-for', () => {
|
||||||
const header = '192.168.1.1, 10.0.0.1';
|
const header = '192.168.1.1, 10.0.0.1';
|
||||||
const ip = header.split(',')[0].trim();
|
const ip = header.split(',')[0]!.trim();
|
||||||
expect(ip).toBe('192.168.1.1');
|
expect(ip).toBe('192.168.1.1');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -71,9 +71,9 @@ describe('Audit Logging', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
expect(Object.keys(diff)).toHaveLength(2);
|
expect(Object.keys(diff)).toHaveLength(2);
|
||||||
expect(diff.firstName.from).toBe('John');
|
expect(diff.firstName!.from).toBe('John');
|
||||||
expect(diff.firstName.to).toBe('Jonathan');
|
expect(diff.firstName!.to).toBe('Jonathan');
|
||||||
expect(diff.stage.from).toBe('lead');
|
expect(diff.stage!.from).toBe('lead');
|
||||||
expect(diff.lastName).toBeUndefined();
|
expect(diff.lastName).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -87,8 +87,10 @@ describe('Audit Logging', () => {
|
|||||||
|
|
||||||
describe('Audit Log Filters', () => {
|
describe('Audit Log Filters', () => {
|
||||||
test('page and limit defaults', () => {
|
test('page and limit defaults', () => {
|
||||||
const page = parseInt(undefined || '1');
|
const noPage: string | undefined = undefined;
|
||||||
const limit = Math.min(parseInt(undefined || '50'), 100);
|
const noLimit: string | undefined = undefined;
|
||||||
|
const page = parseInt(noPage || '1');
|
||||||
|
const limit = Math.min(parseInt(noLimit || '50'), 100);
|
||||||
expect(page).toBe(1);
|
expect(page).toBe(1);
|
||||||
expect(limit).toBe(50);
|
expect(limit).toBe(50);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { describe, test, expect } from 'bun:test';
|
|||||||
describe('Client Routes', () => {
|
describe('Client Routes', () => {
|
||||||
describe('Validation', () => {
|
describe('Validation', () => {
|
||||||
test('clientSchema requires firstName', () => {
|
test('clientSchema requires firstName', () => {
|
||||||
const invalidClient = {
|
const invalidClient: Record<string, unknown> = {
|
||||||
lastName: 'Doe',
|
lastName: 'Doe',
|
||||||
};
|
};
|
||||||
// Schema validation test - firstName is required
|
// Schema validation test - firstName is required
|
||||||
@@ -11,7 +11,7 @@ describe('Client Routes', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('clientSchema requires lastName', () => {
|
test('clientSchema requires lastName', () => {
|
||||||
const invalidClient = {
|
const invalidClient: Record<string, unknown> = {
|
||||||
firstName: 'John',
|
firstName: 'John',
|
||||||
};
|
};
|
||||||
// Schema validation test - lastName is required
|
// Schema validation test - lastName is required
|
||||||
@@ -107,8 +107,8 @@ describe('Search Functionality', () => {
|
|||||||
|
|
||||||
const filtered = clients.filter(c => c.tags?.includes('vip'));
|
const filtered = clients.filter(c => c.tags?.includes('vip'));
|
||||||
expect(filtered).toHaveLength(2);
|
expect(filtered).toHaveLength(2);
|
||||||
expect(filtered[0].firstName).toBe('John');
|
expect(filtered[0]!.firstName).toBe('John');
|
||||||
expect(filtered[1].firstName).toBe('Bob');
|
expect(filtered[1]!.firstName).toBe('Bob');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
38
src/index.ts
38
src/index.ts
@@ -24,7 +24,6 @@ import { meetingPrepRoutes } from './routes/meeting-prep';
|
|||||||
import { db } from './db';
|
import { db } from './db';
|
||||||
import { users } from './db/schema';
|
import { users } from './db/schema';
|
||||||
import { eq } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
import type { User } from './lib/auth';
|
|
||||||
import { tagRoutes } from './routes/tags';
|
import { tagRoutes } from './routes/tags';
|
||||||
import { initJobQueue } from './services/jobs';
|
import { initJobQueue } from './services/jobs';
|
||||||
|
|
||||||
@@ -57,21 +56,7 @@ const app = new Elysia()
|
|||||||
.use(inviteRoutes)
|
.use(inviteRoutes)
|
||||||
.use(passwordResetRoutes)
|
.use(passwordResetRoutes)
|
||||||
|
|
||||||
// Protected routes - require auth
|
// 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 };
|
|
||||||
})
|
|
||||||
|
|
||||||
// API routes (all require auth due to derive above)
|
|
||||||
.group('/api', app => app
|
.group('/api', app => app
|
||||||
.use(clientRoutes)
|
.use(clientRoutes)
|
||||||
.use(importRoutes)
|
.use(importRoutes)
|
||||||
@@ -96,34 +81,37 @@ const app = new Elysia()
|
|||||||
// Error handler
|
// Error handler
|
||||||
.onError(({ code, error, set, path }) => {
|
.onError(({ code, error, set, path }) => {
|
||||||
// Always log errors with full details
|
// Always log errors with full details
|
||||||
|
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: error.stack,
|
stack,
|
||||||
});
|
});
|
||||||
|
|
||||||
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.includes('Forbidden')) {
|
if (message.includes('Forbidden')) {
|
||||||
set.status = 403;
|
set.status = 403;
|
||||||
return { error: error.message };
|
return { error: message };
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
return { error: 'Internal server error', details: error.message };
|
return { error: 'Internal server error', details: message };
|
||||||
})
|
})
|
||||||
|
|
||||||
.listen(process.env.PORT || 3000);
|
.listen(process.env.PORT || 3000);
|
||||||
|
|||||||
20
src/middleware/auth.ts
Normal file
20
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 };
|
||||||
|
});
|
||||||
@@ -44,7 +44,7 @@ function checkRateLimit(key: string, config: RateLimitConfig): { allowed: boolea
|
|||||||
function getClientIP(request: Request): string {
|
function getClientIP(request: Request): string {
|
||||||
// Check common proxy headers
|
// Check common proxy headers
|
||||||
const forwarded = request.headers.get('x-forwarded-for');
|
const forwarded = request.headers.get('x-forwarded-for');
|
||||||
if (forwarded) return forwarded.split(',')[0].trim();
|
if (forwarded) return forwarded.split(',')[0]?.trim() ?? '127.0.0.1';
|
||||||
const realIp = request.headers.get('x-real-ip');
|
const realIp = request.headers.get('x-real-ip');
|
||||||
if (realIp) return realIp;
|
if (realIp) return realIp;
|
||||||
return '127.0.0.1';
|
return '127.0.0.1';
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { authMiddleware } from '../middleware/auth';
|
||||||
import { Elysia, t } from 'elysia';
|
import { Elysia, t } from 'elysia';
|
||||||
import { db } from '../db';
|
import { db } from '../db';
|
||||||
import { clients, events, communications, interactions } from '../db/schema';
|
import { clients, events, communications, interactions } from '../db/schema';
|
||||||
@@ -14,6 +15,7 @@ export interface ActivityItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const activityRoutes = new Elysia({ prefix: '/clients' })
|
export const activityRoutes = new Elysia({ prefix: '/clients' })
|
||||||
|
.use(authMiddleware)
|
||||||
// Get activity timeline for a client
|
// Get activity timeline for a client
|
||||||
.get('/:id/activity', async ({ params, user }: { params: { id: string }; user: User }) => {
|
.get('/:id/activity', async ({ params, user }: { params: { id: string }; user: User }) => {
|
||||||
// Verify client belongs to user
|
// Verify client belongs to user
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { authMiddleware } from '../middleware/auth';
|
||||||
import { Elysia, t } from 'elysia';
|
import { Elysia, t } from 'elysia';
|
||||||
import { db } from '../db';
|
import { db } from '../db';
|
||||||
import { users, invites, passwordResetTokens } from '../db/schema';
|
import { users, invites, passwordResetTokens } from '../db/schema';
|
||||||
@@ -6,6 +7,7 @@ import { auth } from '../lib/auth';
|
|||||||
import type { User } from '../lib/auth';
|
import type { User } from '../lib/auth';
|
||||||
|
|
||||||
export const adminRoutes = new Elysia({ prefix: '/admin' })
|
export const adminRoutes = new Elysia({ prefix: '/admin' })
|
||||||
|
.use(authMiddleware)
|
||||||
// Admin guard — all routes in this group require admin role
|
// Admin guard — all routes in this group require admin role
|
||||||
.onBeforeHandle(({ user, set }: { user: User; set: any }) => {
|
.onBeforeHandle(({ user, set }: { user: User; set: any }) => {
|
||||||
if ((user as any).role !== 'admin') {
|
if ((user as any).role !== 'admin') {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { authMiddleware } from '../middleware/auth';
|
||||||
import { Elysia, t } from 'elysia';
|
import { Elysia, t } from 'elysia';
|
||||||
import { db } from '../db';
|
import { db } from '../db';
|
||||||
import { auditLogs, users } from '../db/schema';
|
import { auditLogs, users } from '../db/schema';
|
||||||
@@ -5,6 +6,7 @@ import { eq, desc, and, gte, lte, ilike, or, sql } from 'drizzle-orm';
|
|||||||
import type { User } from '../lib/auth';
|
import type { User } from '../lib/auth';
|
||||||
|
|
||||||
export const auditLogRoutes = new Elysia({ prefix: '/audit-logs' })
|
export const auditLogRoutes = new Elysia({ prefix: '/audit-logs' })
|
||||||
|
.use(authMiddleware)
|
||||||
// Admin guard
|
// Admin guard
|
||||||
.onBeforeHandle(({ user, set }: { user: User; set: any }) => {
|
.onBeforeHandle(({ user, set }: { user: User; set: any }) => {
|
||||||
if ((user as any).role !== 'admin') {
|
if ((user as any).role !== 'admin') {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { authMiddleware } from '../middleware/auth';
|
||||||
import { Elysia, t } from 'elysia';
|
import { Elysia, t } from 'elysia';
|
||||||
import { db } from '../db';
|
import { db } from '../db';
|
||||||
import { clients, events } from '../db/schema';
|
import { clients, events } from '../db/schema';
|
||||||
@@ -84,6 +85,7 @@ const clientSchema = t.Object({
|
|||||||
const updateClientSchema = t.Partial(clientSchema);
|
const updateClientSchema = t.Partial(clientSchema);
|
||||||
|
|
||||||
export const clientRoutes = new Elysia({ prefix: '/clients' })
|
export const clientRoutes = new Elysia({ prefix: '/clients' })
|
||||||
|
.use(authMiddleware)
|
||||||
// List clients with optional search and pagination
|
// List clients with optional search and pagination
|
||||||
.get('/', async ({ query, user }: { query: { search?: string; tag?: string; page?: string; limit?: string }; user: User }) => {
|
.get('/', async ({ query, user }: { query: { search?: string; tag?: string; page?: string; limit?: string }; user: User }) => {
|
||||||
let baseQuery = db.select().from(clients).where(eq(clients.userId, user.id));
|
let baseQuery = db.select().from(clients).where(eq(clients.userId, user.id));
|
||||||
@@ -179,7 +181,9 @@ export const clientRoutes = new Elysia({ prefix: '/clients' })
|
|||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
// Auto-sync birthday/anniversary events
|
// Auto-sync birthday/anniversary events
|
||||||
await syncClientEvents(user.id, client);
|
if (client) {
|
||||||
|
await syncClientEvents(user.id, client);
|
||||||
|
}
|
||||||
|
|
||||||
return client;
|
return client;
|
||||||
}, {
|
}, {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { authMiddleware } from '../middleware/auth';
|
||||||
import { Elysia, t } from 'elysia';
|
import { Elysia, t } from 'elysia';
|
||||||
import { db } from '../db';
|
import { db } from '../db';
|
||||||
import { clients, communications, userProfiles } from '../db/schema';
|
import { clients, communications, userProfiles } from '../db/schema';
|
||||||
@@ -8,6 +9,7 @@ import type { User } from '../lib/auth';
|
|||||||
import { randomUUID } from 'crypto';
|
import { randomUUID } from 'crypto';
|
||||||
|
|
||||||
export const emailRoutes = new Elysia({ prefix: '/emails' })
|
export const emailRoutes = new Elysia({ prefix: '/emails' })
|
||||||
|
.use(authMiddleware)
|
||||||
// Generate email for a client
|
// Generate email for a client
|
||||||
.post('/generate', async ({ body, user }: {
|
.post('/generate', async ({ body, user }: {
|
||||||
body: {
|
body: {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { authMiddleware } from '../middleware/auth';
|
||||||
import { Elysia, t } from 'elysia';
|
import { Elysia, t } from 'elysia';
|
||||||
import { db } from '../db';
|
import { db } from '../db';
|
||||||
import { events, clients } from '../db/schema';
|
import { events, clients } from '../db/schema';
|
||||||
@@ -5,6 +6,7 @@ import { eq, and, gte, lte, sql } from 'drizzle-orm';
|
|||||||
import type { User } from '../lib/auth';
|
import type { User } from '../lib/auth';
|
||||||
|
|
||||||
export const eventRoutes = new Elysia({ prefix: '/events' })
|
export const eventRoutes = new Elysia({ prefix: '/events' })
|
||||||
|
.use(authMiddleware)
|
||||||
// List events with optional filters
|
// List events with optional filters
|
||||||
.get('/', async ({ query, user }: {
|
.get('/', async ({ query, user }: {
|
||||||
query: {
|
query: {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { authMiddleware } from '../middleware/auth';
|
||||||
import { Elysia, t } from 'elysia';
|
import { Elysia, t } from 'elysia';
|
||||||
import { db } from '../db';
|
import { db } from '../db';
|
||||||
import { clients, events } from '../db/schema';
|
import { clients, events } from '../db/schema';
|
||||||
@@ -160,6 +161,7 @@ async function syncClientEvents(userId: string, client: { id: string; firstName:
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const importRoutes = new Elysia({ prefix: '/clients' })
|
export const importRoutes = new Elysia({ prefix: '/clients' })
|
||||||
|
.use(authMiddleware)
|
||||||
// Preview CSV - returns headers and auto-mapped columns + sample rows
|
// Preview CSV - returns headers and auto-mapped columns + sample rows
|
||||||
.post('/import/preview', async ({ body, user }: { body: { file: File }; user: User }) => {
|
.post('/import/preview', async ({ body, user }: { body: { file: File }; user: User }) => {
|
||||||
const text = await body.file.text();
|
const text = await body.file.text();
|
||||||
@@ -211,6 +213,7 @@ export const importRoutes = new Elysia({ prefix: '/clients' })
|
|||||||
|
|
||||||
for (let i = 0; i < dataRows.length; i++) {
|
for (let i = 0; i < dataRows.length; i++) {
|
||||||
const row = dataRows[i];
|
const row = dataRows[i];
|
||||||
|
if (!row) continue;
|
||||||
try {
|
try {
|
||||||
const record: Record<string, any> = {};
|
const record: Record<string, any> = {};
|
||||||
|
|
||||||
@@ -260,12 +263,14 @@ export const importRoutes = new Elysia({ prefix: '/clients' })
|
|||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
// Sync events
|
// Sync events
|
||||||
await syncClientEvents(user.id, {
|
if (client) {
|
||||||
id: client.id,
|
await syncClientEvents(user.id, {
|
||||||
firstName: client.firstName,
|
id: client.id,
|
||||||
birthday: client.birthday,
|
firstName: client.firstName,
|
||||||
anniversary: client.anniversary,
|
birthday: client.birthday,
|
||||||
});
|
anniversary: client.anniversary,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
results.imported++;
|
results.imported++;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { authMiddleware } from '../middleware/auth';
|
||||||
import { Elysia } from 'elysia';
|
import { Elysia } from 'elysia';
|
||||||
import { db } from '../db';
|
import { db } from '../db';
|
||||||
import { clients, events } from '../db/schema';
|
import { clients, events } from '../db/schema';
|
||||||
@@ -5,6 +6,7 @@ import { eq, and, sql, lte, gte, isNull, or } from 'drizzle-orm';
|
|||||||
import type { User } from '../lib/auth';
|
import type { User } from '../lib/auth';
|
||||||
|
|
||||||
export const insightsRoutes = new Elysia({ prefix: '/insights' })
|
export const insightsRoutes = new Elysia({ prefix: '/insights' })
|
||||||
|
.use(authMiddleware)
|
||||||
.get('/', async ({ user }: { user: User }) => {
|
.get('/', async ({ user }: { user: User }) => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { authMiddleware } from '../middleware/auth';
|
||||||
import { Elysia, t } from 'elysia';
|
import { Elysia, t } from 'elysia';
|
||||||
import { db } from '../db';
|
import { db } from '../db';
|
||||||
import { interactions, clients } from '../db/schema';
|
import { interactions, clients } from '../db/schema';
|
||||||
@@ -5,6 +6,7 @@ import { eq, and, desc } from 'drizzle-orm';
|
|||||||
import type { User } from '../lib/auth';
|
import type { User } from '../lib/auth';
|
||||||
|
|
||||||
export const interactionRoutes = new Elysia()
|
export const interactionRoutes = new Elysia()
|
||||||
|
.use(authMiddleware)
|
||||||
// List interactions for a client
|
// List interactions for a client
|
||||||
.get('/clients/:clientId/interactions', async ({ params, user }: { params: { clientId: string }; user: User }) => {
|
.get('/clients/:clientId/interactions', async ({ params, user }: { params: { clientId: string }; user: User }) => {
|
||||||
// Verify client belongs to user
|
// Verify client belongs to user
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { authMiddleware } from '../middleware/auth';
|
||||||
import { Elysia, t } from 'elysia';
|
import { Elysia, t } from 'elysia';
|
||||||
import { db } from '../db';
|
import { db } from '../db';
|
||||||
import { clients, interactions, communications, events, clientNotes } from '../db/schema';
|
import { clients, interactions, communications, events, clientNotes } from '../db/schema';
|
||||||
@@ -6,6 +7,7 @@ import type { User } from '../lib/auth';
|
|||||||
import { generateMeetingPrep } from '../services/ai';
|
import { generateMeetingPrep } from '../services/ai';
|
||||||
|
|
||||||
export const meetingPrepRoutes = new Elysia()
|
export const meetingPrepRoutes = new Elysia()
|
||||||
|
.use(authMiddleware)
|
||||||
// Get meeting prep for a client
|
// Get meeting prep for a client
|
||||||
.get('/clients/:id/meeting-prep', async ({ params, user, query }: {
|
.get('/clients/:id/meeting-prep', async ({ params, user, query }: {
|
||||||
params: { id: string };
|
params: { id: string };
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { authMiddleware } from '../middleware/auth';
|
||||||
import { Elysia, t } from 'elysia';
|
import { Elysia, t } from 'elysia';
|
||||||
import { db } from '../db';
|
import { db } from '../db';
|
||||||
import { clients } from '../db/schema';
|
import { clients } from '../db/schema';
|
||||||
@@ -23,6 +24,7 @@ function toClientProfile(c: typeof clients.$inferSelect): ClientProfile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const networkRoutes = new Elysia({ prefix: '/network' })
|
export const networkRoutes = new Elysia({ prefix: '/network' })
|
||||||
|
.use(authMiddleware)
|
||||||
// Get all network matches for the user's clients
|
// Get all network matches for the user's clients
|
||||||
.get('/matches', async (ctx) => {
|
.get('/matches', async (ctx) => {
|
||||||
const user = (ctx as any).user;
|
const user = (ctx as any).user;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { authMiddleware } from '../middleware/auth';
|
||||||
import { Elysia, t } from 'elysia';
|
import { Elysia, t } from 'elysia';
|
||||||
import { db } from '../db';
|
import { db } from '../db';
|
||||||
import { clientNotes, clients } from '../db/schema';
|
import { clientNotes, clients } from '../db/schema';
|
||||||
@@ -5,6 +6,7 @@ import { eq, and, desc } from 'drizzle-orm';
|
|||||||
import type { User } from '../lib/auth';
|
import type { User } from '../lib/auth';
|
||||||
|
|
||||||
export const notesRoutes = new Elysia({ prefix: '/clients/:clientId/notes' })
|
export const notesRoutes = new Elysia({ prefix: '/clients/:clientId/notes' })
|
||||||
|
.use(authMiddleware)
|
||||||
// List notes for a client
|
// List notes for a client
|
||||||
.get('/', async ({ params, user }: { params: { clientId: string }; user: User }) => {
|
.get('/', async ({ params, user }: { params: { clientId: string }; user: User }) => {
|
||||||
// Verify client belongs to user
|
// Verify client belongs to user
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { authMiddleware } from '../middleware/auth';
|
||||||
import { Elysia, t } from 'elysia';
|
import { Elysia, t } from 'elysia';
|
||||||
import { db } from '../db';
|
import { db } from '../db';
|
||||||
import { notifications, clients } from '../db/schema';
|
import { notifications, clients } from '../db/schema';
|
||||||
@@ -5,6 +6,7 @@ import { eq, and, desc, sql } from 'drizzle-orm';
|
|||||||
import type { User } from '../lib/auth';
|
import type { User } from '../lib/auth';
|
||||||
|
|
||||||
export const notificationRoutes = new Elysia({ prefix: '/notifications' })
|
export const notificationRoutes = new Elysia({ prefix: '/notifications' })
|
||||||
|
.use(authMiddleware)
|
||||||
// List notifications
|
// List notifications
|
||||||
.get('/', async ({ query, user }: { query: { limit?: string; unreadOnly?: string }; user: User }) => {
|
.get('/', async ({ query, user }: { query: { limit?: string; unreadOnly?: string }; user: User }) => {
|
||||||
const limit = query.limit ? parseInt(query.limit) : 50;
|
const limit = query.limit ? parseInt(query.limit) : 50;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { authMiddleware } from '../middleware/auth';
|
||||||
import { Elysia, t } from 'elysia';
|
import { Elysia, t } from 'elysia';
|
||||||
import { db } from '../db';
|
import { db } from '../db';
|
||||||
import { users, userProfiles, accounts } from '../db/schema';
|
import { users, userProfiles, accounts } from '../db/schema';
|
||||||
@@ -6,6 +7,7 @@ import type { User } from '../lib/auth';
|
|||||||
import { logAudit, getRequestMeta } from '../services/audit';
|
import { logAudit, getRequestMeta } from '../services/audit';
|
||||||
|
|
||||||
export const profileRoutes = new Elysia({ prefix: '/profile' })
|
export const profileRoutes = new Elysia({ prefix: '/profile' })
|
||||||
|
.use(authMiddleware)
|
||||||
// Get current user's profile
|
// Get current user's profile
|
||||||
.get('/', async ({ user }: { user: User }) => {
|
.get('/', async ({ user }: { user: User }) => {
|
||||||
// Get user and profile
|
// Get user and profile
|
||||||
@@ -159,8 +161,9 @@ export const profileRoutes = new Elysia({ prefix: '/profile' })
|
|||||||
.where(eq(userProfiles.userId, user.id))
|
.where(eq(userProfiles.userId, user.id))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
|
const tone = (body.tone || 'friendly') as 'formal' | 'friendly' | 'casual';
|
||||||
const style = {
|
const style = {
|
||||||
tone: body.tone || 'friendly',
|
tone,
|
||||||
greeting: body.greeting || '',
|
greeting: body.greeting || '',
|
||||||
signoff: body.signoff || '',
|
signoff: body.signoff || '',
|
||||||
writingSamples: (body.writingSamples || []).slice(0, 3),
|
writingSamples: (body.writingSamples || []).slice(0, 3),
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { authMiddleware } from '../middleware/auth';
|
||||||
import { Elysia } from 'elysia';
|
import { Elysia } from 'elysia';
|
||||||
import { db } from '../db';
|
import { db } from '../db';
|
||||||
import { clients, events, communications } from '../db/schema';
|
import { clients, events, communications } from '../db/schema';
|
||||||
@@ -5,6 +6,7 @@ import { eq, and, sql, gte, lte, count, desc } from 'drizzle-orm';
|
|||||||
import type { User } from '../lib/auth';
|
import type { User } from '../lib/auth';
|
||||||
|
|
||||||
export const reportsRoutes = new Elysia()
|
export const reportsRoutes = new Elysia()
|
||||||
|
.use(authMiddleware)
|
||||||
// Analytics overview
|
// Analytics overview
|
||||||
.get('/reports/overview', async ({ user }: { user: User }) => {
|
.get('/reports/overview', async ({ user }: { user: User }) => {
|
||||||
const userId = user.id;
|
const userId = user.id;
|
||||||
@@ -84,21 +86,21 @@ export const reportsRoutes = new Elysia()
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
clients: {
|
clients: {
|
||||||
total: totalClients.count,
|
total: totalClients?.count ?? 0,
|
||||||
newThisMonth: newClientsMonth.count,
|
newThisMonth: newClientsMonth?.count ?? 0,
|
||||||
newThisWeek: newClientsWeek.count,
|
newThisWeek: newClientsWeek?.count ?? 0,
|
||||||
contactedRecently: contactedRecently.count,
|
contactedRecently: contactedRecently?.count ?? 0,
|
||||||
neverContacted: neverContacted.count,
|
neverContacted: neverContacted?.count ?? 0,
|
||||||
},
|
},
|
||||||
emails: {
|
emails: {
|
||||||
total: totalEmails.count,
|
total: totalEmails?.count ?? 0,
|
||||||
sent: emailsSent.count,
|
sent: emailsSent?.count ?? 0,
|
||||||
draft: emailsDraft.count,
|
draft: emailsDraft?.count ?? 0,
|
||||||
sentLast30Days: emailsRecent.count,
|
sentLast30Days: emailsRecent?.count ?? 0,
|
||||||
},
|
},
|
||||||
events: {
|
events: {
|
||||||
total: totalEvents.count,
|
total: totalEvents?.count ?? 0,
|
||||||
upcoming30Days: upcomingEvents.count,
|
upcoming30Days: upcomingEvents?.count ?? 0,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
@@ -406,11 +408,11 @@ export const reportsRoutes = new Elysia()
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (draftCount.count > 0) {
|
if ((draftCount?.count ?? 0) > 0) {
|
||||||
notifications.push({
|
notifications.push({
|
||||||
id: 'drafts',
|
id: 'drafts',
|
||||||
type: 'drafts' as const,
|
type: 'drafts' as const,
|
||||||
title: `${draftCount.count} draft email${draftCount.count > 1 ? 's' : ''} pending`,
|
title: `${draftCount?.count ?? 0} draft email${(draftCount?.count ?? 0) > 1 ? 's' : ''} pending`,
|
||||||
description: 'Review and send your drafted emails',
|
description: 'Review and send your drafted emails',
|
||||||
date: new Date().toISOString(),
|
date: new Date().toISOString(),
|
||||||
link: '/emails',
|
link: '/emails',
|
||||||
@@ -434,7 +436,7 @@ export const reportsRoutes = new Elysia()
|
|||||||
overdue: overdueEvents.length,
|
overdue: overdueEvents.length,
|
||||||
upcoming: upcomingEvents.length,
|
upcoming: upcomingEvents.length,
|
||||||
stale: staleClients.length,
|
stale: staleClients.length,
|
||||||
drafts: draftCount.count,
|
drafts: draftCount?.count ?? 0,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { authMiddleware } from '../middleware/auth';
|
||||||
import { Elysia, t } from 'elysia';
|
import { Elysia, t } from 'elysia';
|
||||||
import { db } from '../db';
|
import { db } from '../db';
|
||||||
import { clientSegments, clients } from '../db/schema';
|
import { clientSegments, clients } from '../db/schema';
|
||||||
@@ -77,6 +78,7 @@ function buildClientConditions(filters: SegmentFilters, userId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const segmentRoutes = new Elysia({ prefix: '/segments' })
|
export const segmentRoutes = new Elysia({ prefix: '/segments' })
|
||||||
|
.use(authMiddleware)
|
||||||
// List saved segments
|
// List saved segments
|
||||||
.get('/', async ({ user }: { user: User }) => {
|
.get('/', async ({ user }: { user: User }) => {
|
||||||
return db.select()
|
return db.select()
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { authMiddleware } from '../middleware/auth';
|
||||||
import { Elysia, t } from 'elysia';
|
import { Elysia, t } from 'elysia';
|
||||||
import { db } from '../db';
|
import { db } from '../db';
|
||||||
import { clients } from '../db/schema';
|
import { clients } from '../db/schema';
|
||||||
@@ -5,6 +6,7 @@ import { eq, sql } from 'drizzle-orm';
|
|||||||
import type { User } from '../lib/auth';
|
import type { User } from '../lib/auth';
|
||||||
|
|
||||||
export const tagRoutes = new Elysia({ prefix: '/tags' })
|
export const tagRoutes = new Elysia({ prefix: '/tags' })
|
||||||
|
.use(authMiddleware)
|
||||||
// GET /api/tags - all unique tags with client counts
|
// GET /api/tags - all unique tags with client counts
|
||||||
.get('/', async ({ user }: { user: User }) => {
|
.get('/', async ({ user }: { user: User }) => {
|
||||||
const allClients = await db.select({ tags: clients.tags })
|
const allClients = await db.select({ tags: clients.tags })
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { authMiddleware } from '../middleware/auth';
|
||||||
import { Elysia, t } from 'elysia';
|
import { Elysia, t } from 'elysia';
|
||||||
import { db } from '../db';
|
import { db } from '../db';
|
||||||
import { emailTemplates } from '../db/schema';
|
import { emailTemplates } from '../db/schema';
|
||||||
@@ -5,6 +6,7 @@ import { eq, and, desc, sql } from 'drizzle-orm';
|
|||||||
import type { User } from '../lib/auth';
|
import type { User } from '../lib/auth';
|
||||||
|
|
||||||
export const templateRoutes = new Elysia({ prefix: '/templates' })
|
export const templateRoutes = new Elysia({ prefix: '/templates' })
|
||||||
|
.use(authMiddleware)
|
||||||
// List templates
|
// List templates
|
||||||
.get('/', async ({ query, user }: { query: { category?: string }; user: User }) => {
|
.get('/', async ({ query, user }: { query: { category?: string }; user: User }) => {
|
||||||
let conditions = [eq(emailTemplates.userId, user.id)];
|
let conditions = [eq(emailTemplates.userId, user.id)];
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { PgBoss } from 'pg-boss';
|
import { PgBoss } from 'pg-boss';
|
||||||
|
import type { Job } from 'pg-boss';
|
||||||
import { db } from '../db';
|
import { db } from '../db';
|
||||||
import { events, notifications, clients, users } from '../db/schema';
|
import { events, notifications, clients, users } from '../db/schema';
|
||||||
import { eq, and, gte, lte, sql } from 'drizzle-orm';
|
import { eq, and, gte, lte, sql } from 'drizzle-orm';
|
||||||
@@ -22,8 +23,8 @@ export async function initJobQueue(): Promise<PgBoss> {
|
|||||||
console.log('✅ pg-boss job queue started');
|
console.log('✅ pg-boss job queue started');
|
||||||
|
|
||||||
// Register job handlers
|
// Register job handlers
|
||||||
await boss.work('check-upcoming-events', { teamConcurrency: 1 }, checkUpcomingEvents);
|
await boss.work('check-upcoming-events', { localConcurrency: 1 }, checkUpcomingEvents);
|
||||||
await boss.work('send-event-reminder', { teamConcurrency: 5 }, sendEventReminder);
|
await boss.work('send-event-reminder', { localConcurrency: 5 }, sendEventReminder);
|
||||||
|
|
||||||
// Schedule daily check at 8am UTC
|
// Schedule daily check at 8am UTC
|
||||||
await boss.schedule('check-upcoming-events', '0 8 * * *', {}, {
|
await boss.schedule('check-upcoming-events', '0 8 * * *', {}, {
|
||||||
@@ -39,7 +40,7 @@ export function getJobQueue(): PgBoss | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Job: Check upcoming events and create notifications
|
// Job: Check upcoming events and create notifications
|
||||||
async function checkUpcomingEvents(job: PgBoss.Job) {
|
async function checkUpcomingEvents(jobs: Job[]) {
|
||||||
console.log(`[jobs] Running checkUpcomingEvents at ${new Date().toISOString()}`);
|
console.log(`[jobs] Running checkUpcomingEvents at ${new Date().toISOString()}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -118,14 +119,18 @@ async function checkUpcomingEvents(job: PgBoss.Job) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Job: Send email reminder to advisor
|
// Job: Send email reminder to advisor
|
||||||
async function sendEventReminder(job: PgBoss.Job<{
|
interface EventReminderData {
|
||||||
userId: string;
|
userId: string;
|
||||||
eventId: string;
|
eventId: string;
|
||||||
clientId: string;
|
clientId: string;
|
||||||
eventTitle: string;
|
eventTitle: string;
|
||||||
clientName: string;
|
clientName: string;
|
||||||
daysUntil: number;
|
daysUntil: number;
|
||||||
}>) {
|
}
|
||||||
|
|
||||||
|
async function sendEventReminder(jobs: Job<EventReminderData>[]) {
|
||||||
|
const job = jobs[0];
|
||||||
|
if (!job) return;
|
||||||
const { userId, eventTitle, clientName, daysUntil } = job.data;
|
const { userId, eventTitle, clientName, daysUntil } = job.data;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user