frontend-hamroun 1.2.22 → 1.2.24

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,577 @@
1
+ import { server } from 'frontend-hamroun';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ // Get __dirname equivalent in ESM
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+
9
+ async function startServer() {
10
+ try {
11
+ // Import server components dynamically
12
+ const { Server, AuthService, Database, renderToString } = await server.getServer();
13
+
14
+ // Create the server instance
15
+ const app = new Server({
16
+ port: process.env.PORT ? parseInt(process.env.PORT) : 3000,
17
+ apiDir: './api',
18
+ staticDir: './public',
19
+
20
+ // Enable CORS for API endpoints
21
+ enableCors: true,
22
+
23
+ // Setup authentication
24
+ auth: {
25
+ secret: process.env.JWT_SECRET || 'your-development-secret-key',
26
+ expiresIn: '7d'
27
+ },
28
+
29
+ // Optional database configuration
30
+ /*
31
+ db: {
32
+ url: process.env.DATABASE_URL || 'mongodb://localhost:27017/my_app',
33
+ type: 'mongodb'
34
+ }
35
+ */
36
+ });
37
+
38
+ // Get the Express app instance to add custom routes
39
+ const expressApp = app.getExpressApp();
40
+
41
+ // Get the auth service
42
+ const auth = app.getAuth();
43
+
44
+ // Add authentication routes
45
+ expressApp.post('/api/auth/login', (req, res) => {
46
+ // Mock users for demonstration
47
+ const users = [
48
+ { id: 1, username: 'admin', password: '$2a$10$eBMQ3PFvX0G5a.U/zOA8Y.3JHZ5Ktq0hTjh4n2mjvXcOt6CvTxAZ2', roles: ['admin'] }, // password: admin123
49
+ { id: 2, username: 'user', password: '$2a$10$h6y7r0WSRO.BT9WBnTJj1udTI5OtMBdHcQ3/xNW7UQ1WXzMpYvNHe', roles: ['user'] } // password: user123
50
+ ];
51
+
52
+ const { username, password } = req.body;
53
+
54
+ if (!username || !password) {
55
+ return res.status(400).json({ message: 'Username and password are required' });
56
+ }
57
+
58
+ // Find user
59
+ const user = users.find(u => u.username === username);
60
+
61
+ if (!user) {
62
+ return res.status(401).json({ message: 'Invalid credentials' });
63
+ }
64
+
65
+ // Authenticate user
66
+ auth.comparePasswords(password, user.password)
67
+ .then(isMatch => {
68
+ if (!isMatch) {
69
+ return res.status(401).json({ message: 'Invalid credentials' });
70
+ }
71
+
72
+ // Generate token
73
+ const token = auth.generateToken({
74
+ id: user.id,
75
+ username: user.username,
76
+ roles: user.roles
77
+ });
78
+
79
+ // Return token
80
+ res.json({
81
+ token,
82
+ user: {
83
+ id: user.id,
84
+ username: user.username,
85
+ roles: user.roles
86
+ }
87
+ });
88
+ })
89
+ .catch(err => {
90
+ console.error('Auth error:', err);
91
+ res.status(500).json({ message: 'Authentication error' });
92
+ });
93
+ });
94
+
95
+ // Protected API route example
96
+ expressApp.get('/api/protected', auth.requireAuth(), (req, res) => {
97
+ res.json({
98
+ message: 'This is protected data',
99
+ user: req.user,
100
+ timestamp: new Date().toISOString()
101
+ });
102
+ });
103
+
104
+ // Admin-only route example
105
+ expressApp.get('/api/admin', auth.requireRoles(['admin']), (req, res) => {
106
+ res.json({
107
+ message: 'Admin-only data',
108
+ user: req.user,
109
+ timestamp: new Date().toISOString()
110
+ });
111
+ });
112
+
113
+ // Override the default route handler with our SSR implementation
114
+ expressApp.get('/', async (req, res) => {
115
+ try {
116
+ // Create a virtual DOM tree with more complex structure
117
+ const vnode = {
118
+ type: 'div',
119
+ props: {
120
+ id: 'app',
121
+ children: [
122
+ {
123
+ type: 'header',
124
+ props: {
125
+ className: 'header',
126
+ children: [
127
+ {
128
+ type: 'h1',
129
+ props: { children: 'Frontend Hamroun Full-Stack App' }
130
+ },
131
+ {
132
+ type: 'nav',
133
+ props: {
134
+ children: [
135
+ { type: 'a', props: { href: '/', children: 'Home' } },
136
+ { type: 'span', props: { children: ' | ' } },
137
+ { type: 'a', props: { href: '/about', children: 'About' } },
138
+ { type: 'span', props: { children: ' | ' } },
139
+ { type: 'a', props: { href: '#login', id: 'login-link', children: 'Login' } }
140
+ ]
141
+ }
142
+ }
143
+ ]
144
+ }
145
+ },
146
+ {
147
+ type: 'main',
148
+ props: {
149
+ children: [
150
+ {
151
+ type: 'section',
152
+ props: {
153
+ className: 'hero',
154
+ children: [
155
+ {
156
+ type: 'h2',
157
+ props: { children: 'Server-Side Rendering with Authentication' }
158
+ },
159
+ {
160
+ type: 'p',
161
+ props: { children: `This page was rendered on the server at ${new Date().toISOString()}` }
162
+ }
163
+ ]
164
+ }
165
+ },
166
+ {
167
+ type: 'section',
168
+ props: {
169
+ id: 'login-form',
170
+ className: 'login-form hidden',
171
+ children: [
172
+ {
173
+ type: 'h3',
174
+ props: { children: 'Login' }
175
+ },
176
+ {
177
+ type: 'div',
178
+ props: {
179
+ className: 'form-group',
180
+ children: [
181
+ {
182
+ type: 'label',
183
+ props: { htmlFor: 'username', children: 'Username:' }
184
+ },
185
+ {
186
+ type: 'input',
187
+ props: { id: 'username', type: 'text', placeholder: 'admin or user' }
188
+ }
189
+ ]
190
+ }
191
+ },
192
+ {
193
+ type: 'div',
194
+ props: {
195
+ className: 'form-group',
196
+ children: [
197
+ {
198
+ type: 'label',
199
+ props: { htmlFor: 'password', children: 'Password:' }
200
+ },
201
+ {
202
+ type: 'input',
203
+ props: { id: 'password', type: 'password', placeholder: 'admin123 or user123' }
204
+ }
205
+ ]
206
+ }
207
+ },
208
+ {
209
+ type: 'button',
210
+ props: { id: 'login-button', className: 'btn', children: 'Login' }
211
+ },
212
+ {
213
+ type: 'div',
214
+ props: { id: 'login-message', className: 'message' }
215
+ }
216
+ ]
217
+ }
218
+ },
219
+ {
220
+ type: 'section',
221
+ props: {
222
+ id: 'protected-content',
223
+ className: 'protected-content hidden',
224
+ children: [
225
+ {
226
+ type: 'h3',
227
+ props: { children: 'Protected Content' }
228
+ },
229
+ {
230
+ type: 'div',
231
+ props: { id: 'protected-data', className: 'data-container' }
232
+ },
233
+ {
234
+ type: 'button',
235
+ props: { id: 'load-protected', className: 'btn', children: 'Load Protected Data' }
236
+ }
237
+ ]
238
+ }
239
+ },
240
+ {
241
+ type: 'section',
242
+ props: {
243
+ id: 'admin-content',
244
+ className: 'admin-content hidden',
245
+ children: [
246
+ {
247
+ type: 'h3',
248
+ props: { children: 'Admin Content' }
249
+ },
250
+ {
251
+ type: 'div',
252
+ props: { id: 'admin-data', className: 'data-container' }
253
+ },
254
+ {
255
+ type: 'button',
256
+ props: { id: 'load-admin', className: 'btn', children: 'Load Admin Data' }
257
+ }
258
+ ]
259
+ }
260
+ }
261
+ ]
262
+ }
263
+ },
264
+ {
265
+ type: 'footer',
266
+ props: {
267
+ children: [
268
+ {
269
+ type: 'p',
270
+ props: { children: '© 2023 Frontend Hamroun' }
271
+ }
272
+ ]
273
+ }
274
+ }
275
+ ]
276
+ }
277
+ };
278
+
279
+ // Generate HTML from our virtual node
280
+ const content = await renderToString(vnode);
281
+
282
+ // Send complete HTML document
283
+ res.send(`
284
+ <!DOCTYPE html>
285
+ <html>
286
+ <head>
287
+ <meta charset="UTF-8">
288
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
289
+ <title>Frontend Hamroun Full-Stack</title>
290
+ <style>
291
+ body {
292
+ font-family: -apple-system, BlinkMacSystemFont, sans-serif;
293
+ line-height: 1.6;
294
+ color: #333;
295
+ max-width: 1200px;
296
+ margin: 0 auto;
297
+ padding: 0 1rem;
298
+ }
299
+ .header {
300
+ display: flex;
301
+ justify-content: space-between;
302
+ align-items: center;
303
+ padding: 1rem 0;
304
+ border-bottom: 1px solid #eee;
305
+ }
306
+ .hero {
307
+ padding: 2rem 0;
308
+ }
309
+ .btn {
310
+ background-color: #4CAF50;
311
+ border: none;
312
+ color: white;
313
+ padding: 10px 20px;
314
+ cursor: pointer;
315
+ border-radius: 4px;
316
+ margin-top: 1rem;
317
+ }
318
+ .hidden {
319
+ display: none;
320
+ }
321
+ .form-group {
322
+ margin-bottom: 1rem;
323
+ }
324
+ .form-group label {
325
+ display: block;
326
+ margin-bottom: 0.5rem;
327
+ }
328
+ .form-group input {
329
+ padding: 0.5rem;
330
+ width: 100%;
331
+ max-width: 300px;
332
+ border: 1px solid #ddd;
333
+ border-radius: 4px;
334
+ }
335
+ .message {
336
+ margin-top: 1rem;
337
+ padding: 0.5rem;
338
+ }
339
+ .success {
340
+ color: green;
341
+ background-color: #e8f5e9;
342
+ }
343
+ .error {
344
+ color: red;
345
+ background-color: #ffebee;
346
+ }
347
+ .data-container {
348
+ background-color: #f5f5f5;
349
+ padding: 1rem;
350
+ border-radius: 4px;
351
+ margin: 1rem 0;
352
+ min-height: 100px;
353
+ }
354
+ footer {
355
+ margin-top: 2rem;
356
+ padding: 1rem 0;
357
+ border-top: 1px solid #eee;
358
+ text-align: center;
359
+ }
360
+ </style>
361
+ <script>
362
+ // Client-side JavaScript for interactivity
363
+ document.addEventListener('DOMContentLoaded', () => {
364
+ let token = localStorage.getItem('token');
365
+ const user = JSON.parse(localStorage.getItem('user') || 'null');
366
+
367
+ // Elements
368
+ const loginLink = document.getElementById('login-link');
369
+ const loginForm = document.getElementById('login-form');
370
+ const loginButton = document.getElementById('login-button');
371
+ const loginMessage = document.getElementById('login-message');
372
+ const usernameInput = document.getElementById('username');
373
+ const passwordInput = document.getElementById('password');
374
+ const protectedContent = document.getElementById('protected-content');
375
+ const adminContent = document.getElementById('admin-content');
376
+ const loadProtectedBtn = document.getElementById('load-protected');
377
+ const loadAdminBtn = document.getElementById('load-admin');
378
+ const protectedData = document.getElementById('protected-data');
379
+ const adminData = document.getElementById('admin-data');
380
+
381
+ // Check if user is logged in
382
+ if (token && user) {
383
+ loginLink.textContent = \`Logout (\${user.username})\`;
384
+ protectedContent.classList.remove('hidden');
385
+
386
+ // Show admin content if user has admin role
387
+ if (user.roles && user.roles.includes('admin')) {
388
+ adminContent.classList.remove('hidden');
389
+ }
390
+ }
391
+
392
+ // Toggle login form
393
+ loginLink.addEventListener('click', (e) => {
394
+ e.preventDefault();
395
+
396
+ if (token) {
397
+ // Logout
398
+ localStorage.removeItem('token');
399
+ localStorage.removeItem('user');
400
+ token = null;
401
+ loginLink.textContent = 'Login';
402
+ protectedContent.classList.add('hidden');
403
+ adminContent.classList.add('hidden');
404
+ loginMessage.textContent = '';
405
+ loginMessage.className = 'message';
406
+ } else {
407
+ // Show login form
408
+ loginForm.classList.toggle('hidden');
409
+ }
410
+ });
411
+
412
+ // Login form submission
413
+ loginButton.addEventListener('click', async () => {
414
+ const username = usernameInput.value.trim();
415
+ const password = passwordInput.value.trim();
416
+
417
+ if (!username || !password) {
418
+ loginMessage.textContent = 'Please enter both username and password';
419
+ loginMessage.className = 'message error';
420
+ return;
421
+ }
422
+
423
+ try {
424
+ const response = await fetch('/api/auth/login', {
425
+ method: 'POST',
426
+ headers: {
427
+ 'Content-Type': 'application/json'
428
+ },
429
+ body: JSON.stringify({ username, password })
430
+ });
431
+
432
+ const data = await response.json();
433
+
434
+ if (response.ok) {
435
+ token = data.token;
436
+ localStorage.setItem('token', token);
437
+ localStorage.setItem('user', JSON.stringify(data.user));
438
+
439
+ loginMessage.textContent = 'Login successful';
440
+ loginMessage.className = 'message success';
441
+
442
+ // Update UI
443
+ loginLink.textContent = \`Logout (\${data.user.username})\`;
444
+ protectedContent.classList.remove('hidden');
445
+
446
+ // Show admin content if user has admin role
447
+ if (data.user.roles && data.user.roles.includes('admin')) {
448
+ adminContent.classList.remove('hidden');
449
+ }
450
+
451
+ // Clear form
452
+ usernameInput.value = '';
453
+ passwordInput.value = '';
454
+
455
+ // Hide login form after successful login
456
+ setTimeout(() => {
457
+ loginForm.classList.add('hidden');
458
+ }, 1500);
459
+ } else {
460
+ loginMessage.textContent = data.message || 'Login failed';
461
+ loginMessage.className = 'message error';
462
+ }
463
+ } catch (error) {
464
+ console.error('Login error:', error);
465
+ loginMessage.textContent = 'An error occurred while logging in';
466
+ loginMessage.className = 'message error';
467
+ }
468
+ });
469
+
470
+ // Load protected data
471
+ loadProtectedBtn.addEventListener('click', async () => {
472
+ if (!token) {
473
+ protectedData.textContent = 'You must be logged in to view this data';
474
+ return;
475
+ }
476
+
477
+ try {
478
+ const response = await fetch('/api/protected', {
479
+ headers: {
480
+ 'Authorization': \`Bearer \${token}\`
481
+ }
482
+ });
483
+
484
+ if (response.ok) {
485
+ const data = await response.json();
486
+ protectedData.textContent = JSON.stringify(data, null, 2);
487
+ } else {
488
+ protectedData.textContent = 'Failed to load protected data. Your session may have expired.';
489
+
490
+ if (response.status === 401) {
491
+ // Session expired, clear localStorage
492
+ localStorage.removeItem('token');
493
+ localStorage.removeItem('user');
494
+ token = null;
495
+ }
496
+ }
497
+ } catch (error) {
498
+ console.error('API error:', error);
499
+ protectedData.textContent = 'An error occurred while fetching data';
500
+ }
501
+ });
502
+
503
+ // Load admin data
504
+ loadAdminBtn.addEventListener('click', async () => {
505
+ if (!token) {
506
+ adminData.textContent = 'You must be logged in to view this data';
507
+ return;
508
+ }
509
+
510
+ try {
511
+ const response = await fetch('/api/admin', {
512
+ headers: {
513
+ 'Authorization': \`Bearer \${token}\`
514
+ }
515
+ });
516
+
517
+ if (response.ok) {
518
+ const data = await response.json();
519
+ adminData.textContent = JSON.stringify(data, null, 2);
520
+ } else if (response.status === 403) {
521
+ adminData.textContent = 'You do not have permission to access this data. Admin role required.';
522
+ } else {
523
+ adminData.textContent = 'Failed to load admin data. Your session may have expired.';
524
+ }
525
+ } catch (error) {
526
+ console.error('API error:', error);
527
+ adminData.textContent = 'An error occurred while fetching data';
528
+ }
529
+ });
530
+ });
531
+ </script>
532
+ </head>
533
+ <body>${content}</body>
534
+ </html>
535
+ `);
536
+ } catch (error) {
537
+ console.error('SSR Error:', error);
538
+ res.status(500).send(`
539
+ <!DOCTYPE html>
540
+ <html>
541
+ <head>
542
+ <title>Server Error</title>
543
+ <style>body { font-family: sans-serif; padding: 2rem; }</style>
544
+ </head>
545
+ <body>
546
+ <h1>Server Error</h1>
547
+ <p>An error occurred while rendering the page.</p>
548
+ <pre>${error.stack}</pre>
549
+ </body>
550
+ </html>
551
+ `);
552
+ }
553
+ });
554
+
555
+ // Start the server
556
+ await app.start();
557
+
558
+ console.log(`Server running at http://localhost:${app.config.port}`);
559
+ console.log('Available routes:');
560
+ console.log('- / (Home page with SSR)');
561
+ console.log('- /api/auth/login (POST: Login endpoint)');
562
+ console.log('- /api/protected (GET: Protected data, requires authentication)');
563
+ console.log('- /api/admin (GET: Admin data, requires admin role)');
564
+
565
+ // Handle graceful shutdown
566
+ process.on('SIGINT', async () => {
567
+ console.log('Shutting down server...');
568
+ await app.stop();
569
+ process.exit(0);
570
+ });
571
+ } catch (error) {
572
+ console.error('Failed to start server:', error);
573
+ process.exit(1);
574
+ }
575
+ }
576
+
577
+ startServer();