ultimate-jekyll-manager 0.0.118 → 0.0.120
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/CLAUDE.md +409 -23
- package/README.md +171 -2
- package/TODO.md +10 -2
- package/_backup/form-manager.backup.js +1020 -0
- package/dist/assets/js/core/auth.js +5 -4
- package/dist/assets/js/core/cookieconsent.js +24 -17
- package/dist/assets/js/core/exit-popup.js +15 -12
- package/dist/assets/js/core/social-sharing.js +8 -4
- package/dist/assets/js/libs/auth/pages.js +78 -149
- package/dist/assets/js/libs/dev.js +192 -129
- package/dist/assets/js/libs/form-manager.js +643 -775
- package/dist/assets/js/pages/account/index.js +3 -2
- package/dist/assets/js/pages/account/sections/api-keys.js +37 -52
- package/dist/assets/js/pages/account/sections/connections.js +37 -46
- package/dist/assets/js/pages/account/sections/delete.js +57 -78
- package/dist/assets/js/pages/account/sections/profile.js +37 -56
- package/dist/assets/js/pages/account/sections/security.js +102 -125
- package/dist/assets/js/pages/admin/notifications/new/index.js +73 -151
- package/dist/assets/js/pages/blog/index.js +33 -53
- package/dist/assets/js/pages/contact/index.js +112 -173
- package/dist/assets/js/pages/download/index.js +39 -86
- package/dist/assets/js/pages/oauth2/index.js +17 -17
- package/dist/assets/js/pages/payment/checkout/index.js +23 -36
- package/dist/assets/js/pages/pricing/index.js +5 -2
- package/dist/assets/js/pages/test/libraries/form-manager/index.js +194 -0
- package/dist/assets/themes/classy/css/components/_cards.scss +2 -2
- package/dist/defaults/_.env +6 -0
- package/dist/defaults/_.gitignore +7 -1
- package/dist/defaults/dist/_includes/core/body.html +5 -13
- package/dist/defaults/dist/_includes/core/foot.html +1 -0
- package/dist/defaults/dist/_includes/themes/classy/frontend/sections/nav.html +51 -36
- package/dist/defaults/dist/_layouts/blueprint/admin/notifications/new.html +13 -2
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/about.html +84 -42
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/account/index.html +26 -21
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/auth/signin.html +2 -2
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/auth/signup.html +2 -2
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/blog/index.html +72 -58
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/blog/post.html +46 -29
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/contact.html +46 -53
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/download.html +111 -73
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/index.html +111 -56
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/pricing.html +127 -81
- package/dist/defaults/dist/pages/test/libraries/form-manager.html +181 -0
- package/dist/defaults/dist/pages/test/libraries/lazy-loading.html +1 -1
- package/dist/gulp/tasks/defaults.js +210 -1
- package/dist/gulp/tasks/serve.js +18 -0
- package/dist/lib/logger.js +1 -1
- package/firebase-debug.log +770 -0
- package/package.json +6 -6
- package/.playwright-mcp/page-2025-10-22T19-11-27-666Z.png +0 -0
- package/.playwright-mcp/page-2025-10-22T19-11-57-357Z.png +0 -0
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Security Section JavaScript
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Libraries
|
|
2
6
|
import authorizedFetch from '__main_assets__/js/libs/authorized-fetch.js';
|
|
3
7
|
import { FormManager } from '__main_assets__/js/libs/form-manager.js';
|
|
4
8
|
import { getPrerenderedIcon } from '__main_assets__/js/libs/prerendered-icons.js';
|
|
@@ -6,7 +10,11 @@ import { getPrerenderedIcon } from '__main_assets__/js/libs/prerendered-icons.js
|
|
|
6
10
|
let webManager = null;
|
|
7
11
|
let firebaseAuth = null;
|
|
8
12
|
let signinMethodForms = new Map(); // Store FormManager instances for signin methods
|
|
9
|
-
let
|
|
13
|
+
let signoutAllFormManager = null; // FormManager instance for sign out all sessions
|
|
14
|
+
|
|
15
|
+
// Check query string for popup parameter
|
|
16
|
+
const url = new URL(window.location.href);
|
|
17
|
+
const useAuthPopup = url.searchParams.get('authPopup') === 'true' || window !== window.top;
|
|
10
18
|
|
|
11
19
|
// Initialize security section
|
|
12
20
|
export function init(wm) {
|
|
@@ -17,7 +25,9 @@ export function init(wm) {
|
|
|
17
25
|
|
|
18
26
|
// Load security data
|
|
19
27
|
export function loadData(account) {
|
|
20
|
-
if (!account)
|
|
28
|
+
if (!account) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
21
31
|
|
|
22
32
|
console.log('[DEBUG] security.js - loadData() called with account:', account);
|
|
23
33
|
|
|
@@ -106,7 +116,7 @@ async function updateSigninMethods() {
|
|
|
106
116
|
$googleEmail: !!$googleEmail,
|
|
107
117
|
$googleForm: !!$googleForm,
|
|
108
118
|
$connectButton: !!$connectButton,
|
|
109
|
-
$disconnectButton: !!$disconnectButton
|
|
119
|
+
$disconnectButton: !!$disconnectButton,
|
|
110
120
|
});
|
|
111
121
|
|
|
112
122
|
if ($googleEmail && $connectButton && $disconnectButton) {
|
|
@@ -161,7 +171,9 @@ function update2FAStatus(twoFactorData) {
|
|
|
161
171
|
// Update active sessions
|
|
162
172
|
async function updateActiveSessions(account) {
|
|
163
173
|
const $sessionsList = document.getElementById('active-sessions-list');
|
|
164
|
-
if (!$sessionsList)
|
|
174
|
+
if (!$sessionsList) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
165
177
|
|
|
166
178
|
const sessions = [];
|
|
167
179
|
|
|
@@ -179,7 +191,7 @@ async function updateActiveSessions(account) {
|
|
|
179
191
|
region: account.activity.geolocation?.region,
|
|
180
192
|
country: account.activity.geolocation?.country,
|
|
181
193
|
timestamp: account.activity.created?.timestamp,
|
|
182
|
-
timestampUNIX: account.activity.created?.timestampUNIX
|
|
194
|
+
timestampUNIX: account.activity.created?.timestampUNIX,
|
|
183
195
|
};
|
|
184
196
|
sessions.push(currentSession);
|
|
185
197
|
}
|
|
@@ -195,9 +207,7 @@ async function updateActiveSessions(account) {
|
|
|
195
207
|
tries: 2,
|
|
196
208
|
body: {
|
|
197
209
|
command: 'user:get-active-sessions',
|
|
198
|
-
payload: {
|
|
199
|
-
// id: 'app',
|
|
200
|
-
},
|
|
210
|
+
payload: {},
|
|
201
211
|
},
|
|
202
212
|
});
|
|
203
213
|
|
|
@@ -240,7 +250,6 @@ async function updateActiveSessions(account) {
|
|
|
240
250
|
sessions.push(sessionObj);
|
|
241
251
|
});
|
|
242
252
|
}
|
|
243
|
-
|
|
244
253
|
} catch (error) {
|
|
245
254
|
console.error('Failed to get active sessions:', error);
|
|
246
255
|
}
|
|
@@ -259,7 +268,7 @@ async function updateActiveSessions(account) {
|
|
|
259
268
|
region: account.lastActivity.geolocation?.region,
|
|
260
269
|
country: account.lastActivity.geolocation?.country,
|
|
261
270
|
timestamp: account.lastActivity.timestamp,
|
|
262
|
-
timestampUNIX: account.lastActivity.timestampUNIX
|
|
271
|
+
timestampUNIX: account.lastActivity.timestampUNIX,
|
|
263
272
|
};
|
|
264
273
|
|
|
265
274
|
// Only add if it's different from current session (different IP or timestamp)
|
|
@@ -319,21 +328,16 @@ function initializeSigninMethodForms() {
|
|
|
319
328
|
console.log('[DEBUG] security.js - Initializing password FormManager');
|
|
320
329
|
|
|
321
330
|
const formManager = new FormManager($passwordForm, {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
submitButtonSuccessText: 'Email Sent'
|
|
331
|
+
allowResubmit: false,
|
|
332
|
+
submittingText: 'Sending...',
|
|
333
|
+
submittedText: 'Email Sent!',
|
|
326
334
|
});
|
|
327
335
|
|
|
328
336
|
signinMethodForms.set('password', formManager);
|
|
329
337
|
|
|
330
|
-
formManager.
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
} catch (error) {
|
|
334
|
-
formManager.showError(error);
|
|
335
|
-
formManager.setFormState('ready');
|
|
336
|
-
}
|
|
338
|
+
formManager.on('submit', async () => {
|
|
339
|
+
await handleChangePassword();
|
|
340
|
+
formManager.showSuccess('Password reset email sent!');
|
|
337
341
|
});
|
|
338
342
|
}
|
|
339
343
|
|
|
@@ -345,44 +349,24 @@ function initializeSigninMethodForms() {
|
|
|
345
349
|
console.log('[DEBUG] security.js - Google form exists:', !!$googleForm);
|
|
346
350
|
|
|
347
351
|
const formManager = new FormManager($googleForm, {
|
|
348
|
-
|
|
349
|
-
showSpinner: true
|
|
352
|
+
submittingText: 'Connecting...',
|
|
350
353
|
});
|
|
351
354
|
|
|
352
355
|
signinMethodForms.set('google', formManager);
|
|
353
356
|
console.log('[DEBUG] security.js - Google FormManager initialized and stored');
|
|
354
357
|
|
|
355
|
-
formManager.
|
|
356
|
-
event.preventDefault();
|
|
357
|
-
const { submitButton } = event.detail;
|
|
358
|
-
|
|
358
|
+
formManager.on('submit', async ({ $submitButton }) => {
|
|
359
359
|
// Determine action from the clicked button's data-action attribute
|
|
360
|
-
const action = submitButton?.getAttribute('data-action');
|
|
361
|
-
|
|
362
|
-
try {
|
|
363
|
-
if (action === 'disconnect') {
|
|
364
|
-
await disconnectGoogleProvider();
|
|
365
|
-
} else if (action === 'connect') {
|
|
366
|
-
await connectGoogleProvider();
|
|
367
|
-
}
|
|
360
|
+
const action = $submitButton?.getAttribute('data-action');
|
|
368
361
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
updateSigninMethods();
|
|
374
|
-
} catch (error) {
|
|
375
|
-
// Reset form state
|
|
376
|
-
formManager.setFormState('ready');
|
|
377
|
-
|
|
378
|
-
// If user cancelled, also update the display to ensure button state is correct
|
|
379
|
-
if (error.message === 'Disconnection cancelled') {
|
|
380
|
-
updateSigninMethods();
|
|
381
|
-
} else {
|
|
382
|
-
// Show error for other failures
|
|
383
|
-
formManager.showError(error);
|
|
384
|
-
}
|
|
362
|
+
if (action === 'disconnect') {
|
|
363
|
+
await disconnectGoogleProvider();
|
|
364
|
+
} else if (action === 'connect') {
|
|
365
|
+
await connectGoogleProvider();
|
|
385
366
|
}
|
|
367
|
+
|
|
368
|
+
// Update display after success
|
|
369
|
+
updateSigninMethods();
|
|
386
370
|
});
|
|
387
371
|
}
|
|
388
372
|
|
|
@@ -397,41 +381,31 @@ function initializeSigninMethodForms() {
|
|
|
397
381
|
function initializeSignoutAllForm() {
|
|
398
382
|
const $form = document.getElementById('signout-all-sessions-form');
|
|
399
383
|
|
|
400
|
-
if ($form && !
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
showSpinner: true
|
|
384
|
+
if ($form && !signoutAllFormManager) {
|
|
385
|
+
signoutAllFormManager = new FormManager($form, {
|
|
386
|
+
submittingText: 'Signing out...',
|
|
404
387
|
});
|
|
405
388
|
|
|
406
|
-
|
|
407
|
-
|
|
389
|
+
signoutAllFormManager.on('submit', async () => {
|
|
390
|
+
// 1ms wait to allow form state to update and show processing
|
|
391
|
+
await new Promise(resolve => setTimeout(resolve, 1));
|
|
408
392
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
}
|
|
393
|
+
// Confirm sign out
|
|
394
|
+
if (!confirm('Are you sure you want to sign out of all sessions? This will log you out everywhere, including this device.')) {
|
|
395
|
+
throw new Error('Sign out cancelled.');
|
|
396
|
+
}
|
|
414
397
|
|
|
415
|
-
|
|
416
|
-
|
|
398
|
+
// Sign out of all sessions
|
|
399
|
+
await webManager.auth().signOut();
|
|
417
400
|
|
|
418
|
-
|
|
419
|
-
|
|
401
|
+
// Show success message
|
|
402
|
+
signoutAllFormManager.showSuccess('Successfully signed out of all sessions.');
|
|
420
403
|
|
|
421
|
-
|
|
422
|
-
// so we might not need to reset form state
|
|
423
|
-
} catch (error) {
|
|
424
|
-
if (error.message !== 'Sign out cancelled') {
|
|
425
|
-
signoutAllForm.showError(error);
|
|
426
|
-
}
|
|
427
|
-
signoutAllForm.setFormState('ready');
|
|
428
|
-
}
|
|
404
|
+
// Note: The page will likely redirect due to auth state change
|
|
429
405
|
});
|
|
430
406
|
}
|
|
431
407
|
}
|
|
432
408
|
|
|
433
|
-
|
|
434
|
-
|
|
435
409
|
// Connect Google provider
|
|
436
410
|
async function connectGoogleProvider() {
|
|
437
411
|
// Dynamic import of Firebase auth methods
|
|
@@ -439,35 +413,38 @@ async function connectGoogleProvider() {
|
|
|
439
413
|
|
|
440
414
|
const provider = new GoogleAuthProvider();
|
|
441
415
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
416
|
+
// Use popup if query parameter is set, otherwise use redirect
|
|
417
|
+
if (useAuthPopup) {
|
|
418
|
+
try {
|
|
419
|
+
const result = await linkWithPopup(firebaseAuth.currentUser, provider);
|
|
420
|
+
webManager.utilities().showNotification('Google account connected successfully', 'success');
|
|
446
421
|
|
|
447
|
-
|
|
448
|
-
|
|
422
|
+
// Force refresh of the current user to get updated provider data
|
|
423
|
+
await firebaseAuth.currentUser.reload();
|
|
449
424
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
425
|
+
return result;
|
|
426
|
+
} catch (error) {
|
|
427
|
+
// Check if we should fallback to redirect
|
|
428
|
+
if (error.code === 'auth/popup-blocked'
|
|
429
|
+
|| error.code === 'auth/popup-closed-by-user'
|
|
430
|
+
|| error.code === 'auth/cancelled-popup-request') {
|
|
456
431
|
|
|
457
|
-
|
|
432
|
+
console.log('Popup failed, falling back to redirect:', error.code);
|
|
458
433
|
|
|
459
|
-
|
|
460
|
-
try {
|
|
434
|
+
// Fallback to redirect
|
|
461
435
|
await linkWithRedirect(firebaseAuth.currentUser, provider);
|
|
462
436
|
// This will redirect the page, so no immediate result
|
|
463
|
-
}
|
|
464
|
-
throw
|
|
437
|
+
} else if (error.code === 'auth/credential-already-in-use') {
|
|
438
|
+
throw new Error('This Google account is already linked to another user');
|
|
439
|
+
} else {
|
|
440
|
+
throw error;
|
|
465
441
|
}
|
|
466
|
-
} else if (error.code === 'auth/credential-already-in-use') {
|
|
467
|
-
throw new Error('This Google account is already linked to another user');
|
|
468
|
-
} else {
|
|
469
|
-
throw error;
|
|
470
442
|
}
|
|
443
|
+
} else {
|
|
444
|
+
// Use redirect by default
|
|
445
|
+
console.log('Using redirect for Google account linking');
|
|
446
|
+
await linkWithRedirect(firebaseAuth.currentUser, provider);
|
|
447
|
+
// This will redirect the page, so no immediate result
|
|
471
448
|
}
|
|
472
449
|
}
|
|
473
450
|
|
|
@@ -478,7 +455,7 @@ async function disconnectGoogleProvider() {
|
|
|
478
455
|
|
|
479
456
|
// Confirm disconnection
|
|
480
457
|
if (!confirm('Are you sure you want to disconnect your Google account?')) {
|
|
481
|
-
throw new Error('Disconnection cancelled');
|
|
458
|
+
throw new Error('Disconnection cancelled.');
|
|
482
459
|
}
|
|
483
460
|
|
|
484
461
|
// Dynamic import of Firebase auth methods
|
|
@@ -505,22 +482,16 @@ async function disconnectGoogleProvider() {
|
|
|
505
482
|
|
|
506
483
|
// Handle change password
|
|
507
484
|
async function handleChangePassword() {
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
}
|
|
485
|
+
const user = webManager.auth().getUser();
|
|
486
|
+
if (!user || !user.email) {
|
|
487
|
+
throw new Error('Please log in to reset your password.');
|
|
488
|
+
}
|
|
513
489
|
|
|
514
|
-
|
|
515
|
-
|
|
490
|
+
// Import Firebase auth method
|
|
491
|
+
const { sendPasswordResetEmail } = await import('@firebase/auth');
|
|
516
492
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
webManager.utilities().showNotification('Password reset email sent. Please check your inbox.', 'info');
|
|
520
|
-
} catch (error) {
|
|
521
|
-
console.error('Failed to send password reset:', error);
|
|
522
|
-
throw new Error(error.message || 'Failed to send password reset email. Please try again.');
|
|
523
|
-
}
|
|
493
|
+
// Send password reset email
|
|
494
|
+
await sendPasswordResetEmail(firebaseAuth, user.email);
|
|
524
495
|
}
|
|
525
496
|
|
|
526
497
|
// Handle 2FA button click
|
|
@@ -537,10 +508,11 @@ async function handle2FAClick(event) {
|
|
|
537
508
|
}
|
|
538
509
|
}
|
|
539
510
|
|
|
540
|
-
|
|
541
511
|
// Get device from user agent string
|
|
542
512
|
function getDeviceFromUserAgent(userAgent) {
|
|
543
|
-
if (!userAgent)
|
|
513
|
+
if (!userAgent) {
|
|
514
|
+
return 'Unknown Device';
|
|
515
|
+
}
|
|
544
516
|
|
|
545
517
|
const ua = userAgent.toLowerCase();
|
|
546
518
|
|
|
@@ -565,7 +537,9 @@ function getDeviceFromUserAgent(userAgent) {
|
|
|
565
537
|
|
|
566
538
|
// Get browser from user agent string
|
|
567
539
|
function getBrowserFromUserAgent(userAgent) {
|
|
568
|
-
if (!userAgent)
|
|
540
|
+
if (!userAgent) {
|
|
541
|
+
return 'Unknown Browser';
|
|
542
|
+
}
|
|
569
543
|
|
|
570
544
|
const ua = userAgent.toLowerCase();
|
|
571
545
|
|
|
@@ -582,7 +556,9 @@ function getBrowserFromUserAgent(userAgent) {
|
|
|
582
556
|
|
|
583
557
|
// Get platform name from platform string
|
|
584
558
|
function getPlatformName(platform) {
|
|
585
|
-
if (!platform)
|
|
559
|
+
if (!platform) {
|
|
560
|
+
return 'Unknown Device';
|
|
561
|
+
}
|
|
586
562
|
|
|
587
563
|
const platformLower = platform.toLowerCase();
|
|
588
564
|
|
|
@@ -649,7 +625,6 @@ function formatSessionLocation(session) {
|
|
|
649
625
|
return parts.length > 0 ? parts.join(', ') : null;
|
|
650
626
|
}
|
|
651
627
|
|
|
652
|
-
|
|
653
628
|
// Generate fake sessions for development mode
|
|
654
629
|
function generateFakeSessions() {
|
|
655
630
|
const now = Date.now();
|
|
@@ -662,42 +637,44 @@ function generateFakeSessions() {
|
|
|
662
637
|
platform: 'Windows',
|
|
663
638
|
ip: '98.137.246.8',
|
|
664
639
|
timestamp: new Date(now - (3 * oneHour)).toISOString(),
|
|
665
|
-
timestampUNIX: Math.floor((now - (3 * oneHour)) / 1000)
|
|
640
|
+
timestampUNIX: Math.floor((now - (3 * oneHour)) / 1000),
|
|
666
641
|
},
|
|
667
642
|
'session_def456': {
|
|
668
643
|
_current: false,
|
|
669
644
|
platform: 'Darwin', // macOS
|
|
670
645
|
ip: '192.168.1.42',
|
|
671
646
|
timestamp: new Date(now - (8 * oneHour)).toISOString(),
|
|
672
|
-
timestampUNIX: Math.floor((now - (8 * oneHour)) / 1000)
|
|
647
|
+
timestampUNIX: Math.floor((now - (8 * oneHour)) / 1000),
|
|
673
648
|
},
|
|
674
649
|
'session_ghi789': {
|
|
675
650
|
_current: false,
|
|
676
651
|
platform: 'Linux',
|
|
677
652
|
ip: '45.62.189.3',
|
|
678
653
|
timestamp: new Date(now - (oneDay)).toISOString(),
|
|
679
|
-
timestampUNIX: Math.floor((now - (oneDay)) / 1000)
|
|
654
|
+
timestampUNIX: Math.floor((now - (oneDay)) / 1000),
|
|
680
655
|
},
|
|
681
656
|
'session_jkl012': {
|
|
682
657
|
_current: false,
|
|
683
658
|
platform: 'Win32',
|
|
684
659
|
ip: '203.0.113.45',
|
|
685
660
|
timestamp: new Date(now - (2 * oneDay)).toISOString(),
|
|
686
|
-
timestampUNIX: Math.floor((now - (2 * oneDay)) / 1000)
|
|
661
|
+
timestampUNIX: Math.floor((now - (2 * oneDay)) / 1000),
|
|
687
662
|
},
|
|
688
663
|
'session_mno345': {
|
|
689
664
|
_current: false,
|
|
690
665
|
platform: 'Mac',
|
|
691
666
|
ip: '172.217.16.195',
|
|
692
667
|
timestamp: new Date(now - (5 * oneDay)).toISOString(),
|
|
693
|
-
timestampUNIX: Math.floor((now - (5 * oneDay)) / 1000)
|
|
694
|
-
}
|
|
668
|
+
timestampUNIX: Math.floor((now - (5 * oneDay)) / 1000),
|
|
669
|
+
},
|
|
695
670
|
};
|
|
696
671
|
}
|
|
697
672
|
|
|
698
673
|
// Format date helper
|
|
699
674
|
function formatDate(timestamp) {
|
|
700
|
-
if (!timestamp)
|
|
675
|
+
if (!timestamp) {
|
|
676
|
+
return 'Unknown';
|
|
677
|
+
}
|
|
701
678
|
|
|
702
679
|
const date = new Date(timestamp);
|
|
703
680
|
const now = new Date();
|