BusyBee3333 4e6467ffb0 Add CRESync CRM application with Setup page
- Build complete Next.js CRM for commercial real estate
- Add authentication with JWT sessions and role-based access
- Add GoHighLevel API integration for contacts, conversations, opportunities
- Add AI-powered Control Center with tool calling
- Add Setup page with onboarding checklist (/setup)
- Add sidebar navigation with Setup menu item
- Fix type errors in onboarding API, GHL services, and control center tools
- Add Prisma schema with SQLite for local development
- Add UI components with clay morphism design system

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 17:30:55 -05:00

75 lines
2.0 KiB
TypeScript

import { NextRequest } from 'next/server';
import { verifyToken } from '@/lib/auth/jwt';
import { broadcaster } from '@/lib/realtime/broadcaster';
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
// Helper to get user from request
function getUserFromRequest(request: NextRequest): { userId: string; email: string } | null {
const authHeader = request.headers.get('authorization');
const token = authHeader?.replace('Bearer ', '');
if (!token) return null;
try {
const payload = verifyToken(token);
return { userId: payload.userId, email: payload.email };
} catch {
return null;
}
}
export async function GET(request: NextRequest) {
const user = getUserFromRequest(request);
if (!user) {
return new Response('Unauthorized', { status: 401 });
}
const encoder = new TextEncoder();
const stream = new ReadableStream({
start(controller) {
// Send initial connection message
controller.enqueue(
encoder.encode(`data: ${JSON.stringify({ type: 'connected', userId: user.userId })}\n\n`)
);
// Subscribe to user's events
const unsubscribe = broadcaster.subscribe(user.userId, (message) => {
controller.enqueue(
encoder.encode(`data: ${JSON.stringify(message)}\n\n`)
);
});
// Also subscribe to global events
const unsubscribeGlobal = broadcaster.subscribe('global', (message) => {
controller.enqueue(
encoder.encode(`data: ${JSON.stringify(message)}\n\n`)
);
});
// Keep connection alive with heartbeat
const heartbeat = setInterval(() => {
controller.enqueue(encoder.encode(`: heartbeat\n\n`));
}, 30000);
// Cleanup on close
request.signal.addEventListener('abort', () => {
clearInterval(heartbeat);
unsubscribe();
unsubscribeGlobal();
});
},
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
});
}