![]() Server : Apache/2 System : Linux server-15-235-50-60 5.15.0-164-generic #174-Ubuntu SMP Fri Nov 14 20:25:16 UTC 2025 x86_64 User : gositeme ( 1004) PHP Version : 8.2.29 Disable Function : exec,system,passthru,shell_exec,proc_close,proc_open,dl,popen,show_source,posix_kill,posix_mkfifo,posix_getpwuid,posix_setpgid,posix_setsid,posix_setuid,posix_setgid,posix_seteuid,posix_setegid,posix_uname Directory : /home/gositeme/.cursor-server/data/User/History/-25e5d8ec/ |
import { NextApiRequest, NextApiResponse } from 'next';
import { Server as NetServer } from 'http';
import { Server as SocketIOServer } from 'socket.io';
import { NextApiResponseServerIO } from '@/types/socket';
import { getSession } from 'next-auth/react';
import { prisma } from '@/lib/prisma';
interface CaseChatMessage {
id: string;
caseId: string;
content: string;
senderId: string;
senderName: string;
senderAvatar?: string;
senderRole: string;
timestamp: number;
isPublic: boolean;
reactions?: {
[key: string]: string[];
};
}
interface CaseTypingData {
caseId: string;
userId: string;
userName: string;
isTyping: boolean;
timestamp: number;
}
interface CaseChatUser {
userId: string;
userName: string;
userAvatar?: string;
userRole: string;
joinedAt: number;
}
interface ProfileInteraction {
type: 'follow' | 'unfollow' | 'friend_request' | 'friend_accept' | 'endorse' | 'message' | 'profile_view';
fromUserId: string;
fromUserName: string;
fromUserAvatar?: string;
toUserId: string;
timestamp: number;
data?: any; // Additional data like endorsement text, message content, etc.
}
interface Notification {
id: string;
type: string;
title: string;
message: string;
userId: string;
data?: any;
isRead: boolean;
createdAt: number;
}
// Store active case chat rooms
const caseChatRooms = new Map<string, Set<string>>(); // caseId -> Set of userIds
const caseTypingUsers = new Map<string, Map<string, CaseTypingData>>(); // caseId -> Map of userId -> typing data
// Store user connections for profile interactions
const userConnections = new Map<string, string>(); // userId -> socketId
const onlineUsers = new Set<string>(); // Set of online user IDs
// Helper function to create notifications in the main notification system
const createNotification = async (userId: string, type: string, message: string, data?: any) => {
try {
await prisma.notification.create({
data: {
userId,
type,
title: getNotificationTitle(type),
message,
data: data ? JSON.stringify(data) : null,
isRead: false
}
});
} catch (error) {
console.error('Error creating notification:', error);
}
};
const getNotificationTitle = (type: string) => {
switch (type) {
case 'follow': return 'New Follower';
case 'friend_request': return 'Friend Request';
case 'endorsement': return 'New Endorsement';
case 'message': return 'New Message';
case 'profile_view': return 'Profile Viewed';
default: return 'Notification';
}
};
export const config = {
api: {
bodyParser: false,
},
};
const ioHandler = async (req: NextApiRequest, res: NextApiResponseServerIO) => {
if (!res.socket.server.io) {
console.log('Setting up WebSocket server...');
const httpServer: NetServer = res.socket.server as any;
const io = new SocketIOServer(httpServer, {
path: '/api/_ws',
addTrailingSlash: false,
cors: {
origin: process.env.NEXTAUTH_URL || 'http://localhost:3000',
methods: ['GET', 'POST'],
},
});
// Middleware to authenticate WebSocket connections
io.use(async (socket, next) => {
try {
const session = await getSession({ req: socket.request as any });
if (!session?.user?.id) {
return next(new Error('Unauthorized'));
}
// Attach user data to socket
socket.data.user = session.user;
next();
} catch (error) {
next(new Error('Authentication failed'));
}
});
io.on('connection', (socket) => {
const userId = socket.data.user.id;
const userName = socket.data.user.name;
console.log(`User ${userName} connected to WebSocket`);
// Track user connection
userConnections.set(userId, socket.id);
onlineUsers.add(userId);
// Broadcast user online status
socket.broadcast.emit('USER_STATUS_UPDATE', {
userId,
status: 'online',
timestamp: Date.now()
});
// Handle case chat joining
socket.on('JOIN_CASE_CHAT', async (data: { caseId: string }) => {
try {
const { caseId } = data;
const userId = socket.data.user.id;
const userName = socket.data.user.name;
const userAvatar = socket.data.user.image;
const userRole = socket.data.user.role || 'USER';
// Verify case exists and is public
const caseData = await prisma.legalCase.findUnique({
where: { id: caseId },
select: { id: true, isPublic: true, status: true }
});
if (!caseData || !caseData.isPublic) {
socket.emit('error', { message: 'Case not found or not public' });
return;
}
// Join the case chat room
socket.join(`case_${caseId}`);
// Add user to case chat room tracking
if (!caseChatRooms.has(caseId)) {
caseChatRooms.set(caseId, new Set());
}
caseChatRooms.get(caseId)!.add(userId);
// Notify other users in the case chat
socket.to(`case_${caseId}`).emit('CASE_USER_JOINED', {
caseId,
userId,
userName,
userAvatar,
userRole,
timestamp: Date.now()
});
// Send acknowledgment
socket.emit('CASE_CHAT_JOINED', {
caseId,
userId,
timestamp: Date.now()
});
console.log(`User ${userName} joined case chat: ${caseId}`);
} catch (error) {
console.error('Error joining case chat:', error);
socket.emit('error', { message: 'Failed to join case chat' });
}
});
// Handle case chat leaving
socket.on('LEAVE_CASE_CHAT', async (data: { caseId: string }) => {
try {
const { caseId } = data;
const userId = socket.data.user.id;
const userName = socket.data.user.name;
// Leave the case chat room
socket.leave(`case_${caseId}`);
// Remove user from case chat room tracking
const roomUsers = caseChatRooms.get(caseId);
if (roomUsers) {
roomUsers.delete(userId);
if (roomUsers.size === 0) {
caseChatRooms.delete(caseId);
}
}
// Remove typing indicator
const caseTyping = caseTypingUsers.get(caseId);
if (caseTyping) {
caseTyping.delete(userId);
if (caseTyping.size === 0) {
caseTypingUsers.delete(caseId);
}
}
// Notify other users in the case chat
socket.to(`case_${caseId}`).emit('CASE_USER_LEFT', {
caseId,
userId,
userName,
timestamp: Date.now()
});
// Send acknowledgment
socket.emit('CASE_CHAT_LEFT', {
caseId,
userId,
timestamp: Date.now()
});
console.log(`User ${userName} left case chat: ${caseId}`);
} catch (error) {
console.error('Error leaving case chat:', error);
socket.emit('error', { message: 'Failed to leave case chat' });
}
});
// Handle case chat messages
socket.on('CASE_MESSAGE', async (data: CaseChatMessage) => {
try {
const {
caseId,
content,
isPublic
} = data;
const userId = socket.data.user.id;
const userName = socket.data.user.name;
const userAvatar = socket.data.user.image;
const userRole = socket.data.user.role || 'USER';
// Verify case exists and is public
const caseData = await prisma.legalCase.findUnique({
where: { id: caseId },
select: { id: true, isPublic: true, status: true }
});
if (!caseData || !caseData.isPublic) {
socket.emit('error', { message: 'Case not found or not public' });
return;
}
// Create message object
const message: CaseChatMessage = {
id: `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
caseId,
content: content.trim(),
senderId: userId,
senderName: userName,
senderAvatar: userAvatar,
senderRole: userRole,
timestamp: Date.now(),
isPublic,
reactions: {}
};
// Save message to database if it's public
if (isPublic) {
try {
await prisma.caseChatMessage.create({
data: {
caseId,
content: message.content,
senderId: userId,
senderName: userName,
senderAvatar: userAvatar,
senderRole: userRole,
isPublic: true
}
});
} catch (dbError) {
console.error('Error saving case chat message to database:', dbError);
// Continue even if database save fails
}
}
// Broadcast message to all users in the case chat
io.to(`case_${caseId}`).emit('CASE_MESSAGE', {
type: 'CASE_MESSAGE',
data: message
});
console.log(`Case chat message sent by ${userName} in case ${caseId}: ${content.substring(0, 50)}...`);
} catch (error) {
console.error('Error sending case chat message:', error);
socket.emit('error', { message: 'Failed to send message' });
}
});
// Handle case typing indicators
socket.on('CASE_TYPING', (data: CaseTypingData) => {
try {
const { caseId, isTyping } = data;
const userId = socket.data.user.id;
const userName = socket.data.user.name;
// Update typing state
if (!caseTypingUsers.has(caseId)) {
caseTypingUsers.set(caseId, new Map());
}
const caseTyping = caseTypingUsers.get(caseId)!;
if (isTyping) {
caseTyping.set(userId, {
...data,
userId,
userName
});
} else {
caseTyping.delete(userId);
}
// Broadcast typing indicator to other users in the case chat
socket.to(`case_${caseId}`).emit('CASE_TYPING', {
type: 'CASE_TYPING',
data: {
caseId,
userId,
userName,
isTyping,
timestamp: Date.now()
}
});
} catch (error) {
console.error('Error handling case typing indicator:', error);
}
});
// Handle regular room messages (existing functionality)
socket.on('JOIN_ROOM', async (data: { chatRoomId: string }) => {
try {
const { chatRoomId } = data;
const userId = socket.data.user.id;
// Verify user has access to this chat room
const chatRoom = await prisma.chatRoom.findFirst({
where: {
id: chatRoomId,
participants: {
some: {
userId: userId
}
}
}
});
if (!chatRoom) {
socket.emit('error', { message: 'Access denied to chat room' });
return;
}
socket.join(chatRoomId);
socket.emit('ROOM_JOINED', { chatRoomId });
} catch (error) {
console.error('Error joining room:', error);
socket.emit('error', { message: 'Failed to join room' });
}
});
socket.on('LEAVE_ROOM', (data: { chatRoomId: string }) => {
const { chatRoomId } = data;
socket.leave(chatRoomId);
socket.emit('ROOM_LEFT', { chatRoomId });
});
socket.on('TYPING', (data: { roomId: string; isTyping: boolean }) => {
const { roomId, isTyping } = data;
socket.to(roomId).emit('TYPING', {
roomId,
userId: socket.data.user.id,
userName: socket.data.user.name,
isTyping,
timestamp: Date.now()
});
});
socket.on('PRESENCE_UPDATE', (data: { status: 'online' | 'away' }) => {
// Broadcast presence update to all connected clients
socket.broadcast.emit('PRESENCE_UPDATE', {
userId: socket.data.user.id,
status: data.status,
timestamp: Date.now()
});
});
// Handle disconnection
socket.on('disconnect', () => {
const userId = socket.data.user.id;
const userName = socket.data.user.name;
console.log(`User ${userName} disconnected from WebSocket`);
// Remove user from tracking
userConnections.delete(userId);
onlineUsers.delete(userId);
// Broadcast user offline status
socket.broadcast.emit('USER_STATUS_UPDATE', {
userId,
status: 'offline',
timestamp: Date.now()
});
// Remove user from all case chat rooms
caseChatRooms.forEach((users, caseId) => {
if (users.has(userId)) {
users.delete(userId);
// Notify other users
socket.to(`case_${caseId}`).emit('CASE_USER_LEFT', {
caseId,
userId,
userName,
timestamp: Date.now()
});
if (users.size === 0) {
caseChatRooms.delete(caseId);
}
}
});
// Remove typing indicators
caseTypingUsers.forEach((users, caseId) => {
if (users.has(userId)) {
users.delete(userId);
// Notify other users
socket.to(`case_${caseId}`).emit('CASE_TYPING', {
type: 'CASE_TYPING',
data: {
caseId,
userId,
userName,
isTyping: false,
timestamp: Date.now()
}
});
if (users.size === 0) {
caseTypingUsers.delete(caseId);
}
}
});
});
// Handle ping/pong for latency measurement
socket.on('ping', () => {
socket.emit('pong');
});
// Profile interaction handlers
socket.on('PROFILE_INTERACTION', async (data: ProfileInteraction) => {
try {
const { type, toUserId, data: interactionData } = data;
const fromUserId = socket.data.user.id;
const fromUserName = socket.data.user.name;
const fromUserAvatar = socket.data.user.image;
// Create interaction object
const interaction: ProfileInteraction = {
type,
fromUserId,
fromUserName,
fromUserAvatar,
toUserId,
timestamp: Date.now(),
data: interactionData
};
// Save interaction to database and create notification
try {
switch (type) {
case 'follow':
// Handle follow logic
await createNotification(toUserId, 'follow', `${fromUserName} started following you`, {
fromUserId,
fromUserName,
fromUserAvatar
});
break;
case 'friend_request':
// Handle friend request logic
await createNotification(toUserId, 'friend_request', `${fromUserName} sent you a friend request`, {
fromUserId,
fromUserName,
fromUserAvatar
});
break;
case 'endorse':
// Handle endorsement logic
const endorsementText = interactionData?.text || 'endorsed your profile';
await createNotification(toUserId, 'endorsement', `${fromUserName} ${endorsementText}`, {
fromUserId,
fromUserName,
fromUserAvatar,
endorsementText
});
break;
case 'profile_view':
// Handle profile view tracking (don't create notification for views)
break;
}
} catch (dbError) {
console.error('Error saving profile interaction to database:', dbError);
}
// Send notification to target user if they're online
const targetSocketId = userConnections.get(toUserId);
if (targetSocketId) {
io.to(targetSocketId).emit('PROFILE_INTERACTION', {
type: 'PROFILE_INTERACTION',
data: interaction
});
}
// Broadcast to all users following the target user (for activity feeds)
socket.broadcast.emit('PROFILE_ACTIVITY', {
type: 'PROFILE_ACTIVITY',
data: interaction
});
console.log(`Profile interaction: ${fromUserName} ${type} ${toUserId}`);
} catch (error) {
console.error('Error handling profile interaction:', error);
socket.emit('error', { message: 'Failed to process profile interaction' });
}
});
// Handle online status updates
socket.on('ONLINE_STATUS', (data: { status: 'online' | 'away' | 'offline' }) => {
const userId = socket.data.user.id;
const { status } = data;
if (status === 'online') {
onlineUsers.add(userId);
} else if (status === 'offline') {
onlineUsers.delete(userId);
}
// Broadcast status update to all connected users
socket.broadcast.emit('USER_STATUS_UPDATE', {
userId,
status,
timestamp: Date.now()
});
});
// Handle profile view tracking
socket.on('PROFILE_VIEW', async (data: { targetUserId: string }) => {
try {
const { targetUserId } = data;
const viewerId = socket.data.user.id;
const viewerName = socket.data.user.name;
// Don't track self-views
if (viewerId === targetUserId) return;
// Create profile view interaction
const interaction: ProfileInteraction = {
type: 'profile_view',
fromUserId: viewerId,
fromUserName: viewerName,
toUserId: targetUserId,
timestamp: Date.now()
};
// Send to target user if they're online
const targetSocketId = userConnections.get(targetUserId);
if (targetSocketId) {
io.to(targetSocketId).emit('PROFILE_VIEWED', {
type: 'PROFILE_VIEWED',
data: {
viewerId,
viewerName,
timestamp: Date.now()
}
});
}
console.log(`Profile view: ${viewerName} viewed ${targetUserId}`);
} catch (error) {
console.error('Error handling profile view:', error);
}
});
// Send initial connection confirmation
socket.emit('connected', {
userId: socket.data.user.id,
userName: socket.data.user.name,
timestamp: Date.now()
});
});
res.socket.server.io = io;
}
res.end();
};
export default ioHandler;