create-elit 3.2.7 → 3.2.8
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/dist/templates/README.md +23 -0
- package/dist/templates/elit.config.ts +59 -0
- package/dist/templates/package.json +14 -0
- package/dist/templates/public/favicon.svg +22 -0
- package/dist/templates/public/index.html +14 -0
- package/dist/templates/src/client.ts +15 -0
- package/dist/templates/src/components/Footer.ts +20 -0
- package/dist/templates/src/components/Header.ts +70 -0
- package/dist/templates/src/components/index.ts +2 -0
- package/dist/templates/src/main.ts +22 -0
- package/dist/templates/src/pages/ChatListPage.ts +144 -0
- package/dist/templates/src/pages/ChatPage.ts +186 -0
- package/dist/templates/src/pages/ForgotPasswordPage.ts +110 -0
- package/dist/templates/src/pages/HomePage.ts +166 -0
- package/dist/templates/src/pages/LoginPage.ts +182 -0
- package/dist/templates/src/pages/PrivateChatPage.ts +268 -0
- package/dist/templates/src/pages/ProfilePage.ts +342 -0
- package/dist/templates/src/pages/RegisterPage.ts +230 -0
- package/dist/templates/src/router.ts +30 -0
- package/dist/templates/src/server.ts +595 -0
- package/dist/templates/src/styles.ts +1181 -0
- package/dist/templates/tsconfig.json +23 -0
- package/package.json +2 -3
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
import { ElitRequest, ElitResponse, ServerRouter } from 'elit/server';
|
|
2
|
+
import { Database } from 'elit/database';
|
|
3
|
+
import { resolve } from 'path';
|
|
4
|
+
import { scrypt, randomBytes, timingSafeEqual } from 'crypto';
|
|
5
|
+
import { promisify } from 'util';
|
|
6
|
+
|
|
7
|
+
const scryptAsync = promisify(scrypt);
|
|
8
|
+
|
|
9
|
+
export const router = new ServerRouter();
|
|
10
|
+
|
|
11
|
+
// Store SSE clients for each room
|
|
12
|
+
const sseClients = new Map<string, Set<any>>();
|
|
13
|
+
|
|
14
|
+
// Helper to broadcast to all clients in a room
|
|
15
|
+
function broadcastToRoom(roomId: string, data: any) {
|
|
16
|
+
const clients = sseClients.get(roomId);
|
|
17
|
+
if (clients) {
|
|
18
|
+
clients.forEach(client => {
|
|
19
|
+
try {
|
|
20
|
+
client.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
21
|
+
} catch (err) {
|
|
22
|
+
// Remove dead client
|
|
23
|
+
clients.delete(client);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Initialize database with configuration
|
|
30
|
+
const db = new Database({
|
|
31
|
+
dir: resolve(process.cwd(), 'databases'),
|
|
32
|
+
language: 'ts'
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Helper function to hash password
|
|
36
|
+
async function hashPassword(password: string): Promise<string> {
|
|
37
|
+
const salt = randomBytes(16).toString('hex');
|
|
38
|
+
const derivedKey = await scryptAsync(password, salt, 64) as Buffer;
|
|
39
|
+
return `${salt}.${derivedKey.toString('hex')}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Helper function to verify password
|
|
43
|
+
async function verifyPassword(storedHash: string, suppliedPassword: string): Promise<boolean> {
|
|
44
|
+
const [salt, key] = storedHash.split('.');
|
|
45
|
+
const derivedKey = await scryptAsync(suppliedPassword, salt, 64) as Buffer;
|
|
46
|
+
const keyBuffer = Buffer.from(key, 'hex');
|
|
47
|
+
return timingSafeEqual(derivedKey, keyBuffer);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Helper to execute database code
|
|
51
|
+
// async function executeDb(code: string): Promise<any> {
|
|
52
|
+
// const result = await db.execute(code);
|
|
53
|
+
// return result.namespace;
|
|
54
|
+
// }
|
|
55
|
+
|
|
56
|
+
// GET /api/hello
|
|
57
|
+
router.get('/api/hello', async (req: ElitRequest, res: ElitResponse) => {
|
|
58
|
+
res.setHeader('Content-Type', 'text/html; charset=UTF-8');
|
|
59
|
+
res.send("Hello from Elit ServerRouter!");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// POST /api/auth/register
|
|
63
|
+
router.post('/api/auth/register', async (req: ElitRequest, res: ElitResponse) => {
|
|
64
|
+
const { name, email, password } = req.body;
|
|
65
|
+
|
|
66
|
+
if (!name || !email || !password) {
|
|
67
|
+
return res.status(400).json({ error: 'Please provide name, email, and password' });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!email.includes('@')) {
|
|
71
|
+
return res.status(400).json({ error: 'Please provide a valid email' });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (password.length < 6) {
|
|
75
|
+
return res.status(400).json({ error: 'Password must be at least 6 characters' });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
// Check if email already exists
|
|
80
|
+
const checkEmailCode = `
|
|
81
|
+
import { users } from '@db/users';
|
|
82
|
+
const email = ${JSON.stringify(email)};
|
|
83
|
+
const existingUser = users.find(u => u.email === email);
|
|
84
|
+
if (existingUser) {
|
|
85
|
+
console.log('EMAIL_EXISTS');
|
|
86
|
+
} else {
|
|
87
|
+
console.log('EMAIL_AVAILABLE');
|
|
88
|
+
}
|
|
89
|
+
`;
|
|
90
|
+
|
|
91
|
+
const checkResult = await db.execute(checkEmailCode);
|
|
92
|
+
const emailStatus = checkResult.logs[0]?.args?.[0];
|
|
93
|
+
|
|
94
|
+
if (emailStatus === 'EMAIL_EXISTS') {
|
|
95
|
+
return res.status(409).json({ error: 'Email already registered' });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Hash password before storing
|
|
99
|
+
const hashedPassword = await hashPassword(password);
|
|
100
|
+
|
|
101
|
+
const userId = 'user_' + Date.now();
|
|
102
|
+
const userData = JSON.stringify({
|
|
103
|
+
id: userId,
|
|
104
|
+
name,
|
|
105
|
+
email,
|
|
106
|
+
password: hashedPassword,
|
|
107
|
+
bio: 'New user',
|
|
108
|
+
location: '',
|
|
109
|
+
website: '',
|
|
110
|
+
avatar: '',
|
|
111
|
+
stats: {
|
|
112
|
+
projects: 0,
|
|
113
|
+
followers: 0,
|
|
114
|
+
following: 0,
|
|
115
|
+
stars: 0
|
|
116
|
+
},
|
|
117
|
+
createdAt: new Date().toISOString()
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Use a simpler approach without dynamic imports inside the function
|
|
121
|
+
const code = `
|
|
122
|
+
import { users } from '@db/users';
|
|
123
|
+
const user = ${userData};
|
|
124
|
+
users.push(user);
|
|
125
|
+
update('users', 'users', users);
|
|
126
|
+
console.log(user);
|
|
127
|
+
`;
|
|
128
|
+
|
|
129
|
+
const result = await db.execute(code);
|
|
130
|
+
|
|
131
|
+
console.log('Registration result:', result);
|
|
132
|
+
|
|
133
|
+
// if (!result.logs || result.logs.length === 0) {
|
|
134
|
+
// throw new Error('Failed to create user');
|
|
135
|
+
// }
|
|
136
|
+
|
|
137
|
+
// Extract the user from the first log entry's args
|
|
138
|
+
const user = result.logs[0]?.args?.[0];
|
|
139
|
+
|
|
140
|
+
// Don't send password in response
|
|
141
|
+
const { password: _, ...userWithoutPassword } = user;
|
|
142
|
+
|
|
143
|
+
const token = Buffer.from(`${user.id}:${Date.now()}`).toString('base64');
|
|
144
|
+
|
|
145
|
+
res.status(201).json({
|
|
146
|
+
message: 'User registered successfully',
|
|
147
|
+
token: token,
|
|
148
|
+
user: userWithoutPassword
|
|
149
|
+
});
|
|
150
|
+
} catch (error: any) {
|
|
151
|
+
return res.status(500).json({ error: error?.message || 'Internal server error' });
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// POST /api/auth/login
|
|
156
|
+
router.post('/api/auth/login', async (req: ElitRequest, res: ElitResponse) => {
|
|
157
|
+
const { email, password } = req.body;
|
|
158
|
+
|
|
159
|
+
if (!email || !password) {
|
|
160
|
+
return res.status(400).json({ error: 'Please provide email and password' });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
// First, find user by email
|
|
165
|
+
const findUserCode = `
|
|
166
|
+
import { users } from './users';
|
|
167
|
+
const email = ${JSON.stringify(email)};
|
|
168
|
+
const user = users.find(u => u.email === email);
|
|
169
|
+
if (user) {
|
|
170
|
+
console.log(JSON.stringify(user));
|
|
171
|
+
} else {
|
|
172
|
+
console.error('USER_NOT_FOUND');
|
|
173
|
+
}
|
|
174
|
+
`;
|
|
175
|
+
|
|
176
|
+
const findResult = await db.execute(findUserCode);
|
|
177
|
+
|
|
178
|
+
if (!findResult.logs || findResult.logs.length === 0) {
|
|
179
|
+
return res.status(401).json({ error: 'Invalid email or password' });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const userLog = findResult.logs[0]?.args?.[0];
|
|
183
|
+
|
|
184
|
+
if (userLog === 'USER_NOT_FOUND') {
|
|
185
|
+
return res.status(401).json({ error: 'Invalid email or password' });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const user = typeof userLog === 'string' ? JSON.parse(userLog) : userLog;
|
|
189
|
+
|
|
190
|
+
// Verify password
|
|
191
|
+
const isValidPassword = await verifyPassword(user.password, password);
|
|
192
|
+
|
|
193
|
+
if (!isValidPassword) {
|
|
194
|
+
return res.status(401).json({ error: 'Invalid email or password' });
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Don't send password in response
|
|
198
|
+
const { password: _, ...userWithoutPassword } = user;
|
|
199
|
+
|
|
200
|
+
const token = Buffer.from(`${user.id}:${Date.now()}`).toString('base64');
|
|
201
|
+
|
|
202
|
+
res.json({
|
|
203
|
+
message: 'Login successful',
|
|
204
|
+
token: token,
|
|
205
|
+
user: userWithoutPassword
|
|
206
|
+
});
|
|
207
|
+
} catch (error: any) {
|
|
208
|
+
if (error.message === 'Invalid email or password') {
|
|
209
|
+
return res.status(401).json({ error: error.message });
|
|
210
|
+
}
|
|
211
|
+
return res.status(500).json({ error: 'Internal server error' });
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// POST /api/auth/forgot-password
|
|
216
|
+
router.post('/api/auth/forgot-password', async (req: ElitRequest, res: ElitResponse) => {
|
|
217
|
+
const { email } = req.body;
|
|
218
|
+
|
|
219
|
+
if (!email) {
|
|
220
|
+
return res.status(400).json({ error: 'Please provide email' });
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
res.json({
|
|
224
|
+
message: 'If an account exists with this email, a password reset link has been sent'
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Helper function to verify token and get user ID
|
|
229
|
+
function verifyToken(token: string): string | null {
|
|
230
|
+
try {
|
|
231
|
+
const decoded = Buffer.from(token, 'base64').toString('utf-8');
|
|
232
|
+
const [userId] = decoded.split(':');
|
|
233
|
+
return userId || null;
|
|
234
|
+
} catch {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// GET /api/profile
|
|
240
|
+
router.get('/api/profile', async (req: ElitRequest, res: ElitResponse) => {
|
|
241
|
+
const authHeader = req.headers.authorization;
|
|
242
|
+
|
|
243
|
+
if (!authHeader) {
|
|
244
|
+
return res.status(401).json({ error: 'Unauthorized - No token provided' });
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Handle both string and string[] cases
|
|
248
|
+
const token = Array.isArray(authHeader) ? authHeader[0] : authHeader;
|
|
249
|
+
|
|
250
|
+
if (!token.startsWith('Bearer ')) {
|
|
251
|
+
return res.status(401).json({ error: 'Unauthorized - Invalid token format' });
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const tokenValue = token.substring(7); // Remove 'Bearer ' prefix
|
|
255
|
+
const userId = verifyToken(tokenValue);
|
|
256
|
+
|
|
257
|
+
if (!userId) {
|
|
258
|
+
return res.status(401).json({ error: 'Unauthorized - Invalid token' });
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
const code = `
|
|
263
|
+
import { users } from './users';
|
|
264
|
+
const userId = ${JSON.stringify(userId)};
|
|
265
|
+
const user = users.find(u => u.id === userId);
|
|
266
|
+
if (user) {
|
|
267
|
+
console.log(JSON.stringify(user));
|
|
268
|
+
} else {
|
|
269
|
+
console.error('USER_NOT_FOUND');
|
|
270
|
+
}
|
|
271
|
+
`;
|
|
272
|
+
|
|
273
|
+
const result = await db.execute(code);
|
|
274
|
+
|
|
275
|
+
if (!result.logs || result.logs.length === 0) {
|
|
276
|
+
return res.status(404).json({ error: 'User not found' });
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const userLog = result.logs[0]?.args?.[0];
|
|
280
|
+
|
|
281
|
+
if (userLog === 'USER_NOT_FOUND') {
|
|
282
|
+
return res.status(404).json({ error: 'User not found' });
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const user = typeof userLog === 'string' ? JSON.parse(userLog) : userLog;
|
|
286
|
+
|
|
287
|
+
// Don't send password in response
|
|
288
|
+
const { password: _, ...userWithoutPassword } = user;
|
|
289
|
+
|
|
290
|
+
res.json({ user: userWithoutPassword });
|
|
291
|
+
} catch (error: any) {
|
|
292
|
+
return res.status(500).json({ error: error.message || 'Internal server error' });
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// PUT /api/profile
|
|
297
|
+
router.put('/api/profile', async (req: ElitRequest, res: ElitResponse) => {
|
|
298
|
+
const { name, bio, location, website } = req.body;
|
|
299
|
+
|
|
300
|
+
const authHeader = req.headers.authorization;
|
|
301
|
+
|
|
302
|
+
if (!authHeader) {
|
|
303
|
+
return res.status(401).json({ error: 'Unauthorized - No token provided' });
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Handle both string and string[] cases
|
|
307
|
+
const token = Array.isArray(authHeader) ? authHeader[0] : authHeader;
|
|
308
|
+
|
|
309
|
+
if (!token.startsWith('Bearer ')) {
|
|
310
|
+
return res.status(401).json({ error: 'Unauthorized - Invalid token format' });
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const tokenValue = token.substring(7); // Remove 'Bearer ' prefix
|
|
314
|
+
const userId = verifyToken(tokenValue);
|
|
315
|
+
|
|
316
|
+
if (!userId) {
|
|
317
|
+
return res.status(401).json({ error: 'Unauthorized - Invalid token' });
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
try {
|
|
321
|
+
const code = `
|
|
322
|
+
import { users } from './users';
|
|
323
|
+
const userId = ${JSON.stringify(userId)};
|
|
324
|
+
const updates = ${JSON.stringify({ name, bio, location, website })};
|
|
325
|
+
const userIndex = users.findIndex(u => u.id === userId);
|
|
326
|
+
|
|
327
|
+
if (userIndex === -1) {
|
|
328
|
+
console.error('USER_NOT_FOUND');
|
|
329
|
+
} else {
|
|
330
|
+
const user = users[userIndex];
|
|
331
|
+
if (updates.name) user.name = updates.name;
|
|
332
|
+
if (updates.bio) user.bio = updates.bio;
|
|
333
|
+
if (updates.location) user.location = updates.location;
|
|
334
|
+
if (updates.website) user.website = updates.website;
|
|
335
|
+
update('users', 'users', users);
|
|
336
|
+
console.log(JSON.stringify(user));
|
|
337
|
+
}
|
|
338
|
+
`;
|
|
339
|
+
|
|
340
|
+
const result = await db.execute(code);
|
|
341
|
+
|
|
342
|
+
if (!result.logs || result.logs.length === 0) {
|
|
343
|
+
return res.status(404).json({ error: 'User not found' });
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const userLog = result.logs[0]?.args?.[0];
|
|
347
|
+
|
|
348
|
+
if (userLog === 'USER_NOT_FOUND') {
|
|
349
|
+
return res.status(404).json({ error: 'User not found' });
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const user = typeof userLog === 'string' ? JSON.parse(userLog) : userLog;
|
|
353
|
+
|
|
354
|
+
// Don't send password in response
|
|
355
|
+
const { password: _, ...userWithoutPassword } = user;
|
|
356
|
+
|
|
357
|
+
res.json({
|
|
358
|
+
message: 'Profile updated successfully',
|
|
359
|
+
user: userWithoutPassword
|
|
360
|
+
});
|
|
361
|
+
} catch (error: any) {
|
|
362
|
+
return res.status(500).json({ error: error.message || 'Internal server error' });
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// GET /api/users
|
|
367
|
+
router.get('/api/users', async (_req: ElitRequest, res: ElitResponse) => {
|
|
368
|
+
try {
|
|
369
|
+
const code = `
|
|
370
|
+
import { users } from './users';
|
|
371
|
+
// Remove passwords from users before returning
|
|
372
|
+
const usersWithoutPasswords = users.map(({ password, ...user }) => user);
|
|
373
|
+
console.log(JSON.stringify(usersWithoutPasswords));
|
|
374
|
+
`;
|
|
375
|
+
|
|
376
|
+
const result = await db.execute(code);
|
|
377
|
+
|
|
378
|
+
// Extract the user list from the first log entry's args and parse if string
|
|
379
|
+
const userLog = result.logs && result.logs.length > 0 ? result.logs[0]?.args?.[0] : [];
|
|
380
|
+
const userList = typeof userLog === 'string' ? JSON.parse(userLog) : userLog;
|
|
381
|
+
res.json({ users: userList, count: Array.isArray(userList) ? userList.length : 0 });
|
|
382
|
+
} catch (error: any) {
|
|
383
|
+
res.json({ users: [], count: 0 });
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
// GET /api/users/:id
|
|
388
|
+
router.get('/api/users/:id', async (req: ElitRequest, res: ElitResponse) => {
|
|
389
|
+
const url = req.url || '';
|
|
390
|
+
const userId = url.split('/').pop();
|
|
391
|
+
|
|
392
|
+
if (!userId) {
|
|
393
|
+
return res.status(400).json({ error: 'User ID required' });
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
try {
|
|
397
|
+
const code = `
|
|
398
|
+
import { users } from './users';
|
|
399
|
+
const userId = ${JSON.stringify(userId)};
|
|
400
|
+
const user = users.find(u => u.id === userId);
|
|
401
|
+
if (user) {
|
|
402
|
+
// Remove password before sending
|
|
403
|
+
const { password, ...userWithoutPassword } = user;
|
|
404
|
+
console.log(JSON.stringify(userWithoutPassword));
|
|
405
|
+
} else {
|
|
406
|
+
console.error('USER_NOT_FOUND');
|
|
407
|
+
}
|
|
408
|
+
`;
|
|
409
|
+
|
|
410
|
+
const result = await db.execute(code);
|
|
411
|
+
|
|
412
|
+
if (!result.logs || result.logs.length === 0) {
|
|
413
|
+
return res.status(404).json({ error: 'User not found' });
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const userLog = result.logs[0]?.args?.[0];
|
|
417
|
+
|
|
418
|
+
if (userLog === 'USER_NOT_FOUND') {
|
|
419
|
+
return res.status(404).json({ error: 'User not found' });
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const user = typeof userLog === 'string' ? JSON.parse(userLog) : userLog;
|
|
423
|
+
res.json({ user });
|
|
424
|
+
} catch (error: any) {
|
|
425
|
+
if (error.message === 'User not found') {
|
|
426
|
+
return res.status(404).json({ error: error.message });
|
|
427
|
+
}
|
|
428
|
+
return res.status(500).json({ error: 'Internal server error' });
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
// ===== Chat API with SharedState =====
|
|
433
|
+
|
|
434
|
+
// In-memory chat messages storage (for demo)
|
|
435
|
+
const chatMessages = new Map<string, Array<{
|
|
436
|
+
id: string;
|
|
437
|
+
roomId: string;
|
|
438
|
+
userId: string;
|
|
439
|
+
userName: string;
|
|
440
|
+
text: string;
|
|
441
|
+
timestamp: number;
|
|
442
|
+
}>>();
|
|
443
|
+
|
|
444
|
+
// GET /api/chat/messages - Get messages for a room (roomId from query string)
|
|
445
|
+
router.get('/api/chat/messages', async (req: ElitRequest, res: ElitResponse) => {
|
|
446
|
+
// Extract roomId from query string or use 'general' as default
|
|
447
|
+
const url = new URL(req.url || '', `http://${req.headers.host || 'localhost'}`);
|
|
448
|
+
const roomId = url.searchParams.get('roomId') || 'general';
|
|
449
|
+
const authHeader = req.headers.authorization;
|
|
450
|
+
|
|
451
|
+
if (!authHeader) {
|
|
452
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const token = Array.isArray(authHeader) ? authHeader[0] : authHeader;
|
|
456
|
+
if (!token.startsWith('Bearer ')) {
|
|
457
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const userId = verifyToken(token.substring(7));
|
|
461
|
+
if (!userId) {
|
|
462
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
try {
|
|
466
|
+
const messages = chatMessages.get(roomId) || [];
|
|
467
|
+
res.json({ messages, roomId });
|
|
468
|
+
} catch (error: any) {
|
|
469
|
+
res.status(500).json({ error: 'Failed to fetch messages' });
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
// POST /api/chat/send - Send a message
|
|
474
|
+
router.post('/api/chat/send', async (req: ElitRequest, res: ElitResponse) => {
|
|
475
|
+
const { roomId = 'general', message } = req.body;
|
|
476
|
+
const authHeader = req.headers.authorization;
|
|
477
|
+
|
|
478
|
+
if (!authHeader) {
|
|
479
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const token = Array.isArray(authHeader) ? authHeader[0] : authHeader;
|
|
483
|
+
if (!token.startsWith('Bearer ')) {
|
|
484
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const userId = verifyToken(token.substring(7));
|
|
488
|
+
if (!userId) {
|
|
489
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (!message || typeof message !== 'string' || message.trim().length === 0) {
|
|
493
|
+
return res.status(400).json({ error: 'Message is required' });
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
try {
|
|
497
|
+
// Get user info
|
|
498
|
+
const findUserCode = `
|
|
499
|
+
import { users } from './users';
|
|
500
|
+
const userId = ${JSON.stringify(userId)};
|
|
501
|
+
const user = users.find(u => u.id === userId);
|
|
502
|
+
if (user) {
|
|
503
|
+
console.log(JSON.stringify(user));
|
|
504
|
+
} else {
|
|
505
|
+
console.error('USER_NOT_FOUND');
|
|
506
|
+
}
|
|
507
|
+
`;
|
|
508
|
+
|
|
509
|
+
const findResult = await db.execute(findUserCode);
|
|
510
|
+
const userLog = findResult.logs[0]?.args?.[0];
|
|
511
|
+
|
|
512
|
+
if (userLog === 'USER_NOT_FOUND') {
|
|
513
|
+
return res.status(404).json({ error: 'User not found' });
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const user = typeof userLog === 'string' ? JSON.parse(userLog) : userLog;
|
|
517
|
+
|
|
518
|
+
// Create new message
|
|
519
|
+
const newMessage = {
|
|
520
|
+
id: `msg_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`,
|
|
521
|
+
roomId,
|
|
522
|
+
userId: user.id,
|
|
523
|
+
userName: user.name,
|
|
524
|
+
text: message.trim(),
|
|
525
|
+
timestamp: Date.now()
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
// Get existing messages and add new one
|
|
529
|
+
const messages = chatMessages.get(roomId) || [];
|
|
530
|
+
messages.push(newMessage);
|
|
531
|
+
|
|
532
|
+
// Keep only last 100 messages
|
|
533
|
+
if (messages.length > 100) {
|
|
534
|
+
messages.shift();
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
chatMessages.set(roomId, messages);
|
|
538
|
+
|
|
539
|
+
// Broadcast to all connected clients in this room via SSE
|
|
540
|
+
broadcastToRoom(roomId, {
|
|
541
|
+
type: 'new-message',
|
|
542
|
+
data: newMessage
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
res.json({
|
|
546
|
+
success: true,
|
|
547
|
+
message: newMessage
|
|
548
|
+
});
|
|
549
|
+
} catch (error: any) {
|
|
550
|
+
console.error('Error sending message:', error);
|
|
551
|
+
res.status(500).json({ error: 'Failed to send message' });
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
// GET /api/chat/events - SSE endpoint for real-time messages
|
|
556
|
+
router.get('/api/chat/events', async (req: ElitRequest, res: ElitResponse) => {
|
|
557
|
+
// Parse roomId from URL
|
|
558
|
+
const url = new URL(req.url || '', `http://${req.headers.host}`);
|
|
559
|
+
const roomId = url.searchParams.get('roomId') || 'general';
|
|
560
|
+
|
|
561
|
+
// Set SSE headers
|
|
562
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
563
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
564
|
+
res.setHeader('Connection', 'keep-alive');
|
|
565
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
566
|
+
|
|
567
|
+
// Create client set for this room if not exists
|
|
568
|
+
if (!sseClients.has(roomId)) {
|
|
569
|
+
sseClients.set(roomId, new Set());
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const clients = sseClients.get(roomId)!;
|
|
573
|
+
clients.add(res);
|
|
574
|
+
|
|
575
|
+
// Send initial connection message
|
|
576
|
+
res.write(`data: ${JSON.stringify({ type: 'connected', roomId })}\n\n`);
|
|
577
|
+
|
|
578
|
+
// Send heartbeat every 30 seconds to keep connection alive
|
|
579
|
+
const heartbeat = setInterval(() => {
|
|
580
|
+
try {
|
|
581
|
+
res.write(`: heartbeat\n\n`);
|
|
582
|
+
} catch (err) {
|
|
583
|
+
clearInterval(heartbeat);
|
|
584
|
+
clients.delete(res);
|
|
585
|
+
}
|
|
586
|
+
}, 30000);
|
|
587
|
+
|
|
588
|
+
// Remove client on disconnect
|
|
589
|
+
req.on('close', () => {
|
|
590
|
+
clearInterval(heartbeat);
|
|
591
|
+
clients.delete(res);
|
|
592
|
+
});
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
export const server = router;
|