offbyt 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/cli/index.js +2 -0
- package/cli.js +206 -0
- package/core/detector/detectAxios.js +107 -0
- package/core/detector/detectFetch.js +148 -0
- package/core/detector/detectForms.js +55 -0
- package/core/detector/detectSocket.js +341 -0
- package/core/generator/generateControllers.js +17 -0
- package/core/generator/generateModels.js +25 -0
- package/core/generator/generateRoutes.js +17 -0
- package/core/generator/generateServer.js +18 -0
- package/core/generator/generateSocket.js +160 -0
- package/core/index.js +14 -0
- package/core/ir/IRTypes.js +25 -0
- package/core/ir/buildIR.js +83 -0
- package/core/parser/parseJS.js +26 -0
- package/core/parser/parseTS.js +27 -0
- package/core/rules/relationRules.js +38 -0
- package/core/rules/resourceRules.js +32 -0
- package/core/rules/schemaInference.js +26 -0
- package/core/scanner/scanProject.js +58 -0
- package/deploy/cloudflare.js +41 -0
- package/deploy/cloudflareWorker.js +122 -0
- package/deploy/connect.js +198 -0
- package/deploy/flyio.js +51 -0
- package/deploy/index.js +322 -0
- package/deploy/netlify.js +29 -0
- package/deploy/railway.js +215 -0
- package/deploy/render.js +195 -0
- package/deploy/utils.js +383 -0
- package/deploy/vercel.js +29 -0
- package/index.js +18 -0
- package/lib/generator/advancedCrudGenerator.js +475 -0
- package/lib/generator/crudCodeGenerator.js +486 -0
- package/lib/generator/irBasedGenerator.js +360 -0
- package/lib/ir-builder/index.js +16 -0
- package/lib/ir-builder/irBuilder.js +330 -0
- package/lib/ir-builder/rulesEngine.js +353 -0
- package/lib/ir-builder/templateEngine.js +193 -0
- package/lib/ir-builder/templates/index.js +14 -0
- package/lib/ir-builder/templates/model.template.js +47 -0
- package/lib/ir-builder/templates/routes-generic.template.js +66 -0
- package/lib/ir-builder/templates/routes-user.template.js +105 -0
- package/lib/ir-builder/templates/routes.template.js +102 -0
- package/lib/ir-builder/templates/validation.template.js +15 -0
- package/lib/ir-integration.js +349 -0
- package/lib/modes/benchmark.js +162 -0
- package/lib/modes/configBasedGenerator.js +2258 -0
- package/lib/modes/connect.js +1125 -0
- package/lib/modes/doctorAi.js +172 -0
- package/lib/modes/generateApi.js +435 -0
- package/lib/modes/interactiveSetup.js +548 -0
- package/lib/modes/offline.clean.js +14 -0
- package/lib/modes/offline.enhanced.js +787 -0
- package/lib/modes/offline.js +295 -0
- package/lib/modes/offline.v2.js +13 -0
- package/lib/modes/sync.js +629 -0
- package/lib/scanner/apiEndpointExtractor.js +387 -0
- package/lib/scanner/authPatternDetector.js +54 -0
- package/lib/scanner/frontendScanner.js +642 -0
- package/lib/utils/apiClientGenerator.js +242 -0
- package/lib/utils/apiScanner.js +95 -0
- package/lib/utils/codeInjector.js +350 -0
- package/lib/utils/doctor.js +381 -0
- package/lib/utils/envGenerator.js +36 -0
- package/lib/utils/loadTester.js +61 -0
- package/lib/utils/performanceAnalyzer.js +298 -0
- package/lib/utils/resourceDetector.js +281 -0
- package/package.json +20 -0
- package/templates/.env.template +31 -0
- package/templates/advanced.model.template.js +201 -0
- package/templates/advanced.route.template.js +341 -0
- package/templates/auth.middleware.template.js +87 -0
- package/templates/auth.routes.template.js +238 -0
- package/templates/auth.user.model.template.js +78 -0
- package/templates/cache.middleware.js +34 -0
- package/templates/chat.models.template.js +260 -0
- package/templates/chat.routes.template.js +478 -0
- package/templates/compression.middleware.js +19 -0
- package/templates/database.config.js +74 -0
- package/templates/errorHandler.middleware.js +54 -0
- package/templates/express/controller.ejs +26 -0
- package/templates/express/model.ejs +9 -0
- package/templates/express/route.ejs +18 -0
- package/templates/express/server.ejs +16 -0
- package/templates/frontend.env.template +14 -0
- package/templates/model.template.js +86 -0
- package/templates/package.production.json +51 -0
- package/templates/package.template.json +41 -0
- package/templates/pagination.utility.js +110 -0
- package/templates/production.server.template.js +233 -0
- package/templates/rateLimiter.middleware.js +36 -0
- package/templates/requestLogger.middleware.js +19 -0
- package/templates/response.helper.js +179 -0
- package/templates/route.template.js +130 -0
- package/templates/security.middleware.js +78 -0
- package/templates/server.template.js +91 -0
- package/templates/socket.server.template.js +433 -0
- package/templates/utils.helper.js +157 -0
- package/templates/validation.middleware.js +63 -0
- package/templates/validation.schema.js +128 -0
- package/utils/fileWriter.js +15 -0
- package/utils/logger.js +18 -0
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Socket.io Server Template
|
|
3
|
+
*
|
|
4
|
+
* Full-featured Socket.io server with:
|
|
5
|
+
* - Authentication middleware
|
|
6
|
+
* - Room/channel management
|
|
7
|
+
* - Presence tracking
|
|
8
|
+
* - Message history
|
|
9
|
+
* - Typing indicators
|
|
10
|
+
* - File sharing support
|
|
11
|
+
* - Rate limiting
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export const socketServerTemplate = `import { Server } from 'socket.io';
|
|
15
|
+
import jwt from 'jsonwebtoken';
|
|
16
|
+
import Message from '../models/Message.js';
|
|
17
|
+
import Conversation from '../models/Conversation.js';
|
|
18
|
+
import User from '../models/User.js';
|
|
19
|
+
|
|
20
|
+
// Ensure JWT_SECRET is consistent across all auth checks
|
|
21
|
+
const JWT_SECRET = process.env.JWT_SECRET || 'your_super_secret_jwt_key_change_this_in_production';
|
|
22
|
+
|
|
23
|
+
// Active users map (userId -> socketId)
|
|
24
|
+
const activeUsers = new Map();
|
|
25
|
+
|
|
26
|
+
// Typing indicators (roomId -> Set of userIds)
|
|
27
|
+
const typingUsers = new Map();
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Initialize Socket.io server
|
|
31
|
+
*/
|
|
32
|
+
export function initializeSocket(httpServer) {
|
|
33
|
+
const io = new Server(httpServer, {
|
|
34
|
+
cors: {
|
|
35
|
+
origin: process.env.CLIENT_URL || process.env.REACT_APP_API_URL,
|
|
36
|
+
credentials: true
|
|
37
|
+
},
|
|
38
|
+
pingTimeout: 60000,
|
|
39
|
+
pingInterval: 25000
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// ============ AUTHENTICATION MIDDLEWARE ============
|
|
43
|
+
io.use(async (socket, next) => {
|
|
44
|
+
try {
|
|
45
|
+
const token = socket.handshake.auth.token || socket.handshake.headers.authorization?.split(' ')[1];
|
|
46
|
+
|
|
47
|
+
console.log(\`\n🔐 SOCKET.IO AUTH CHECK\`);
|
|
48
|
+
console.log(\` Token received: \${token ? 'YES ✅' : 'NO ❌'}\`);
|
|
49
|
+
console.log(\` Token preview: \${token ? token.substring(0, 30) + '...' : 'N/A'}\`);
|
|
50
|
+
|
|
51
|
+
if (!token) {
|
|
52
|
+
console.log(\` ❌ Error: No token provided\`);
|
|
53
|
+
return next(new Error('Authentication required'));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let decoded;
|
|
57
|
+
try {
|
|
58
|
+
decoded = jwt.verify(token, JWT_SECRET);
|
|
59
|
+
console.log(\` ✅ JWT verified successfully\`);
|
|
60
|
+
console.log(\` User ID from token: \${decoded.id || decoded.userId}\`);
|
|
61
|
+
} catch (jwtError) {
|
|
62
|
+
console.log(\` ❌ JWT verification failed: \${jwtError.message}\`);
|
|
63
|
+
console.log(\` Error type: \${jwtError.name}\`);
|
|
64
|
+
throw jwtError;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const user = await User.findById(decoded.id || decoded.userId).select('-password');
|
|
68
|
+
|
|
69
|
+
if (!user) {
|
|
70
|
+
console.log(\` ❌ User not found in database\`);
|
|
71
|
+
return next(new Error('User not found'));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
console.log(\` ✅ User found: \${user.email}\`);
|
|
75
|
+
console.log(\` ✅ Socket authentication successful!\n\`);
|
|
76
|
+
|
|
77
|
+
socket.userId = user._id.toString();
|
|
78
|
+
socket.user = user;
|
|
79
|
+
next();
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.log(\` ❌ Authentication error: \${error.message}\n\`);
|
|
82
|
+
next(new Error(\`Socket auth failed: \${error.message}\`));
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// ============ CONNECTION HANDLER ============
|
|
87
|
+
io.on('connection', (socket) => {
|
|
88
|
+
console.log(\`\n✅ USER CONNECTED\`);
|
|
89
|
+
console.log(\` User ID: \${socket.userId}\`);
|
|
90
|
+
console.log(\` Socket ID: \${socket.id}\`);
|
|
91
|
+
console.log(\` User email: \${socket.user?.email}\`);
|
|
92
|
+
console.log(\` Total connected users: \${io.engine.clientsCount}\n\`);
|
|
93
|
+
|
|
94
|
+
// Register user as online
|
|
95
|
+
activeUsers.set(socket.userId, socket.id);
|
|
96
|
+
io.emit('user:online', { userId: socket.userId, socketId: socket.id });
|
|
97
|
+
|
|
98
|
+
// ============ JOIN CONVERSATION/ROOM ============
|
|
99
|
+
socket.on('conversation:join', async (data) => {
|
|
100
|
+
try {
|
|
101
|
+
const { conversationId } = data;
|
|
102
|
+
|
|
103
|
+
console.log(\`\n🚪 JOIN CONVERSATION REQUEST\`);
|
|
104
|
+
console.log(\` User ID: \${socket.userId}\`);
|
|
105
|
+
console.log(\` Socket ID: \${socket.id}\`);
|
|
106
|
+
console.log(\` Conversation ID: \${conversationId}\`);
|
|
107
|
+
|
|
108
|
+
// Verify user has access to conversation
|
|
109
|
+
const conversation = await Conversation.findOne({
|
|
110
|
+
_id: conversationId,
|
|
111
|
+
participants: socket.userId
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
if (!conversation) {
|
|
115
|
+
console.log(\` ❌ FAILED: User NOT found in conversation participants\`);
|
|
116
|
+
return socket.emit('error', { message: 'Conversation not found or access denied' });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
socket.join(conversationId);
|
|
120
|
+
const socketsInRoom = io.sockets.adapter.rooms.get(conversationId);
|
|
121
|
+
const participantCount = socketsInRoom?.size || 0;
|
|
122
|
+
|
|
123
|
+
console.log(\` ✅ SUCCESS: User joined room!\`);
|
|
124
|
+
console.log(\` 📊 Room "\${conversationId}" status:\`);
|
|
125
|
+
console.log(\` - Active socket connections: \${participantCount}\`);
|
|
126
|
+
console.log(\` - Conversation participants: \${conversation.participants.length}\`);
|
|
127
|
+
console.log(\` - Participant IDs: \${conversation.participants.join(', ')}\`);
|
|
128
|
+
|
|
129
|
+
// List all sockets in the room
|
|
130
|
+
const socketsInRoomArray = Array.from(socketsInRoom || []);
|
|
131
|
+
console.log(\` - Socket IDs in room: \${socketsInRoomArray.join(', ')}\`);
|
|
132
|
+
|
|
133
|
+
// Map socket IDs to user IDs
|
|
134
|
+
const usersInRoom = socketsInRoomArray.map(socketId => {
|
|
135
|
+
const s = io.sockets.sockets.get(socketId);
|
|
136
|
+
return s?.userId || 'unknown';
|
|
137
|
+
});
|
|
138
|
+
console.log(\` - User IDs in room: \${usersInRoom.join(', ')}\n\`);
|
|
139
|
+
|
|
140
|
+
socket.emit('conversation:joined', { conversationId });
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.error('❌ Join conversation error:', error);
|
|
143
|
+
socket.emit('error', { message: 'Failed to join conversation' });
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// ============ LEAVE CONVERSATION/ROOM ============
|
|
148
|
+
socket.on('conversation:leave', (data) => {
|
|
149
|
+
const { conversationId } = data;
|
|
150
|
+
socket.leave(conversationId);
|
|
151
|
+
console.log(\`User \${socket.userId} left conversation \${conversationId}\`);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// ============ SEND MESSAGE ============
|
|
155
|
+
socket.on('message:send', async (data) => {
|
|
156
|
+
try {
|
|
157
|
+
const { conversationId, content, type = 'text', metadata } = data;
|
|
158
|
+
|
|
159
|
+
console.log(\`\n📨 MESSAGE SEND REQUEST\`);
|
|
160
|
+
console.log(\` Sender: \${socket.userId}\`);
|
|
161
|
+
console.log(\` Conversation: \${conversationId}\`);
|
|
162
|
+
console.log(\` Content: "\${content}\"\`);
|
|
163
|
+
|
|
164
|
+
// Verify user has access
|
|
165
|
+
const conversation = await Conversation.findOne({
|
|
166
|
+
_id: conversationId,
|
|
167
|
+
participants: socket.userId
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
if (!conversation) {
|
|
171
|
+
console.log(\`❌ User \${socket.userId} is NOT a participant in conversation \${conversationId}\`);
|
|
172
|
+
return socket.emit('error', { message: 'Conversation not found' });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
console.log(\`✅ User \${socket.userId} is a participant. Participants: \${conversation.participants.join(', ')}\`);
|
|
176
|
+
|
|
177
|
+
// Create message
|
|
178
|
+
const message = await Message.create({
|
|
179
|
+
conversation: conversationId,
|
|
180
|
+
sender: socket.userId,
|
|
181
|
+
content,
|
|
182
|
+
type,
|
|
183
|
+
metadata,
|
|
184
|
+
timestamp: new Date()
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// Populate sender details
|
|
188
|
+
await message.populate('sender', 'name email avatar');
|
|
189
|
+
|
|
190
|
+
// Update conversation last message
|
|
191
|
+
conversation.lastMessage = message._id;
|
|
192
|
+
conversation.lastMessageAt = new Date();
|
|
193
|
+
await conversation.save();
|
|
194
|
+
|
|
195
|
+
// Check who is in the room
|
|
196
|
+
const socketsInRoom = io.sockets.adapter.rooms.get(conversationId);
|
|
197
|
+
const usersInRoom = socketsInRoom ? Array.from(socketsInRoom).map(socketId => {
|
|
198
|
+
const s = io.sockets.sockets.get(socketId);
|
|
199
|
+
return s?.userId || 'unknown';
|
|
200
|
+
}) : [];
|
|
201
|
+
|
|
202
|
+
console.log(\`📊 Broadcasting to room "\${conversationId}"\`);
|
|
203
|
+
console.log(\` Users in room: \${usersInRoom.length > 0 ? usersInRoom.join(', ') : 'NOBODY'}\`);
|
|
204
|
+
console.log(\` Expected participants: \${conversation.participants.join(', ')}\`);
|
|
205
|
+
|
|
206
|
+
if (usersInRoom.length === 0) {
|
|
207
|
+
console.log(\` ⚠️ WARNING: No users currently in room! Message will not be delivered in real-time\`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Emit to all users in conversation
|
|
211
|
+
io.to(conversationId).emit('message:new', {
|
|
212
|
+
message: message.toObject(),
|
|
213
|
+
conversationId
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
console.log(\`✅ Message broadcasted to room. Message ID: \${message._id}\n\`);
|
|
217
|
+
|
|
218
|
+
// Send delivery confirmation to sender
|
|
219
|
+
socket.emit('message:sent', {
|
|
220
|
+
tempId: data.tempId,
|
|
221
|
+
message: message.toObject()
|
|
222
|
+
});
|
|
223
|
+
} catch (error) {
|
|
224
|
+
console.error('❌ Send message error:', error);
|
|
225
|
+
socket.emit('error', { message: 'Failed to send message' });
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// ============ TYPING INDICATOR ============
|
|
230
|
+
socket.on('typing:start', (data) => {
|
|
231
|
+
const { conversationId } = data;
|
|
232
|
+
|
|
233
|
+
if (!typingUsers.has(conversationId)) {
|
|
234
|
+
typingUsers.set(conversationId, new Set());
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
typingUsers.get(conversationId).add(socket.userId);
|
|
238
|
+
|
|
239
|
+
// Broadcast to others in conversation
|
|
240
|
+
socket.to(conversationId).emit('typing:update', {
|
|
241
|
+
conversationId,
|
|
242
|
+
userId: socket.userId,
|
|
243
|
+
isTyping: true
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
socket.on('typing:stop', (data) => {
|
|
248
|
+
const { conversationId } = data;
|
|
249
|
+
|
|
250
|
+
if (typingUsers.has(conversationId)) {
|
|
251
|
+
typingUsers.get(conversationId).delete(socket.userId);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
socket.to(conversationId).emit('typing:update', {
|
|
255
|
+
conversationId,
|
|
256
|
+
userId: socket.userId,
|
|
257
|
+
isTyping: false
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// ============ MESSAGE READ/DELIVERY STATUS ============
|
|
262
|
+
socket.on('message:read', async (data) => {
|
|
263
|
+
try {
|
|
264
|
+
const { messageId, conversationId } = data;
|
|
265
|
+
|
|
266
|
+
const message = await Message.findById(messageId);
|
|
267
|
+
if (!message) {
|
|
268
|
+
return socket.emit('error', { message: 'Message not found' });
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Add to read receipts
|
|
272
|
+
if (!message.readBy.includes(socket.userId)) {
|
|
273
|
+
message.readBy.push(socket.userId);
|
|
274
|
+
message.readAt = new Date();
|
|
275
|
+
await message.save();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Notify sender
|
|
279
|
+
io.to(conversationId).emit('message:read:update', {
|
|
280
|
+
messageId,
|
|
281
|
+
userId: socket.userId,
|
|
282
|
+
readAt: message.readAt
|
|
283
|
+
});
|
|
284
|
+
} catch (error) {
|
|
285
|
+
console.error('Message read error:', error);
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// ============ GET CONVERSATION HISTORY ============
|
|
290
|
+
socket.on('messages:fetch', async (data) => {
|
|
291
|
+
try {
|
|
292
|
+
const { conversationId, limit = 50, before } = data;
|
|
293
|
+
|
|
294
|
+
const query = { conversation: conversationId };
|
|
295
|
+
if (before) {
|
|
296
|
+
query.timestamp = { $lt: new Date(before) };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const messages = await Message.find(query)
|
|
300
|
+
.sort({ timestamp: -1 })
|
|
301
|
+
.limit(limit)
|
|
302
|
+
.populate('sender', 'name email avatar')
|
|
303
|
+
.lean();
|
|
304
|
+
|
|
305
|
+
socket.emit('messages:history', {
|
|
306
|
+
conversationId,
|
|
307
|
+
messages: messages.reverse(),
|
|
308
|
+
hasMore: messages.length === limit
|
|
309
|
+
});
|
|
310
|
+
} catch (error) {
|
|
311
|
+
console.error('Fetch messages error:', error);
|
|
312
|
+
socket.emit('error', { message: 'Failed to fetch messages' });
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// ============ CREATE CONVERSATION ============
|
|
317
|
+
socket.on('conversation:create', async (data) => {
|
|
318
|
+
try {
|
|
319
|
+
const { participantIds, name, type = 'direct' } = data;
|
|
320
|
+
|
|
321
|
+
// Include current user
|
|
322
|
+
const participants = [socket.userId, ...participantIds];
|
|
323
|
+
|
|
324
|
+
// For direct chats, check if conversation already exists
|
|
325
|
+
if (type === 'direct' && participants.length === 2) {
|
|
326
|
+
const existing = await Conversation.findOne({
|
|
327
|
+
type: 'direct',
|
|
328
|
+
participants: { $all: participants, $size: 2 }
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
if (existing) {
|
|
332
|
+
return socket.emit('conversation:created', { conversation: existing });
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Create new conversation
|
|
337
|
+
const conversation = await Conversation.create({
|
|
338
|
+
name,
|
|
339
|
+
type,
|
|
340
|
+
participants,
|
|
341
|
+
createdBy: socket.userId
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
await conversation.populate('participants', 'name email avatar');
|
|
345
|
+
|
|
346
|
+
// Join all participants to the room
|
|
347
|
+
participants.forEach(participantId => {
|
|
348
|
+
const socketId = activeUsers.get(participantId);
|
|
349
|
+
if (socketId) {
|
|
350
|
+
io.sockets.sockets.get(socketId)?.join(conversation._id.toString());
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// Notify all participants
|
|
355
|
+
io.to(conversation._id.toString()).emit('conversation:new', {
|
|
356
|
+
conversation: conversation.toObject()
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
socket.emit('conversation:created', { conversation });
|
|
360
|
+
} catch (error) {
|
|
361
|
+
console.error('Create conversation error:', error);
|
|
362
|
+
socket.emit('error', { message: 'Failed to create conversation' });
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// ============ FILE UPLOAD (metadata only) ============
|
|
367
|
+
socket.on('file:upload', async (data) => {
|
|
368
|
+
try {
|
|
369
|
+
const { conversationId, fileName, fileSize, fileType, fileUrl } = data;
|
|
370
|
+
|
|
371
|
+
const message = await Message.create({
|
|
372
|
+
conversation: conversationId,
|
|
373
|
+
sender: socket.userId,
|
|
374
|
+
content: fileName,
|
|
375
|
+
type: 'file',
|
|
376
|
+
metadata: {
|
|
377
|
+
fileName,
|
|
378
|
+
fileSize,
|
|
379
|
+
fileType,
|
|
380
|
+
fileUrl
|
|
381
|
+
},
|
|
382
|
+
timestamp: new Date()
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
await message.populate('sender', 'name email avatar');
|
|
386
|
+
|
|
387
|
+
io.to(conversationId).emit('message:new', {
|
|
388
|
+
message: message.toObject(),
|
|
389
|
+
conversationId
|
|
390
|
+
});
|
|
391
|
+
} catch (error) {
|
|
392
|
+
console.error('File upload error:', error);
|
|
393
|
+
socket.emit('error', { message: 'Failed to process file' });
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
// ============ DISCONNECT HANDLER ============
|
|
398
|
+
socket.on('disconnect', () => {
|
|
399
|
+
console.log(\`❌ User disconnected: \${socket.userId} (\${socket.id})\`);
|
|
400
|
+
|
|
401
|
+
// Remove from active users
|
|
402
|
+
activeUsers.delete(socket.userId);
|
|
403
|
+
|
|
404
|
+
// Remove from typing indicators
|
|
405
|
+
typingUsers.forEach((users, conversationId) => {
|
|
406
|
+
if (users.has(socket.userId)) {
|
|
407
|
+
users.delete(socket.userId);
|
|
408
|
+
io.to(conversationId).emit('typing:update', {
|
|
409
|
+
conversationId,
|
|
410
|
+
userId: socket.userId,
|
|
411
|
+
isTyping: false
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
// Broadcast offline status
|
|
417
|
+
io.emit('user:offline', { userId: socket.userId });
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
// ============ ERROR HANDLER ============
|
|
421
|
+
socket.on('error', (error) => {
|
|
422
|
+
console.error('Socket error:', error);
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
console.log('✅ Socket.io initialized');
|
|
427
|
+
return io;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export default initializeSocket;
|
|
431
|
+
`;
|
|
432
|
+
|
|
433
|
+
export default socketServerTemplate;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response Utilities
|
|
3
|
+
* Standardized response formatting across the application
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export class ResponseHelper {
|
|
7
|
+
static success(res, data, message = 'Success', statusCode = 200) {
|
|
8
|
+
return res.status(statusCode).json({
|
|
9
|
+
success: true,
|
|
10
|
+
message,
|
|
11
|
+
data,
|
|
12
|
+
timestamp: new Date().toISOString()
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
static paginated(res, data, pagination, message = 'Success', statusCode = 200) {
|
|
17
|
+
return res.status(statusCode).json({
|
|
18
|
+
success: true,
|
|
19
|
+
message,
|
|
20
|
+
data,
|
|
21
|
+
pagination,
|
|
22
|
+
timestamp: new Date().toISOString()
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static error(res, message = 'Error', errors = [], statusCode = 500) {
|
|
27
|
+
return res.status(statusCode).json({
|
|
28
|
+
success: false,
|
|
29
|
+
message,
|
|
30
|
+
errors: Array.isArray(errors) ? errors : [errors],
|
|
31
|
+
timestamp: new Date().toISOString()
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
static validationError(res, errors) {
|
|
36
|
+
return res.status(400).json({
|
|
37
|
+
success: false,
|
|
38
|
+
message: 'Validation Error',
|
|
39
|
+
errors,
|
|
40
|
+
timestamp: new Date().toISOString()
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
static notFound(res, resource = 'Resource') {
|
|
45
|
+
return res.status(404).json({
|
|
46
|
+
success: false,
|
|
47
|
+
message: `${resource} not found`,
|
|
48
|
+
timestamp: new Date().toISOString()
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
static unauthorized(res) {
|
|
53
|
+
return res.status(401).json({
|
|
54
|
+
success: false,
|
|
55
|
+
message: 'Unauthorized access',
|
|
56
|
+
timestamp: new Date().toISOString()
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
static forbidden(res) {
|
|
61
|
+
return res.status(403).json({
|
|
62
|
+
success: false,
|
|
63
|
+
message: 'Forbidden',
|
|
64
|
+
timestamp: new Date().toISOString()
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Error Helper
|
|
71
|
+
* Custom error classes for better error handling
|
|
72
|
+
*/
|
|
73
|
+
export class AppError extends Error {
|
|
74
|
+
constructor(message, statusCode = 500) {
|
|
75
|
+
super(message);
|
|
76
|
+
this.statusCode = statusCode;
|
|
77
|
+
Error.captureStackTrace(this, this.constructor);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export class ValidationError extends AppError {
|
|
82
|
+
constructor(message = 'Validation Error', errors = []) {
|
|
83
|
+
super(message, 400);
|
|
84
|
+
this.errors = errors;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export class NotFoundError extends AppError {
|
|
89
|
+
constructor(resource = 'Resource') {
|
|
90
|
+
super(`${resource} not found`, 404);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export class UnauthorizedError extends AppError {
|
|
95
|
+
constructor(message = 'Unauthorized') {
|
|
96
|
+
super(message, 401);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export class ForbiddenError extends AppError {
|
|
101
|
+
constructor(message = 'Forbidden') {
|
|
102
|
+
super(message, 403);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export class ConflictError extends AppError {
|
|
107
|
+
constructor(message = 'Conflict') {
|
|
108
|
+
super(message, 409);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Logger Utility
|
|
114
|
+
* Structured logging with timestamps
|
|
115
|
+
*/
|
|
116
|
+
export class Logger {
|
|
117
|
+
static info(message, data = {}) {
|
|
118
|
+
console.log(JSON.stringify({
|
|
119
|
+
level: 'INFO',
|
|
120
|
+
message,
|
|
121
|
+
data,
|
|
122
|
+
timestamp: new Date().toISOString()
|
|
123
|
+
}));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
static error(message, error = {}) {
|
|
127
|
+
console.error(JSON.stringify({
|
|
128
|
+
level: 'ERROR',
|
|
129
|
+
message,
|
|
130
|
+
error: error.message || error,
|
|
131
|
+
stack: error.stack,
|
|
132
|
+
timestamp: new Date().toISOString()
|
|
133
|
+
}));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
static warn(message, data = {}) {
|
|
137
|
+
console.warn(JSON.stringify({
|
|
138
|
+
level: 'WARN',
|
|
139
|
+
message,
|
|
140
|
+
data,
|
|
141
|
+
timestamp: new Date().toISOString()
|
|
142
|
+
}));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
static debug(message, data = {}) {
|
|
146
|
+
if (process.env.NODE_ENV === 'development') {
|
|
147
|
+
console.debug(JSON.stringify({
|
|
148
|
+
level: 'DEBUG',
|
|
149
|
+
message,
|
|
150
|
+
data,
|
|
151
|
+
timestamp: new Date().toISOString()
|
|
152
|
+
}));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export default { ResponseHelper, AppError, Logger };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { validationResult } from 'express-validator';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Validation Error Middleware
|
|
5
|
+
* Checks for validation errors and returns them if found
|
|
6
|
+
*/
|
|
7
|
+
export const validateErrors = (req, res, next) => {
|
|
8
|
+
const errors = validationResult(req);
|
|
9
|
+
|
|
10
|
+
if (!errors.isEmpty()) {
|
|
11
|
+
return res.status(400).json({
|
|
12
|
+
success: false,
|
|
13
|
+
message: 'Validation failed',
|
|
14
|
+
errors: errors.array().map(err => ({
|
|
15
|
+
field: err.param || err.path,
|
|
16
|
+
message: err.msg,
|
|
17
|
+
value: err.value
|
|
18
|
+
}))
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
next();
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Custom Validation Helpers
|
|
27
|
+
*/
|
|
28
|
+
export const ValidationHelpers = {
|
|
29
|
+
/**
|
|
30
|
+
* Validate email format
|
|
31
|
+
*/
|
|
32
|
+
isValidEmail: (email) => {
|
|
33
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
34
|
+
return emailRegex.test(email);
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Validate phone format
|
|
39
|
+
*/
|
|
40
|
+
isValidPhone: (phone) => {
|
|
41
|
+
const phoneRegex = /^[\d\s\-\+\(\)]{10,}$/;
|
|
42
|
+
return phoneRegex.test(phone);
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Validate URL format
|
|
47
|
+
*/
|
|
48
|
+
isValidUrl: (url) => {
|
|
49
|
+
try {
|
|
50
|
+
new URL(url);
|
|
51
|
+
return true;
|
|
52
|
+
} catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Check if string length is within range
|
|
59
|
+
*/
|
|
60
|
+
isLengthBetween: (str, min, max) => {
|
|
61
|
+
return str && str.length >= min && str.length <= max;
|
|
62
|
+
}
|
|
63
|
+
};
|