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.
@@ -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;