import express from 'express'; import { createServer } from 'http'; import { Server } from 'socket.io'; import { PrismaClient } from '@prisma/client'; import { handler } from './build/handler.js'; const app = express(); const server = createServer(app); const io = new Server(server, { cors: { origin: process.env.ORIGIN || 'http://localhost:3000', credentials: true } }); // ─── Prisma client for session validation ─────────────────── const prisma = new PrismaClient(); // ─── Presence tracking ────────────────────────────────────── // Map> const presence = new Map(); function getPresenceList(boardId) { const room = presence.get(boardId); if (!room) return []; return Array.from(room.values()); } // ─── Cookie parser ────────────────────────────────────────── function parseCookies(cookieHeader) { const cookies = {}; if (!cookieHeader) return cookies; cookieHeader.split(';').forEach((cookie) => { const [name, ...rest] = cookie.trim().split('='); cookies[name] = decodeURIComponent(rest.join('=')); }); return cookies; } // ─── Socket.IO auth middleware ─────────────────────────────── io.use(async (socket, next) => { try { const cookies = parseCookies(socket.handshake.headers.cookie); const sessionId = cookies['session']; if (!sessionId) { socket.data.user = null; return next(); } const session = await prisma.session.findUnique({ where: { id: sessionId }, include: { user: { select: { id: true, name: true, avatarUrl: true, tenantId: true } } } }); if (session && session.expiresAt > new Date()) { socket.data.user = session.user; } else { socket.data.user = null; } next(); } catch (err) { console.error('Socket auth error:', err); socket.data.user = null; next(); } }); // ─── Socket.IO connection handler ──────────────────────────── io.on('connection', (socket) => { const user = socket.data.user; console.log(`Socket connected: ${socket.id} (user: ${user?.name ?? 'anon'})`); // Track which boards this socket has joined (for cleanup on disconnect) const joinedBoards = new Set(); socket.on('join-board', (boardId) => { socket.join(`board:${boardId}`); joinedBoards.add(boardId); if (user) { if (!presence.has(boardId)) presence.set(boardId, new Map()); presence.get(boardId).set(socket.id, { id: user.id, name: user.name, avatarUrl: user.avatarUrl }); } // Notify others socket.to(`board:${boardId}`).emit('user-joined', { user: user ? { id: user.id, name: user.name, avatarUrl: user.avatarUrl } : null, socketId: socket.id }); // Send full presence list to everyone in the room (including the joiner) io.to(`board:${boardId}`).emit('presence:update', getPresenceList(boardId)); }); socket.on('leave-board', (boardId) => { socket.leave(`board:${boardId}`); joinedBoards.delete(boardId); if (presence.has(boardId)) { presence.get(boardId).delete(socket.id); if (presence.get(boardId).size === 0) presence.delete(boardId); } socket.to(`board:${boardId}`).emit('user-left', { user: user ? { id: user.id, name: user.name } : null, socketId: socket.id }); // Update presence for remaining const room = presence.get(boardId); io.to(`board:${boardId}`).emit('presence:update', room ? Array.from(room.values()) : []); }); socket.on('disconnect', () => { console.log(`Socket disconnected: ${socket.id}`); for (const boardId of joinedBoards) { if (presence.has(boardId)) { presence.get(boardId).delete(socket.id); if (presence.get(boardId).size === 0) { presence.delete(boardId); } } socket.to(`board:${boardId}`).emit('user-left', { user: user ? { id: user.id, name: user.name } : null, socketId: socket.id }); const room = presence.get(boardId); io.to(`board:${boardId}`).emit('presence:update', room ? Array.from(room.values()) : []); } }); }); // Store io globally so SvelteKit API routes can broadcast via realtime.ts globalThis.__socketIO = io; // SvelteKit handler app.use(handler); const port = process.env.PORT || 3000; const host = process.env.HOST || '0.0.0.0'; server.listen(port, host, () => { console.log(`Server running on http://${host}:${port}`); });