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/README.md +407 -78
- package/package.json +1 -1
- package/src/login.svg +1 -0
- package/src/nskd-lbr.js +775 -9
- package/src/nskd-lbr.min.js +443 -7
- package/src/t.html +65 -0
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.
|
|
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: ${
|
|
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.
|
|
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
|
|
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');
|