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.
- package/dist/index.js +69 -73
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +69 -73
- package/dist/index.mjs.map +1 -1
- package/dist/server-renderer.d.ts +5 -1
- package/package.json +18 -3
- package/templates/ssr-template/package-lock.json +3544 -0
- package/templates/ssr-template/package.json +3 -16
- package/templates/ssr-template/public/index.html +29 -8
- package/templates/ssr-template/readme.md +63 -0
- package/templates/ssr-template/server.js +577 -0
- package/templates/ssr-template/server.ts +111 -34
- package/templates/ssr-template/src/client.ts +29 -0
- package/templates/ssr-template/vite.config.js +9 -12
@@ -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();
|