ldap-rest 0.0.1 → 0.1.1
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 +38 -0
- package/bin/sync-app-accounts.mjs +276 -0
- package/dist/abstract/ldapFlat.js +1 -1
- package/dist/abstract/plugin.js +1 -1
- package/dist/bin/index.js +1 -1
- package/dist/config/args.js +1 -1
- package/dist/lib/ldapActions.js +1 -1
- package/dist/lib/utils.js +1 -1
- package/dist/logger/winston.js +1 -1
- package/dist/plugins/auth/hmac.js +1 -0
- package/dist/plugins/auth/totp.js +1 -0
- package/dist/plugins/demo/helloworld.js +1 -1
- package/dist/plugins/ldap/groups.js +1 -1
- package/dist/plugins/ldap/onChange.js +1 -1
- package/dist/plugins/static.js +1 -1
- package/dist/plugins/twake/appAccountsApi.js +1 -0
- package/dist/plugins/twake/appAccountsConsistency.js +1 -0
- package/dist/plugins/twake/james.js +1 -1
- package/dist/plugins/weblogs.js +1 -1
- package/dist/src/abstract/ldapFlat.d.ts +0 -5
- package/dist/src/abstract/plugin.d.ts +1 -0
- package/dist/src/bin/index.d.ts +1 -0
- package/dist/src/browser/shared/index.d.ts +1 -0
- package/dist/src/browser/shared/utils/hmac.d.ts +73 -0
- package/dist/src/browser/shared/utils/totp.d.ts +64 -0
- package/dist/src/config/args.d.ts +9 -1
- package/dist/src/hooks.d.ts +2 -2
- package/dist/src/lib/ldapActions.d.ts +3 -7
- package/dist/src/lib/utils.d.ts +17 -0
- package/dist/src/logger/winston.d.ts +5 -0
- package/dist/src/plugins/auth/hmac.d.ts +24 -0
- package/dist/src/plugins/auth/totp.d.ts +28 -0
- package/dist/src/plugins/ldap/onChange.d.ts +1 -0
- package/dist/src/plugins/static.d.ts +4 -0
- package/dist/src/plugins/twake/appAccountsApi.d.ts +52 -0
- package/dist/src/plugins/twake/appAccountsConsistency.d.ts +58 -0
- package/docs/DEVELOPER_GUIDE.md +1 -0
- package/docs/PLUGIN_DEPENDENCIES.md +19 -0
- package/docs/README.md +76 -30
- package/docs/authentication.md +604 -12
- package/docs/plugins/DEVELOPMENT.md +162 -76
- package/docs/testing-with-embedded-ldap.md +347 -0
- package/docs/twakeAppAccountsApi.md +485 -0
- package/docs/weblogs.md +37 -8
- package/examples/web/README.md +175 -0
- package/examples/web/ldap-tree-viewer.html +16 -2
- package/examples/web/totp-client.html +541 -0
- package/package.json +132 -87
- package/static/browser/shared/index.js +1 -0
- package/static/browser/shared/index.js.map +1 -1
- package/static/browser/shared/utils/hmac.js +202 -0
- package/static/browser/shared/utils/hmac.js.map +1 -0
- package/static/browser/shared/utils/totp.js +182 -0
- package/static/browser/shared/utils/totp.js.map +1 -0
- package/static/browser/shared.esm.js +181 -1
- package/static/browser/shared.esm.js.map +1 -1
- package/static/browser/shared.js +184 -0
- package/static/browser/shared.js.map +1 -1
- package/static/browser/shared.min.js +1 -1
- package/static/browser/shared.min.js.map +1 -1
- package/static/schemas/obm/organizations.json +24 -0
- package/static/schemas/obm/users.json +192 -0
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>TOTP Client Demo</title>
|
|
7
|
+
|
|
8
|
+
<!-- Google Fonts -->
|
|
9
|
+
<link
|
|
10
|
+
href="https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@400;500;700&family=Roboto:wght@300;400;500;700&display=swap"
|
|
11
|
+
rel="stylesheet"
|
|
12
|
+
/>
|
|
13
|
+
<link
|
|
14
|
+
href="https://fonts.googleapis.com/icon?family=Material+Icons"
|
|
15
|
+
rel="stylesheet"
|
|
16
|
+
/>
|
|
17
|
+
|
|
18
|
+
<!-- Shared CSS -->
|
|
19
|
+
<link rel="stylesheet" href="/static/browser/common-demo.css" />
|
|
20
|
+
|
|
21
|
+
<style>
|
|
22
|
+
:root {
|
|
23
|
+
--primary-color: #00bfa5;
|
|
24
|
+
--primary-color-dark: #00897b;
|
|
25
|
+
--success-color: #4caf50;
|
|
26
|
+
--error-color: #f44336;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
body {
|
|
30
|
+
background: linear-gradient(135deg, #00bfa5 0%, #00695c 100%);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.demo-header h1 {
|
|
34
|
+
color: #00bfa5;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.totp-code {
|
|
38
|
+
font-family: 'Roboto Mono', monospace;
|
|
39
|
+
font-size: 48px;
|
|
40
|
+
font-weight: 700;
|
|
41
|
+
text-align: center;
|
|
42
|
+
padding: 30px;
|
|
43
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
44
|
+
color: white;
|
|
45
|
+
border-radius: 12px;
|
|
46
|
+
letter-spacing: 8px;
|
|
47
|
+
margin: 20px 0;
|
|
48
|
+
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
|
|
49
|
+
user-select: all;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.totp-code.expired {
|
|
53
|
+
opacity: 0.5;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.countdown {
|
|
57
|
+
text-align: center;
|
|
58
|
+
font-size: 18px;
|
|
59
|
+
color: #666;
|
|
60
|
+
margin-top: 10px;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.countdown-bar {
|
|
64
|
+
height: 6px;
|
|
65
|
+
background: #e0e0e0;
|
|
66
|
+
border-radius: 3px;
|
|
67
|
+
overflow: hidden;
|
|
68
|
+
margin: 10px 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.countdown-bar-fill {
|
|
72
|
+
height: 100%;
|
|
73
|
+
background: linear-gradient(
|
|
74
|
+
90deg,
|
|
75
|
+
#4caf50 0%,
|
|
76
|
+
#ff9800 50%,
|
|
77
|
+
#f44336 100%
|
|
78
|
+
);
|
|
79
|
+
transition: width 0.3s ease;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.secret-input {
|
|
83
|
+
font-family: 'Roboto Mono', monospace;
|
|
84
|
+
width: 100%;
|
|
85
|
+
padding: 12px;
|
|
86
|
+
font-size: 16px;
|
|
87
|
+
border: 2px solid #ddd;
|
|
88
|
+
border-radius: 8px;
|
|
89
|
+
margin: 10px 0;
|
|
90
|
+
text-transform: uppercase;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.secret-input:focus {
|
|
94
|
+
outline: none;
|
|
95
|
+
border-color: var(--primary-color);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.config-grid {
|
|
99
|
+
display: grid;
|
|
100
|
+
grid-template-columns: 1fr 1fr;
|
|
101
|
+
gap: 15px;
|
|
102
|
+
margin: 20px 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.config-field label {
|
|
106
|
+
display: block;
|
|
107
|
+
font-weight: 500;
|
|
108
|
+
margin-bottom: 5px;
|
|
109
|
+
color: #555;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.config-field input,
|
|
113
|
+
.config-field select {
|
|
114
|
+
width: 100%;
|
|
115
|
+
padding: 10px;
|
|
116
|
+
border: 2px solid #ddd;
|
|
117
|
+
border-radius: 8px;
|
|
118
|
+
font-size: 14px;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.api-test {
|
|
122
|
+
margin-top: 30px;
|
|
123
|
+
padding: 20px;
|
|
124
|
+
background: #f9f9f9;
|
|
125
|
+
border-radius: 8px;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.api-response {
|
|
129
|
+
font-family: 'Roboto Mono', monospace;
|
|
130
|
+
font-size: 12px;
|
|
131
|
+
background: #263238;
|
|
132
|
+
color: #aed581;
|
|
133
|
+
padding: 15px;
|
|
134
|
+
border-radius: 8px;
|
|
135
|
+
overflow-x: auto;
|
|
136
|
+
white-space: pre-wrap;
|
|
137
|
+
word-wrap: break-word;
|
|
138
|
+
max-height: 300px;
|
|
139
|
+
overflow-y: auto;
|
|
140
|
+
margin-top: 10px;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.api-response.error {
|
|
144
|
+
color: #ef5350;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.info-box {
|
|
148
|
+
background: #e3f2fd;
|
|
149
|
+
border-left: 4px solid #2196f3;
|
|
150
|
+
padding: 15px;
|
|
151
|
+
margin: 20px 0;
|
|
152
|
+
border-radius: 4px;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.info-box h3 {
|
|
156
|
+
margin-top: 0;
|
|
157
|
+
color: #1976d2;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.info-box code {
|
|
161
|
+
background: #fff;
|
|
162
|
+
padding: 2px 6px;
|
|
163
|
+
border-radius: 3px;
|
|
164
|
+
font-family: 'Roboto Mono', monospace;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.button-group {
|
|
168
|
+
display: flex;
|
|
169
|
+
gap: 10px;
|
|
170
|
+
margin: 15px 0;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.demo-button.secondary {
|
|
174
|
+
background: #6c757d;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.demo-button.secondary:hover {
|
|
178
|
+
background: #5a6268;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.demo-button.success {
|
|
182
|
+
background: #4caf50;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.demo-button.success:hover {
|
|
186
|
+
background: #45a049;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
@media (max-width: 768px) {
|
|
190
|
+
.config-grid {
|
|
191
|
+
grid-template-columns: 1fr;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.totp-code {
|
|
195
|
+
font-size: 36px;
|
|
196
|
+
letter-spacing: 6px;
|
|
197
|
+
padding: 20px;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
</style>
|
|
201
|
+
</head>
|
|
202
|
+
<body>
|
|
203
|
+
<div class="demo-container">
|
|
204
|
+
<div class="demo-header">
|
|
205
|
+
<h1>🔐 TOTP Client Demo</h1>
|
|
206
|
+
<p>Time-based One-Time Password Generator and API Client</p>
|
|
207
|
+
</div>
|
|
208
|
+
|
|
209
|
+
<div id="status-container"></div>
|
|
210
|
+
|
|
211
|
+
<div class="demo-grid">
|
|
212
|
+
<!-- TOTP Generator Panel -->
|
|
213
|
+
<div class="demo-panel">
|
|
214
|
+
<h2>
|
|
215
|
+
<span class="material-icons">vpn_key</span>
|
|
216
|
+
TOTP Generator
|
|
217
|
+
</h2>
|
|
218
|
+
|
|
219
|
+
<div class="info-box">
|
|
220
|
+
<h3>What is TOTP?</h3>
|
|
221
|
+
<p>
|
|
222
|
+
TOTP generates temporary codes that change every 30 seconds. Enter
|
|
223
|
+
your Base32 secret to generate codes compatible with Google
|
|
224
|
+
Authenticator, Authy, and other authenticator apps.
|
|
225
|
+
</p>
|
|
226
|
+
</div>
|
|
227
|
+
|
|
228
|
+
<label
|
|
229
|
+
for="secret-input"
|
|
230
|
+
style="font-weight: 500; margin-bottom: 5px; display: block"
|
|
231
|
+
>
|
|
232
|
+
Base32 Secret
|
|
233
|
+
</label>
|
|
234
|
+
<input
|
|
235
|
+
type="text"
|
|
236
|
+
id="secret-input"
|
|
237
|
+
class="secret-input"
|
|
238
|
+
placeholder="JBSWY3DPEHPK3PXP"
|
|
239
|
+
value="JBSWY3DPEHPK3PXP"
|
|
240
|
+
/>
|
|
241
|
+
|
|
242
|
+
<div class="config-grid">
|
|
243
|
+
<div class="config-field">
|
|
244
|
+
<label for="digits-input">Digits</label>
|
|
245
|
+
<select id="digits-input">
|
|
246
|
+
<option value="6" selected>6 digits</option>
|
|
247
|
+
<option value="8">8 digits</option>
|
|
248
|
+
<option value="10">10 digits</option>
|
|
249
|
+
</select>
|
|
250
|
+
</div>
|
|
251
|
+
<div class="config-field">
|
|
252
|
+
<label for="step-input">Time Step (seconds)</label>
|
|
253
|
+
<input
|
|
254
|
+
type="number"
|
|
255
|
+
id="step-input"
|
|
256
|
+
value="30"
|
|
257
|
+
min="10"
|
|
258
|
+
max="120"
|
|
259
|
+
/>
|
|
260
|
+
</div>
|
|
261
|
+
</div>
|
|
262
|
+
|
|
263
|
+
<div class="button-group">
|
|
264
|
+
<button id="btn-generate" class="demo-button success">
|
|
265
|
+
<span class="material-icons" style="vertical-align: middle">
|
|
266
|
+
refresh
|
|
267
|
+
</span>
|
|
268
|
+
Generate Code
|
|
269
|
+
</button>
|
|
270
|
+
<button id="btn-copy" class="demo-button secondary">
|
|
271
|
+
<span class="material-icons" style="vertical-align: middle">
|
|
272
|
+
content_copy
|
|
273
|
+
</span>
|
|
274
|
+
Copy Code
|
|
275
|
+
</button>
|
|
276
|
+
</div>
|
|
277
|
+
|
|
278
|
+
<div id="totp-display">
|
|
279
|
+
<div class="totp-code" id="totp-code">------</div>
|
|
280
|
+
<div class="countdown-bar">
|
|
281
|
+
<div class="countdown-bar-fill" id="countdown-bar"></div>
|
|
282
|
+
</div>
|
|
283
|
+
<div class="countdown" id="countdown-text">
|
|
284
|
+
Waiting for code generation...
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
287
|
+
</div>
|
|
288
|
+
|
|
289
|
+
<!-- API Client Panel -->
|
|
290
|
+
<div class="demo-panel">
|
|
291
|
+
<h2>
|
|
292
|
+
<span class="material-icons">http</span>
|
|
293
|
+
API Client Test
|
|
294
|
+
</h2>
|
|
295
|
+
|
|
296
|
+
<div class="info-box">
|
|
297
|
+
<h3>API Authentication</h3>
|
|
298
|
+
<p>
|
|
299
|
+
The TOTP client automatically adds the current code to the
|
|
300
|
+
<code>Authorization: Bearer</code> header for each request.
|
|
301
|
+
</p>
|
|
302
|
+
</div>
|
|
303
|
+
|
|
304
|
+
<div class="api-test">
|
|
305
|
+
<h3>Test API Request</h3>
|
|
306
|
+
<p>
|
|
307
|
+
Test the TOTP authentication by making a request to the LDAP REST
|
|
308
|
+
API.
|
|
309
|
+
</p>
|
|
310
|
+
|
|
311
|
+
<div class="config-field">
|
|
312
|
+
<label for="api-endpoint">API Endpoint</label>
|
|
313
|
+
<input
|
|
314
|
+
type="text"
|
|
315
|
+
id="api-endpoint"
|
|
316
|
+
value="/api/hello"
|
|
317
|
+
placeholder="/api/v1/ldap/users"
|
|
318
|
+
/>
|
|
319
|
+
</div>
|
|
320
|
+
|
|
321
|
+
<div class="button-group">
|
|
322
|
+
<button id="btn-test-api" class="demo-button">
|
|
323
|
+
<span class="material-icons" style="vertical-align: middle">
|
|
324
|
+
send
|
|
325
|
+
</span>
|
|
326
|
+
Test Request
|
|
327
|
+
</button>
|
|
328
|
+
</div>
|
|
329
|
+
|
|
330
|
+
<div id="api-response-container" style="display: none">
|
|
331
|
+
<h4>Response:</h4>
|
|
332
|
+
<div id="api-response" class="api-response"></div>
|
|
333
|
+
</div>
|
|
334
|
+
</div>
|
|
335
|
+
</div>
|
|
336
|
+
</div>
|
|
337
|
+
|
|
338
|
+
<!-- Usage Examples Panel -->
|
|
339
|
+
<div class="demo-panel" style="margin-top: 20px">
|
|
340
|
+
<h2>
|
|
341
|
+
<span class="material-icons">code</span>
|
|
342
|
+
Usage Examples
|
|
343
|
+
</h2>
|
|
344
|
+
|
|
345
|
+
<h3>Installation</h3>
|
|
346
|
+
<div class="api-response" style="margin-bottom: 20px">
|
|
347
|
+
npm install ldap-rest # or yarn add ldap-rest
|
|
348
|
+
</div>
|
|
349
|
+
|
|
350
|
+
<h3>Import the TOTP Client</h3>
|
|
351
|
+
<div class="api-response" style="margin-bottom: 20px">
|
|
352
|
+
import { generateTotp, TotpAuthClient } from
|
|
353
|
+
'ldap-rest/browser-shared-utils-totp';
|
|
354
|
+
</div>
|
|
355
|
+
|
|
356
|
+
<h3>Generate TOTP Code</h3>
|
|
357
|
+
<div class="api-response" style="margin-bottom: 20px">
|
|
358
|
+
// Generate a TOTP code const code = await generateTotp({ secret:
|
|
359
|
+
'JBSWY3DPEHPK3PXP', digits: 6, step: 30 }); console.log('Current TOTP
|
|
360
|
+
code:', code);
|
|
361
|
+
</div>
|
|
362
|
+
|
|
363
|
+
<h3>Use TotpAuthClient for API Requests</h3>
|
|
364
|
+
<div class="api-response" style="margin-bottom: 20px">
|
|
365
|
+
// Create authenticated client const client = new TotpAuthClient({
|
|
366
|
+
secret: 'JBSWY3DPEHPK3PXP', digits: 6, step: 30 }); // Make
|
|
367
|
+
authenticated requests const users = await
|
|
368
|
+
client.get('/api/v1/ldap/users'); const userData = await users.json();
|
|
369
|
+
// POST with automatic authentication await
|
|
370
|
+
client.post('/api/v1/ldap/users', { uid: 'user1', mail:
|
|
371
|
+
'user1@example.com' }); // Other methods: put, patch, delete await
|
|
372
|
+
client.put('/api/v1/ldap/users/user1', { mail: 'new@example.com' });
|
|
373
|
+
await client.delete('/api/v1/ldap/users/user1');
|
|
374
|
+
</div>
|
|
375
|
+
|
|
376
|
+
<h3>Helper Functions</h3>
|
|
377
|
+
<div class="api-response">
|
|
378
|
+
import { getRemainingSeconds, isValidBase32 } from
|
|
379
|
+
'ldap-rest/browser-shared-utils-totp'; // Check how much time until
|
|
380
|
+
next code const remaining = getRemainingSeconds(30); console.log(`Code
|
|
381
|
+
expires in ${remaining} seconds`); // Validate Base32 secret if
|
|
382
|
+
(isValidBase32('JBSWY3DPEHPK3PXP')) { console.log('Valid secret!'); }
|
|
383
|
+
</div>
|
|
384
|
+
</div>
|
|
385
|
+
</div>
|
|
386
|
+
|
|
387
|
+
<script type="module">
|
|
388
|
+
import {
|
|
389
|
+
generateTotp,
|
|
390
|
+
TotpAuthClient,
|
|
391
|
+
getRemainingSeconds,
|
|
392
|
+
isValidBase32,
|
|
393
|
+
} from '/static/browser/shared/utils/totp.js';
|
|
394
|
+
import { showStatus } from '/static/browser/shared/components/StatusMessage.js';
|
|
395
|
+
|
|
396
|
+
const secretInput = document.getElementById('secret-input');
|
|
397
|
+
const digitsInput = document.getElementById('digits-input');
|
|
398
|
+
const stepInput = document.getElementById('step-input');
|
|
399
|
+
const totpCodeEl = document.getElementById('totp-code');
|
|
400
|
+
const countdownBar = document.getElementById('countdown-bar');
|
|
401
|
+
const countdownText = document.getElementById('countdown-text');
|
|
402
|
+
const btnGenerate = document.getElementById('btn-generate');
|
|
403
|
+
const btnCopy = document.getElementById('btn-copy');
|
|
404
|
+
const btnTestApi = document.getElementById('btn-test-api');
|
|
405
|
+
const apiEndpoint = document.getElementById('api-endpoint');
|
|
406
|
+
const apiResponseContainer = document.getElementById(
|
|
407
|
+
'api-response-container'
|
|
408
|
+
);
|
|
409
|
+
const apiResponse = document.getElementById('api-response');
|
|
410
|
+
|
|
411
|
+
let intervalId = null;
|
|
412
|
+
let currentCode = '';
|
|
413
|
+
|
|
414
|
+
async function updateTotpCode() {
|
|
415
|
+
const secret = secretInput.value.trim().toUpperCase();
|
|
416
|
+
const digits = parseInt(digitsInput.value);
|
|
417
|
+
const step = parseInt(stepInput.value);
|
|
418
|
+
|
|
419
|
+
if (!secret) {
|
|
420
|
+
showStatus('error', 'Please enter a Base32 secret');
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (!isValidBase32(secret)) {
|
|
425
|
+
showStatus('error', 'Invalid Base32 secret (use A-Z and 2-7)');
|
|
426
|
+
totpCodeEl.textContent = 'ERROR';
|
|
427
|
+
totpCodeEl.classList.add('expired');
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
try {
|
|
432
|
+
const code = await generateTotp({ secret, digits, step });
|
|
433
|
+
currentCode = code;
|
|
434
|
+
totpCodeEl.textContent = code;
|
|
435
|
+
totpCodeEl.classList.remove('expired');
|
|
436
|
+
|
|
437
|
+
// Update countdown
|
|
438
|
+
const remaining = getRemainingSeconds(step);
|
|
439
|
+
const percentage = (remaining / step) * 100;
|
|
440
|
+
countdownBar.style.width = `${percentage}%`;
|
|
441
|
+
countdownText.textContent = `Expires in ${remaining} seconds`;
|
|
442
|
+
|
|
443
|
+
// Mark as expired if less than 3 seconds
|
|
444
|
+
if (remaining <= 3) {
|
|
445
|
+
totpCodeEl.classList.add('expired');
|
|
446
|
+
}
|
|
447
|
+
} catch (error) {
|
|
448
|
+
showStatus('error', `Error generating code: ${error.message}`);
|
|
449
|
+
totpCodeEl.textContent = 'ERROR';
|
|
450
|
+
totpCodeEl.classList.add('expired');
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function startAutoRefresh() {
|
|
455
|
+
if (intervalId) {
|
|
456
|
+
clearInterval(intervalId);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
updateTotpCode();
|
|
460
|
+
intervalId = setInterval(updateTotpCode, 1000);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
btnGenerate.addEventListener('click', () => {
|
|
464
|
+
startAutoRefresh();
|
|
465
|
+
showStatus('success', 'TOTP code generated and auto-refresh started');
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
btnCopy.addEventListener('click', async () => {
|
|
469
|
+
if (
|
|
470
|
+
currentCode &&
|
|
471
|
+
currentCode !== '------' &&
|
|
472
|
+
currentCode !== 'ERROR'
|
|
473
|
+
) {
|
|
474
|
+
try {
|
|
475
|
+
await navigator.clipboard.writeText(currentCode);
|
|
476
|
+
showStatus('success', `Code ${currentCode} copied to clipboard!`);
|
|
477
|
+
} catch (error) {
|
|
478
|
+
showStatus('error', 'Failed to copy to clipboard');
|
|
479
|
+
}
|
|
480
|
+
} else {
|
|
481
|
+
showStatus('error', 'No valid code to copy');
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
btnTestApi.addEventListener('click', async () => {
|
|
486
|
+
const secret = secretInput.value.trim().toUpperCase();
|
|
487
|
+
const digits = parseInt(digitsInput.value);
|
|
488
|
+
const step = parseInt(stepInput.value);
|
|
489
|
+
const endpoint = apiEndpoint.value;
|
|
490
|
+
|
|
491
|
+
if (!secret) {
|
|
492
|
+
showStatus('error', 'Please enter a Base32 secret first');
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (!isValidBase32(secret)) {
|
|
497
|
+
showStatus('error', 'Invalid Base32 secret');
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
try {
|
|
502
|
+
showStatus('info', 'Sending request...');
|
|
503
|
+
|
|
504
|
+
const client = new TotpAuthClient({ secret, digits, step });
|
|
505
|
+
const response = await client.get(endpoint);
|
|
506
|
+
|
|
507
|
+
apiResponseContainer.style.display = 'block';
|
|
508
|
+
|
|
509
|
+
const data = await response.json();
|
|
510
|
+
apiResponse.textContent = JSON.stringify(data, null, 2);
|
|
511
|
+
apiResponse.classList.remove('error');
|
|
512
|
+
|
|
513
|
+
if (response.ok) {
|
|
514
|
+
showStatus(
|
|
515
|
+
'success',
|
|
516
|
+
`API request successful (${response.status})`
|
|
517
|
+
);
|
|
518
|
+
} else {
|
|
519
|
+
showStatus('warning', `API returned status ${response.status}`);
|
|
520
|
+
apiResponse.classList.add('error');
|
|
521
|
+
}
|
|
522
|
+
} catch (error) {
|
|
523
|
+
apiResponseContainer.style.display = 'block';
|
|
524
|
+
apiResponse.textContent = `Error: ${error.message}`;
|
|
525
|
+
apiResponse.classList.add('error');
|
|
526
|
+
showStatus('error', `Request failed: ${error.message}`);
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
// Auto-start on load
|
|
531
|
+
startAutoRefresh();
|
|
532
|
+
|
|
533
|
+
// Clean up on page unload
|
|
534
|
+
window.addEventListener('beforeunload', () => {
|
|
535
|
+
if (intervalId) {
|
|
536
|
+
clearInterval(intervalId);
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
</script>
|
|
540
|
+
</body>
|
|
541
|
+
</html>
|