/opt/canhelp/apps/api/src
NameSizeModeActions
lib/-0755rm
middleware/-0755rm
routes/-0755rm
auth.ts156000644editdlrm
db.ts1740644editdlrm
index.ts239420644editdlrm
redis.ts2490644editdlrm
seed.ts830180644editdlrm
socket.ts57540644editdlrm
sync-categories-from-prod.ts27920644editdlrm
translate.ts220300644editdlrm
Edit: /opt/canhelp/apps/api/src/socket.ts (5754B)
import type { Server as HttpServer } from 'http' import { Server } from 'socket.io' import { db } from './db.js' import { messages, chatRooms, users } from '@canhelp/db' import { notifyNewMessage } from './lib/notif.js' import { eq } from 'drizzle-orm' import { auth } from './auth.js' import { emailNewMessage } from './lib/email.js' import { abbrevName } from './lib/nameUtils.js' import { translateMessageAllLocales } from './translate.js' import { checkMessageLimit, incrementMessages } from './lib/daily-limits.js' import { canSendMessage } from './lib/messaging-rules.js' export function setupSocket(httpServer: HttpServer) { const io = new Server(httpServer, { cors: { origin: process.env.WEB_URL || 'http://localhost:3000', credentials: true, }, }) // Auth middleware — supports Bearer token (Flutter) and cookies (web) io.use(async (socket, next) => { try { const token = socket.handshake.auth?.token || socket.handshake.headers?.authorization?.replace('Bearer ', '') const cookieHeader = socket.handshake.headers?.cookie let session = null if (token) { const headers = new Headers({ authorization: `Bearer ${token}` }) session = await auth.api.getSession({ headers }) } else if (cookieHeader) { const headers = new Headers({ cookie: cookieHeader }) session = await auth.api.getSession({ headers }) } if (!session) return next(new Error('Unauthorized')) socket.data.userId = session.user.id next() } catch { next(new Error('Unauthorized')) } }) io.on('connection', (socket) => { const userId: string = socket.data.userId console.log(`[Socket] User connected: ${userId}`) // Join user's personal room for notifications socket.join(`user:${userId}`) // Join a chat room socket.on('join_room', async (roomId: string) => { const [room] = await db.select().from(chatRooms).where(eq(chatRooms.id, roomId)) if (!room) return if (room.customerId !== userId && room.specialistId !== userId) return socket.join(`room:${roomId}`) }) // Send a message socket.on('send_message', async (data: { roomId: string; body: string; locale?: string; imageUrl?: string }) => { if (!data.roomId || (!data.body?.trim() && !data.imageUrl)) return // Validate imageUrl is an internal upload path if (data.imageUrl && !data.imageUrl.startsWith('/api/uploads/')) return const [room] = await db.select().from(chatRooms).where(eq(chatRooms.id, data.roomId)) if (!room) return if (room.customerId !== userId && room.specialistId !== userId) return // Check messaging rules (plan-based) const recipientId = room.customerId === userId ? room.specialistId : room.customerId const msgRule = await canSendMessage(userId, recipientId) if (!msgRule.allowed) { socket.emit('error_message', { error: msgRule.reason, code: msgRule.code, senderTier: msgRule.senderTier, recipientTier: msgRule.recipientTier, }) return } // Check daily message limit const msgCheck = await checkMessageLimit(userId) if (!msgCheck.allowed) { socket.emit('error_message', { error: 'Daily message limit reached', code: 'DAILY_LIMIT_REACHED', limit: msgCheck.limit, current: msgCheck.current, }) return } const [msg] = await db .insert(messages) .values({ roomId: data.roomId, senderId: userId, body: data.body?.trim() ?? '', imageUrl: data.imageUrl ?? null }) .returning() // Increment daily message counter incrementMessages(userId).catch(() => {}) io.to(`room:${data.roomId}`).emit('new_message', msg) // Translate text body (skip for image-only messages) if (data.body?.trim()) { translateMessageAllLocales(data.body.trim(), data.locale ?? 'el').then(async (translations) => { await db.update(messages).set({ translations }).where(eq(messages.id, msg.id)).catch(() => {}) io.to(`room:${data.roomId}`).emit('message_translated', { id: msg.id, roomId: data.roomId, translations }) }).catch(() => {}) } // Notify recipient const notifPreview = data.imageUrl ? '📷 Photo' : data.body const notif = await notifyNewMessage(recipientId, notifPreview, data.roomId) if (notif) io.to(`user:${recipientId}`).emit('notification', notif) // Email recipient (fire-and-forget, only if they're not currently connected) const recipientSockets = await io.in(`user:${recipientId}`).fetchSockets() if (recipientSockets.length === 0) { const [sender] = await db.select().from(users).where(eq(users.id, userId)) const [recipient] = await db.select().from(users).where(eq(users.id, recipientId)) if (recipient?.email && sender) { emailNewMessage({ to: recipient.email, recipientName: recipient.firstName || recipient.name || 'Χρήστη', senderName: abbrevName(sender.firstName, sender.lastName, sender.name), preview: data.imageUrl ? '📷 Photo' : data.body.slice(0, 120), roomId: data.roomId, locale: (recipient.locale as 'el' | 'en' | 'ru') || 'el', }).catch(() => {}) } } }) // Mark messages as read socket.on('mark_read', async (roomId: string) => { await db .update(messages) .set({ isRead: true }) .where(eq(messages.roomId, roomId)) }) socket.on('disconnect', () => { console.log(`[Socket] User disconnected: ${userId}`) }) }) return io }