feat: Enhance provider components with detailed documentation and comments

- Added comprehensive comments and JSDoc-style documentation to NotificationProvider, ThemeProvider, UserProvider, and WebsiteThemeProvider for better clarity and maintainability.
- Improved type definitions in index.ts for better code understanding and usage.
- Introduced Docker support with Dockerfiles for various services including AI server, signaling server, and browser-use service.
- Created a docker-compose.yml file to orchestrate multiple services including PostgreSQL, AI, scraper, and frontend.
- Added a startup guide (startup.txt) for setting up the CRM environment on Ubuntu with Docker.
- Included a .dockerignore file to exclude unnecessary files from Docker builds.
This commit is contained in:
Ace
2026-07-13 13:05:30 +02:00
parent dba4c84cd5
commit d35c806d5b
167 changed files with 3474 additions and 102 deletions
+63
View File
@@ -1,8 +1,20 @@
// ── WebRTC Signaling Server ─────────────────────────────────────────
// Provides real-time signaling for the CRM chat system using Socket.IO:
// - JWT-based authentication middleware
// - Online user presence tracking (online/offline)
// - Peer-to-peer call signaling (offer/answer/ICE candidates)
// - Multi-participant room-based WebRTC (join/leave/relay)
// - Message deletion broadcast to conversation participants
//
// Clients authenticate via socket handshake auth token; unauthenticated
// users can still join rooms as anonymous participants.
import { createServer } from "http"
import { Server } from "socket.io"
import { SignJWT, jwtVerify } from "jose"
import pg from "pg"
// ── Configuration ─────────────────────────────────────────────────
const PORT = process.env.SIGNALING_PORT || 3007
const rawSecret = process.env.JWT_SECRET
if (!rawSecret) throw new Error("JWT_SECRET environment variable is required")
@@ -10,14 +22,23 @@ const JWT_SECRET = new TextEncoder().encode(rawSecret)
const DATABASE_URL = process.env.DATABASE_URL
if (!DATABASE_URL) throw new Error("DATABASE_URL environment variable is required")
// ── Database pool ─────────────────────────────────────────────────
const pool = new pg.Pool({ connectionString: DATABASE_URL })
// ── HTTP + Socket.IO server ───────────────────────────────────────
const httpServer = createServer()
const io = new Server(httpServer, { cors: { origin: "*", methods: ["GET", "POST"] } })
// ── In-memory state ───────────────────────────────────────────────
// onlineUsers maps userId -> socketId for presence tracking.
// rooms maps roomId -> array of { socketId, id, label } participants.
const onlineUsers = new Map()
const rooms = new Map()
// ── Authentication middleware ─────────────────────────────────────
// Verifies the JWT token from handshake auth. If valid, attaches
// userId and role to the socket's data bag. Unauthenticated sockets
// proceed with null user info.
io.use((socket, next) => {
const token = socket.handshake.auth?.token
if (token) {
@@ -39,15 +60,25 @@ io.use((socket, next) => {
}
})
// ── Connection handler ────────────────────────────────────────────
// Registers authenticated users as online, then wires up all
// per-socket event listeners for signaling and presence.
io.on("connection", (socket) => {
const userId = socket.data.userId
// Mark the user online and broadcast to other clients
if (userId) {
onlineUsers.set(userId, socket.id)
socket.broadcast.emit("user:online", { userId })
}
// ── Authenticated event handlers ──────────────────────────────
// Only users with a valid JWT token may use these features.
if (userId) {
// user:lookup — looks up a user by phone number in the database.
// Fires when the client searches for a contact to start a call.
// Returns { found, user } or { found: false }.
socket.on("user:lookup", async ({ phone }, callback) => {
try {
const result = await pool.query(
@@ -79,6 +110,9 @@ io.on("connection", (socket) => {
}
})
// call:offer — relays an SDP offer to the target user.
// Fires when the caller initiates a WebRTC call.
// callerInfo is fetched from the DB if not supplied.
socket.on("call:offer", async ({ to, sdp, callerInfo }) => {
const targetSocketId = onlineUsers.get(to)
if (targetSocketId) {
@@ -112,6 +146,8 @@ io.on("connection", (socket) => {
}
})
// call:answer — relays an SDP answer back to the caller.
// Fires when the callee accepts the incoming call.
socket.on("call:answer", ({ to, sdp }) => {
const targetSocketId = onlineUsers.get(to)
if (targetSocketId) {
@@ -119,6 +155,8 @@ io.on("connection", (socket) => {
}
})
// call:ice-candidate — relays ICE candidates between peers.
// Fires during connection negotiation for NAT traversal.
socket.on("call:ice-candidate", ({ to, candidate }) => {
const targetSocketId = onlineUsers.get(to)
if (targetSocketId) {
@@ -126,6 +164,7 @@ io.on("connection", (socket) => {
}
})
// call:end — notifies the remote peer that the call ended.
socket.on("call:end", ({ to }) => {
const targetSocketId = onlineUsers.get(to)
if (targetSocketId) {
@@ -133,6 +172,7 @@ io.on("connection", (socket) => {
}
})
// call:reject — tells the caller the callee rejected the call.
socket.on("call:reject", ({ to }) => {
const targetSocketId = onlineUsers.get(to)
if (targetSocketId) {
@@ -140,6 +180,7 @@ io.on("connection", (socket) => {
}
})
// call:busy — tells the caller the callee is on another call.
socket.on("call:busy", ({ to }) => {
const targetSocketId = onlineUsers.get(to)
if (targetSocketId) {
@@ -147,6 +188,9 @@ io.on("connection", (socket) => {
}
})
// message:deleted — broadcasts a deletion event to all other
// participants in the conversation. Looks up participant IDs
// from the database so only relevant clients are notified.
socket.on("message:deleted", async ({ conversationId, messageId, senderId }) => {
try {
const result = await pool.query(
@@ -167,6 +211,15 @@ io.on("connection", (socket) => {
})
}
// ── Room-based signaling (authenticated + anonymous) ──────────
// These events support multi-participant rooms used for features
// like screen sharing or group calls. Anonymous users are assigned
// an "anon-" prefix ID.
// room:join — adds the socket to a named room. When the first
// participant joins they receive "room:waiting". When the second
// joins the first is told to initiate an offer. Additional
// participants are notified normally.
socket.on("room:join", ({ roomId, displayName }) => {
const participantId = userId || `anon-${socket.id}`
const participantLabel = displayName || participantId
@@ -191,18 +244,24 @@ io.on("connection", (socket) => {
}
})
// room:offer — relays an SDP offer to all other room participants.
socket.on("room:offer", ({ roomId, sdp }) => {
socket.to(roomId).emit("room:offer", { sdp })
})
// room:answer — relays an SDP answer to all other room participants.
socket.on("room:answer", ({ roomId, sdp }) => {
socket.to(roomId).emit("room:answer", { sdp })
})
// room:ice-candidate — relays ICE candidates to all other room participants.
socket.on("room:ice-candidate", ({ roomId, candidate }) => {
socket.to(roomId).emit("room:ice-candidate", { candidate })
})
// room:leave — removes the socket from a room. Cleans up the
// in-memory room map and notifies remaining participants. Rooms
// are fully deleted when empty.
socket.on("room:leave", ({ roomId }) => {
socket.leave(roomId)
const participants = rooms.get(roomId)
@@ -218,6 +277,9 @@ io.on("connection", (socket) => {
}
})
// disconnect — fires when the socket loses connection. Cleans up
// the user from all rooms they were in, removes them from the
// online users map, and broadcasts the offline event.
socket.on("disconnect", () => {
for (const [roomId, participants] of rooms) {
const idx = participants.findIndex(p => p.socketId === socket.id)
@@ -238,6 +300,7 @@ io.on("connection", (socket) => {
})
})
// ── Start ─────────────────────────────────────────────────────────
httpServer.listen(PORT, () => {
console.log(`[signaling] server running on port ${PORT}`)
})