nskd-lbr 1.0.2 → 1.1.0

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/src/nskd-lbr.js CHANGED
@@ -1,14 +1,18 @@
1
1
  /**
2
2
  * NoSkid Certificate Library
3
3
  * A JavaScript library for working with NoSkid certificates
4
- *
5
- * @version 1.0.0
4
+ *
5
+ * @version 1.1.0
6
6
  * @author Douxx <douxx@douxx.tech>
7
7
  * @param {string} [options.apiUrl='https://check.noskid.today/'] - Logs debug messages to console
8
8
  * @param {boolean} [options.debug=false] - Logs debug messages to console
9
9
  * @param {boolean} [options.strictCheck=true] - Whether to validate local data against API response
10
10
  * @param {integer} [options.timeout=10000] - API request timeout in milliseconds
11
+ * @param {boolean} [options.useLegacyAPI=false] - Whether to use the legacy API format
11
12
  * @param {function} [options.onLog=null] - Whether to validate local data against API response
13
+ * @param {string} [options.loginEndpoint=''] - Login API endpoint for authentication
14
+ * @param {function} [options.onLoginSuccess=null] - Callback function for successful login
15
+ * @param {function} [options.onLoginFail=null] - Callback function for failed login
12
16
  */
13
17
 
14
18
  class NskdLbr {
@@ -18,10 +22,15 @@ class NskdLbr {
18
22
  this.timeout = options.timeout || 10000;
19
23
  this.strictCheck = options.strictCheck !== undefined ? options.strictCheck : true;
20
24
  this.onLog = options.onLog || null;
25
+ this.useLegacyAPI = options.useLegacyAPI || false;
26
+ this.loginEndpoint = options.loginEndpoint || '';
27
+ this.onLoginSuccess = options.onLoginSuccess || null;
28
+ this.onLoginFail = options.onLoginFail || null;
21
29
  this.certificateData = null;
22
30
  this.verificationKey = null;
23
31
  this.localData = null;
24
32
  this.isValid = false;
33
+ this.currentLoginModal = null;
25
34
  }
26
35
 
27
36
  /**
@@ -53,6 +62,762 @@ class NskdLbr {
53
62
  }
54
63
  }
55
64
 
65
+ /**
66
+ * Show login modal (only available for webpages with loginEndpoint set)
67
+ * @returns {Promise<Object>} Login result
68
+ */
69
+ async showLoginModal() {
70
+ if (typeof window === 'undefined') {
71
+ throw new Error('Login modal is only available in browser environments');
72
+ }
73
+
74
+ if (!this.loginEndpoint || this.loginEndpoint.trim() === '') {
75
+ throw new Error('Login endpoint is not configured');
76
+ }
77
+
78
+ return new Promise((resolve, reject) => {
79
+ this.createLoginModal(resolve, reject);
80
+ });
81
+ }
82
+
83
+ /**
84
+ * Create and display the login modal
85
+ * @private
86
+ */
87
+ createLoginModal(resolve, reject) {
88
+ if (this.currentLoginModal) {
89
+ this.closeLoginModal();
90
+ }
91
+
92
+ const overlay = document.createElement('div');
93
+ overlay.id = 'noskid-login-overlay';
94
+ overlay.style.cssText = `
95
+ position: fixed;
96
+ top: 0;
97
+ left: 0;
98
+ width: 100%;
99
+ height: 100%;
100
+ background: rgba(139, 69, 19, 0.15);
101
+ display: flex;
102
+ justify-content: center;
103
+ align-items: center;
104
+ z-index: 10000;
105
+ backdrop-filter: blur(8px);
106
+ opacity: 0;
107
+ transition: opacity 0.3s ease;
108
+ `;
109
+
110
+ const modal = document.createElement('div');
111
+ modal.id = 'noskid-login-modal';
112
+ modal.style.cssText = `
113
+ background: linear-gradient(135deg, #fcfaf5 0%, #f0ede0 100%);
114
+ border: 2px solid #8b4513;
115
+ border-radius: 16px;
116
+ padding: 0;
117
+ max-width: 480px;
118
+ width: 95%;
119
+ max-height: 95vh;
120
+ overflow: hidden;
121
+ box-shadow:
122
+ 0 25px 50px -12px rgba(139, 69, 19, 0.25),
123
+ 0 0 0 1px rgba(139, 69, 19, 0.1),
124
+ inset 0 1px 0 rgba(252, 250, 245, 0.9);
125
+ transform: translateY(20px) scale(0.95);
126
+ transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
127
+ position: relative;
128
+ `;
129
+
130
+ const style = document.createElement('style');
131
+ style.textContent = `
132
+ @keyframes noskidModalSlideIn {
133
+ from {
134
+ opacity: 0;
135
+ transform: translateY(20px) scale(0.95);
136
+ }
137
+ to {
138
+ opacity: 1;
139
+ transform: translateY(0) scale(1);
140
+ }
141
+ }
142
+
143
+ @keyframes noskidPulse {
144
+ 0%, 100% { transform: scale(1); }
145
+ 50% { transform: scale(1.05); }
146
+ }
147
+
148
+ @keyframes noskidShake {
149
+ 0%, 100% { transform: translateX(0); }
150
+ 25% { transform: translateX(-4px); }
151
+ 75% { transform: translateX(4px); }
152
+ }
153
+
154
+ .noskid-modal-active {
155
+ opacity: 1 !important;
156
+ }
157
+
158
+ .noskid-modal-active #noskid-login-modal {
159
+ transform: translateY(0) scale(1) !important;
160
+ }
161
+
162
+ .noskid-header {
163
+ background: linear-gradient(135deg, #f9f7f0 0%, #e8e5d8 100%);
164
+ border-bottom: 1px solid #8b4513;
165
+ padding: 24px;
166
+ text-align: center;
167
+ position: relative;
168
+ }
169
+
170
+ .noskid-header::before {
171
+ content: '';
172
+ position: absolute;
173
+ top: 0;
174
+ left: 0;
175
+ right: 0;
176
+ height: 1px;
177
+ background: linear-gradient(90deg, transparent, rgba(139, 69, 19, 0.3), transparent);
178
+ }
179
+
180
+ .noskid-content {
181
+ padding: 32px;
182
+ overflow-y: auto;
183
+ max-height: calc(95vh - 140px);
184
+ }
185
+
186
+ .noskid-title {
187
+ margin: 0;
188
+ color: #8b4513;
189
+ font-size: 24px;
190
+ font-family: 'Times New Roman', serif;
191
+ font-weight: bold;
192
+ display: flex;
193
+ align-items: center;
194
+ justify-content: center;
195
+ gap: 12px;
196
+ }
197
+
198
+ .noskid-subtitle {
199
+ margin: 8px 0 0 0;
200
+ color: rgba(139, 69, 19, 0.7);
201
+ font-size: 14px;
202
+ font-family: system-ui, -apple-system, sans-serif;
203
+ }
204
+
205
+ .noskid-icon {
206
+ width: 28px;
207
+ height: 28px;
208
+ background: #8b4513;
209
+ border-radius: 50%;
210
+ display: flex;
211
+ align-items: center;
212
+ justify-content: center;
213
+ color: #fcfaf5;
214
+ font-size: 16px;
215
+ box-shadow: 0 2px 4px rgba(139, 69, 19, 0.2);
216
+ }
217
+
218
+ .noskid-form-group {
219
+ margin-bottom: 24px;
220
+ }
221
+
222
+ .noskid-label {
223
+ display: block;
224
+ margin-bottom: 8px;
225
+ color: #8b4513;
226
+ font-size: 14px;
227
+ font-weight: 600;
228
+ font-family: system-ui, -apple-system, sans-serif;
229
+ }
230
+
231
+ .noskid-cert-upload {
232
+ border: 2px dashed rgba(139, 69, 19, 0.3);
233
+ border-radius: 12px;
234
+ padding: 24px;
235
+ text-align: center;
236
+ background: linear-gradient(135deg, rgba(252, 250, 245, 0.5), rgba(240, 237, 224, 0.5));
237
+ transition: all 0.3s ease;
238
+ cursor: pointer;
239
+ position: relative;
240
+ min-height: 120px;
241
+ display: flex;
242
+ flex-direction: column;
243
+ align-items: center;
244
+ justify-content: center;
245
+ }
246
+
247
+ .noskid-cert-upload:hover {
248
+ border-color: #8b4513;
249
+ background: linear-gradient(135deg, rgba(252, 250, 245, 0.8), rgba(240, 237, 224, 0.8));
250
+ transform: translateY(-2px);
251
+ box-shadow: 0 4px 12px rgba(139, 69, 19, 0.1);
252
+ }
253
+
254
+ .noskid-cert-upload.dragover {
255
+ border-color: #8b4513;
256
+ background: linear-gradient(135deg, rgba(139, 69, 19, 0.1), rgba(139, 69, 19, 0.05));
257
+ animation: noskidPulse 1s infinite;
258
+ }
259
+
260
+ .noskid-cert-upload.error {
261
+ border-color: #dc2626;
262
+ animation: noskidShake 0.5s ease-in-out;
263
+ }
264
+
265
+ .noskid-cert-upload.success {
266
+ border-color: #059669;
267
+ background: linear-gradient(135deg, rgba(5, 150, 105, 0.1), rgba(5, 150, 105, 0.05));
268
+ }
269
+
270
+ .noskid-upload-icon {
271
+ width: 48px;
272
+ height: 48px;
273
+ margin-bottom: 12px;
274
+ background: rgba(139, 69, 19, 0.1);
275
+ border-radius: 50%;
276
+ display: flex;
277
+ align-items: center;
278
+ justify-content: center;
279
+ color: #8b4513;
280
+ font-size: 24px;
281
+ transition: all 0.3s ease;
282
+ }
283
+
284
+ .noskid-cert-upload:hover .noskid-upload-icon {
285
+ background: rgba(139, 69, 19, 0.2);
286
+ transform: scale(1.1);
287
+ }
288
+
289
+ .noskid-upload-text {
290
+ color: #8b4513;
291
+ font-size: 16px;
292
+ font-weight: 600;
293
+ margin-bottom: 4px;
294
+ }
295
+
296
+ .noskid-upload-hint {
297
+ color: rgba(139, 69, 19, 0.6);
298
+ font-size: 12px;
299
+ line-height: 1.4;
300
+ }
301
+
302
+ .noskid-file-input {
303
+ position: absolute;
304
+ top: 0;
305
+ left: 0;
306
+ width: 100%;
307
+ height: 100%;
308
+ opacity: 0;
309
+ cursor: pointer;
310
+ }
311
+
312
+ .noskid-input {
313
+ width: 100%;
314
+ padding: 16px;
315
+ border: 2px solid rgba(139, 69, 19, 0.2);
316
+ border-radius: 12px;
317
+ font-size: 16px;
318
+ box-sizing: border-box;
319
+ background: rgba(252, 250, 245, 0.8);
320
+ color: #8b4513;
321
+ font-family: system-ui, -apple-system, sans-serif;
322
+ transition: all 0.3s ease;
323
+ }
324
+
325
+ .noskid-input:focus {
326
+ outline: none;
327
+ border-color: #8b4513;
328
+ box-shadow: 0 0 0 4px rgba(139, 69, 19, 0.1);
329
+ background: #fcfaf5;
330
+ }
331
+
332
+ .noskid-input::placeholder {
333
+ color: rgba(139, 69, 19, 0.5);
334
+ }
335
+
336
+ .noskid-btn {
337
+ background: linear-gradient(135deg, #8b4513 0%, #6d3410 100%);
338
+ color: #fcfaf5;
339
+ border: none;
340
+ padding: 16px 32px;
341
+ border-radius: 12px;
342
+ cursor: pointer;
343
+ font-size: 16px;
344
+ font-weight: 600;
345
+ font-family: system-ui, -apple-system, sans-serif;
346
+ transition: all 0.3s ease;
347
+ box-shadow: 0 4px 12px rgba(139, 69, 19, 0.2);
348
+ position: relative;
349
+ overflow: hidden;
350
+ }
351
+
352
+ .noskid-btn::before {
353
+ content: '';
354
+ position: absolute;
355
+ top: 0;
356
+ left: -100%;
357
+ width: 100%;
358
+ height: 100%;
359
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
360
+ transition: left 0.5s ease;
361
+ }
362
+
363
+ .noskid-btn:hover::before {
364
+ left: 100%;
365
+ }
366
+
367
+ .noskid-btn:hover {
368
+ background: linear-gradient(135deg, #6d3410 0%, #5a2b0d 100%);
369
+ transform: translateY(-2px);
370
+ box-shadow: 0 6px 20px rgba(139, 69, 19, 0.3);
371
+ }
372
+
373
+ .noskid-btn:active {
374
+ transform: translateY(0);
375
+ }
376
+
377
+ .noskid-btn:disabled {
378
+ background: rgba(139, 69, 19, 0.3);
379
+ cursor: not-allowed;
380
+ transform: none;
381
+ box-shadow: none;
382
+ }
383
+
384
+ .noskid-btn:disabled::before {
385
+ display: none;
386
+ }
387
+
388
+ .noskid-btn-secondary {
389
+ background: linear-gradient(135deg, rgba(139, 69, 19, 0.1) 0%, rgba(139, 69, 19, 0.2) 100%);
390
+ color: #8b4513;
391
+ border: 2px solid rgba(139, 69, 19, 0.3);
392
+ padding: 14px 30px;
393
+ border-radius: 12px;
394
+ cursor: pointer;
395
+ font-size: 16px;
396
+ font-weight: 600;
397
+ font-family: system-ui, -apple-system, sans-serif;
398
+ transition: all 0.3s ease;
399
+ }
400
+
401
+ .noskid-btn-secondary:hover {
402
+ background: linear-gradient(135deg, rgba(139, 69, 19, 0.2) 0%, rgba(139, 69, 19, 0.3) 100%);
403
+ border-color: #8b4513;
404
+ transform: translateY(-2px);
405
+ }
406
+
407
+ .noskid-error {
408
+ color: #dc2626;
409
+ font-size: 14px;
410
+ margin-top: 8px;
411
+ padding: 12px;
412
+ background: rgba(220, 38, 38, 0.1);
413
+ border: 1px solid rgba(220, 38, 38, 0.2);
414
+ border-radius: 8px;
415
+ font-family: system-ui, -apple-system, sans-serif;
416
+ }
417
+
418
+ .noskid-success {
419
+ color: #059669;
420
+ font-size: 14px;
421
+ margin-top: 8px;
422
+ padding: 12px;
423
+ background: rgba(5, 150, 105, 0.1);
424
+ border: 1px solid rgba(5, 150, 105, 0.2);
425
+ border-radius: 8px;
426
+ font-family: system-ui, -apple-system, sans-serif;
427
+ display: flex;
428
+ align-items: center;
429
+ gap: 8px;
430
+ }
431
+
432
+ .noskid-actions {
433
+ display: flex;
434
+ gap: 16px;
435
+ justify-content: flex-end;
436
+ margin-top: 32px;
437
+ }
438
+
439
+ .noskid-cert-info {
440
+ background: rgba(139, 69, 19, 0.05);
441
+ border: 1px solid rgba(139, 69, 19, 0.2);
442
+ border-radius: 8px;
443
+ padding: 16px;
444
+ margin-top: 12px;
445
+ }
446
+
447
+ .noskid-cert-user {
448
+ font-weight: 600;
449
+ color: #8b4513;
450
+ margin-bottom: 4px;
451
+ }
452
+
453
+ .noskid-cert-details {
454
+ font-size: 12px;
455
+ color: rgba(139, 69, 19, 0.7);
456
+ }
457
+
458
+ .noskid-paste-hint {
459
+ margin-top: 8px;
460
+ font-size: 11px;
461
+ color: rgba(139, 69, 19, 0.5);
462
+ text-align: center;
463
+ font-style: italic;
464
+ }
465
+
466
+ @media (max-width: 600px) {
467
+ .noskid-content {
468
+ padding: 24px;
469
+ }
470
+
471
+ .noskid-header {
472
+ padding: 20px;
473
+ }
474
+
475
+ .noskid-title {
476
+ font-size: 20px;
477
+ }
478
+
479
+ .noskid-actions {
480
+ flex-direction: column-reverse;
481
+ }
482
+
483
+ .noskid-btn,
484
+ .noskid-btn-secondary {
485
+ width: 100%;
486
+ justify-content: center;
487
+ }
488
+ }
489
+ `;
490
+ document.head.appendChild(style);
491
+
492
+ modal.innerHTML = `
493
+ <div class="noskid-header">
494
+ <h2 class="noskid-title">
495
+ <div class="noskid-icon">🔐</div>
496
+ Login with NoSkid
497
+ </h2>
498
+ <p class="noskid-subtitle">Upload your certificate and enter your password to authenticate</p>
499
+ </div>
500
+
501
+ <div class="noskid-content">
502
+ <div class="noskid-form-group">
503
+ <label class="noskid-label">Certificate File</label>
504
+ <div class="noskid-cert-upload" id="noskid-cert-upload">
505
+ <div class="noskid-upload-icon">📄</div>
506
+ <div class="noskid-upload-text">Drop certificate here or click to browse</div>
507
+ <div class="noskid-upload-hint">
508
+ Supports .png files • Drag & drop • Paste from clipboard<br>
509
+ <kbd>Ctrl+V</kbd> to paste certificate
510
+ </div>
511
+ <input type="file" id="noskid-cert-file" accept=".png" class="noskid-file-input">
512
+ </div>
513
+ <div id="noskid-file-error" class="noskid-error" style="display: none;"></div>
514
+ <div id="noskid-file-success" class="noskid-success" style="display: none;">
515
+ <span>✓</span>
516
+ <div id="noskid-cert-info"></div>
517
+ </div>
518
+ </div>
519
+
520
+ <div class="noskid-form-group">
521
+ <label class="noskid-label">Password</label>
522
+ <input type="password" id="noskid-password" class="noskid-input" placeholder="Enter your password">
523
+ <div id="noskid-password-error" class="noskid-error" style="display: none;"></div>
524
+ </div>
525
+
526
+ <div id="noskid-login-error" class="noskid-error" style="display: none;"></div>
527
+
528
+ <div class="noskid-actions">
529
+ <button id="noskid-cancel-btn" class="noskid-btn-secondary">Cancel</button>
530
+ <button id="noskid-login-btn" class="noskid-btn" disabled>
531
+ <span>Login</span>
532
+ </button>
533
+ </div>
534
+ </div>
535
+ `;
536
+
537
+ overlay.appendChild(modal);
538
+ document.body.appendChild(overlay);
539
+ this.currentLoginModal = overlay;
540
+
541
+ requestAnimationFrame(() => {
542
+ overlay.classList.add('noskid-modal-active');
543
+ });
544
+
545
+ const uploadArea = modal.querySelector('#noskid-cert-upload');
546
+ const fileInput = modal.querySelector('#noskid-cert-file');
547
+ const passwordInput = modal.querySelector('#noskid-password');
548
+ const loginBtn = modal.querySelector('#noskid-login-btn');
549
+ const cancelBtn = modal.querySelector('#noskid-cancel-btn');
550
+ const fileError = modal.querySelector('#noskid-file-error');
551
+ const fileSuccess = modal.querySelector('#noskid-file-success');
552
+ const certInfo = modal.querySelector('#noskid-cert-info');
553
+ const passwordError = modal.querySelector('#noskid-password-error');
554
+ const loginError = modal.querySelector('#noskid-login-error');
555
+
556
+ let certificateValid = false;
557
+ let tempCertData = null;
558
+
559
+ const processFile = async (file) => {
560
+ fileError.style.display = 'none';
561
+ fileSuccess.style.display = 'none';
562
+ uploadArea.classList.remove('error', 'success');
563
+ certificateValid = false;
564
+ tempCertData = null;
565
+ updateLoginButton();
566
+
567
+ if (!file) return;
568
+
569
+ try {
570
+ this.nskdLbrLog('Processing certificate for login...', 'info');
571
+
572
+ const tempInstance = new NskdLbr({
573
+ apiUrl: this.apiUrl,
574
+ debug: this.debug,
575
+ timeout: this.timeout,
576
+ strictCheck: this.strictCheck,
577
+ useLegacyAPI: this.useLegacyAPI
578
+ });
579
+
580
+ const result = await tempInstance.loadFromFile(file);
581
+
582
+ if (result.valid) {
583
+ certificateValid = true;
584
+ tempCertData = tempInstance.getCertificateData();
585
+
586
+ certInfo.innerHTML = `
587
+ <div class="noskid-cert-user">${tempCertData.localUsername || 'Unknown User'}</div>
588
+ <div class="noskid-cert-details">
589
+ Certificate verified • ${(file.size / 1024).toFixed(1)} KB
590
+ </div>
591
+ `;
592
+
593
+ uploadArea.classList.add('success');
594
+ uploadArea.querySelector('.noskid-upload-text').textContent = 'Certificate loaded successfully!';
595
+ uploadArea.querySelector('.noskid-upload-icon').textContent = '✓';
596
+ fileSuccess.style.display = 'block';
597
+
598
+ this.nskdLbrLog('Certificate verified for login', 'success');
599
+ } else {
600
+ throw new Error(result.message || 'Certificate verification failed');
601
+ }
602
+ } catch (error) {
603
+ uploadArea.classList.add('error');
604
+ uploadArea.querySelector('.noskid-upload-text').textContent = 'Certificate verification failed';
605
+ uploadArea.querySelector('.noskid-upload-icon').textContent = '❌';
606
+ fileError.textContent = error.message;
607
+ fileError.style.display = 'block';
608
+ this.nskdLbrLog(`Certificate verification failed: ${error.message}`, 'error');
609
+ }
610
+
611
+ updateLoginButton();
612
+ };
613
+
614
+ fileInput.addEventListener('change', async (e) => {
615
+ const file = e.target.files[0];
616
+ await processFile(file);
617
+ });
618
+
619
+ uploadArea.addEventListener('dragover', (e) => {
620
+ e.preventDefault();
621
+ uploadArea.classList.add('dragover');
622
+ });
623
+
624
+ uploadArea.addEventListener('dragleave', (e) => {
625
+ e.preventDefault();
626
+ if (!uploadArea.contains(e.relatedTarget)) {
627
+ uploadArea.classList.remove('dragover');
628
+ }
629
+ });
630
+
631
+ uploadArea.addEventListener('drop', async (e) => {
632
+ e.preventDefault();
633
+ uploadArea.classList.remove('dragover');
634
+
635
+ const files = e.dataTransfer.files;
636
+ if (files.length > 0) {
637
+ const file = files[0];
638
+ if (file.type === 'image/png' || file.name.endsWith('.png')) {
639
+ await processFile(file);
640
+ } else {
641
+ uploadArea.classList.add('error');
642
+ fileError.textContent = 'Please drop a valid PNG certificate file';
643
+ fileError.style.display = 'block';
644
+ }
645
+ }
646
+ });
647
+
648
+ document.addEventListener('paste', async (e) => {
649
+ if (!overlay.classList.contains('noskid-modal-active')) return;
650
+
651
+ const items = e.clipboardData.items;
652
+ for (let item of items) {
653
+ if (item.type === 'image/png') {
654
+ e.preventDefault();
655
+ const file = item.getAsFile();
656
+ if (file) {
657
+ await processFile(file);
658
+ }
659
+ break;
660
+ }
661
+ }
662
+ });
663
+
664
+ passwordInput.addEventListener('input', () => {
665
+ passwordError.style.display = 'none';
666
+ loginError.style.display = 'none';
667
+ updateLoginButton();
668
+ });
669
+
670
+ const updateLoginButton = () => {
671
+ const hasPassword = passwordInput.value.trim().length > 0;
672
+ loginBtn.disabled = !certificateValid || !hasPassword;
673
+
674
+ loginBtn.innerHTML = '<span>Login</span>';
675
+ };
676
+
677
+ loginBtn.addEventListener('click', async () => {
678
+ const password = passwordInput.value.trim();
679
+
680
+ if (!certificateValid || !tempCertData) {
681
+ passwordError.textContent = 'Please upload a valid certificate first';
682
+ passwordError.style.display = 'block';
683
+ return;
684
+ }
685
+
686
+ if (!password) {
687
+ passwordError.textContent = 'Please enter your password';
688
+ passwordError.style.display = 'block';
689
+ return;
690
+ }
691
+
692
+ loginBtn.disabled = true;
693
+ loginBtn.innerHTML = '<span>Logging in...</span>';
694
+ loginError.style.display = 'none';
695
+
696
+ try {
697
+ const loginResult = await this.performLogin(tempCertData, password);
698
+
699
+ if (loginResult.success) {
700
+ this.nskdLbrLog('Login successful', 'success');
701
+
702
+ this.certificateData = tempCertData;
703
+ this.verificationKey = tempCertData.key;
704
+ this.isValid = true;
705
+
706
+ if (this.onLoginSuccess && typeof this.onLoginSuccess === 'function') {
707
+ this.onLoginSuccess(loginResult, tempCertData);
708
+ }
709
+
710
+ loginBtn.innerHTML = '<span>Success!</span>';
711
+ setTimeout(() => {
712
+ this.closeLoginModal();
713
+ resolve({ success: true, data: tempCertData, response: loginResult });
714
+ }, 800);
715
+ } else {
716
+ throw new Error(loginResult.message || 'Login failed');
717
+ }
718
+ } catch (error) {
719
+ this.nskdLbrLog(`Login failed: ${error.message}`, 'error');
720
+ loginError.textContent = error.message;
721
+ loginError.style.display = 'block';
722
+
723
+ if (this.onLoginFail && typeof this.onLoginFail === 'function') {
724
+ this.onLoginFail(error, tempCertData);
725
+ }
726
+
727
+ loginBtn.disabled = false;
728
+ loginBtn.innerHTML = '<span>Try Again</span>';
729
+ }
730
+ });
731
+
732
+ cancelBtn.addEventListener('click', () => {
733
+ this.nskdLbrLog('Login cancelled by user', 'info');
734
+ this.closeLoginModal();
735
+ reject(new Error('Login cancelled by user'));
736
+ });
737
+
738
+ overlay.addEventListener('click', (e) => {
739
+ if (e.target === overlay) {
740
+ this.nskdLbrLog('Login cancelled by clicking outside', 'info');
741
+ this.closeLoginModal();
742
+ reject(new Error('Login cancelled'));
743
+ }
744
+ });
745
+
746
+ const escHandler = (e) => {
747
+ if (e.key === 'Escape') {
748
+ this.nskdLbrLog('Login cancelled by ESC key', 'info');
749
+ this.closeLoginModal();
750
+ document.removeEventListener('keydown', escHandler);
751
+ reject(new Error('Login cancelled'));
752
+ }
753
+ };
754
+ document.addEventListener('keydown', escHandler);
755
+
756
+ if (certificateValid) {
757
+ passwordInput.focus();
758
+ }
759
+ }
760
+
761
+ /**
762
+ * Perform login API call
763
+ * @private
764
+ */
765
+ async performLogin(certData, password) {
766
+ const controller = new AbortController();
767
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
768
+
769
+ try {
770
+ const response = await fetch(this.loginEndpoint, {
771
+ method: 'POST',
772
+ headers: {
773
+ 'Content-Type': 'application/json',
774
+ 'User-Agent': 'NskdLbr/1.1.0'
775
+ },
776
+ body: JSON.stringify({
777
+ certificate: {
778
+ key: certData.key,
779
+ username: certData.localUsername,
780
+ nickname: certData.nickname,
781
+ certificate_number: certData.certificate_number,
782
+ percentage: certData.percentage,
783
+ country: certData.country,
784
+ countryCode: certData.countryCode,
785
+ creationDate: certData.creationDate
786
+ },
787
+ password: password
788
+ }),
789
+ signal: controller.signal
790
+ });
791
+
792
+ clearTimeout(timeoutId);
793
+
794
+ if (!response.ok) {
795
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
796
+ }
797
+
798
+ const result = await response.json();
799
+ return result;
800
+
801
+ } catch (error) {
802
+ clearTimeout(timeoutId);
803
+ if (error.name === 'AbortError') {
804
+ throw new Error('Login timeout - server took too long to respond');
805
+ }
806
+ throw new Error(`Login API error: ${error.message}`);
807
+ }
808
+ }
809
+
810
+ /**
811
+ * Close the login modal
812
+ * @private
813
+ */
814
+ closeLoginModal() {
815
+ if (this.currentLoginModal) {
816
+ this.currentLoginModal.remove();
817
+ this.currentLoginModal = null;
818
+ }
819
+ }
820
+
56
821
  /**
57
822
  * Load and verify a certificate from a PNG file
58
823
  * @param {File} file - The PNG certificate file
@@ -146,8 +911,6 @@ class NskdLbr {
146
911
  };
147
912
  }
148
913
 
149
-
150
-
151
914
  /**
152
915
  * Check if the certificate is valid
153
916
  * @returns {boolean} True if certificate is valid
@@ -166,10 +929,12 @@ class NskdLbr {
166
929
  }
167
930
 
168
931
  const data = this.certificateData;
932
+ const username = this.useLegacyAPI ? data.username : data.nickname || data.username;
933
+
169
934
  return `
170
935
  Certificate Details:
171
936
  - Certificate #: ${data.certificate_number}
172
- - Username: ${data.username}
937
+ - Username: ${username}
173
938
  - Percentage: ${data.percentage}%
174
939
  - Creation Date: ${data.creationDate}
175
940
  - Country: ${data.country} (${data.countryCode})
@@ -184,6 +949,7 @@ Certificate Details:
184
949
  this.verificationKey = null;
185
950
  this.localData = null;
186
951
  this.isValid = false;
952
+ this.closeLoginModal();
187
953
  this.nskdLbrLog('Certificate data reset', 'info');
188
954
  }
189
955
 
@@ -248,7 +1014,6 @@ Certificate Details:
248
1014
  }
249
1015
  }
250
1016
  }
251
-
252
1017
  pos += 8 + length + 4;
253
1018
  }
254
1019
 
@@ -324,7 +1089,7 @@ Certificate Details:
324
1089
  {
325
1090
  signal: controller.signal,
326
1091
  headers: {
327
- 'User-Agent': 'NskdLbr/1.0.0'
1092
+ 'User-Agent': 'NskdLbr/1.1.0'
328
1093
  }
329
1094
  }
330
1095
  );
@@ -349,7 +1114,9 @@ Certificate Details:
349
1114
 
350
1115
  // Compare local data with API data if available and strictCheck is enabled
351
1116
  if (this.localData && this.strictCheck) {
352
- const validationResult = this.compareData(this.localData, apiData.data);
1117
+ const apiUsername = this.useLegacyAPI ? apiData.data.username : (apiData.data.nickname || apiData.data.username);
1118
+ const validationResult = this.compareData(this.localData, { ...apiData.data, username: apiUsername });
1119
+
353
1120
  if (!validationResult.valid) {
354
1121
  this.isValid = false;
355
1122
  this.nskdLbrLog('Certificate data mismatch!', 'error');
@@ -367,7 +1134,6 @@ Certificate Details:
367
1134
  this.nskdLbrLog('Strict checking disabled - skipping local data validation', 'warning');
368
1135
  }
369
1136
 
370
- // Certificate is valid
371
1137
  this.isValid = true;
372
1138
  this.certificateData = apiData.data;
373
1139
  this.nskdLbrLog('Certificate is VALID!', 'success');