create-tinny-backend 1.0.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/index.js +107 -0
- package/package.json +38 -0
- package/template/admin/auth.ts +85 -0
- package/template/admin/monitor.ts +102 -0
- package/template/admin/routes.ts +192 -0
- package/template/app/index.ts +6 -0
- package/template/lib/CreateSrv.ts +9 -0
- package/template/package.json +48 -0
- package/template/public/admin/index.html +2012 -0
- package/template/public/doc/index.html +1651 -0
- package/template/public/example/default.html +437 -0
- package/template/public/imgs/logo-square.png +0 -0
- package/template/public/imgs/logo.png +0 -0
- package/template/public/imgs/no-bg-logo.png +0 -0
- package/template/public/login/css/style.css +109 -0
- package/template/public/login/index.html +883 -0
- package/template/public/login/new-password.html +1173 -0
- package/template/public/status/401.html +649 -0
- package/template/public/status/404.html +668 -0
- package/template/tsconfig.json +44 -0
|
@@ -0,0 +1,883 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" data-theme="light">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
|
6
|
+
<title>Admin Login | Server Dashboard</title>
|
|
7
|
+
<link rel="shortcut icon" href="/imgs/logo.png" type="image/x-icon">
|
|
8
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
|
9
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
10
|
+
|
|
11
|
+
<style>
|
|
12
|
+
:root {
|
|
13
|
+
--bg: #f4f6fa;
|
|
14
|
+
--text: #1e293b;
|
|
15
|
+
--text-secondary: #475569;
|
|
16
|
+
--text-muted: #64748b;
|
|
17
|
+
--border: #e2e8f0;
|
|
18
|
+
--card-bg: #ffffff;
|
|
19
|
+
--card-border: #e9eef3;
|
|
20
|
+
--input-bg: #f8fafc;
|
|
21
|
+
--input-border: #e2e8f0;
|
|
22
|
+
--input-focus: #2563eb;
|
|
23
|
+
--btn-bg: #2563eb;
|
|
24
|
+
--btn-hover: #1d4ed8;
|
|
25
|
+
--btn-text: #ffffff;
|
|
26
|
+
--heading: #0f172a;
|
|
27
|
+
--shadow-sm: 0 1px 3px rgba(0,0,0,0.06);
|
|
28
|
+
--shadow-md: 0 4px 12px rgba(0,0,0,0.08);
|
|
29
|
+
--shadow-lg: 0 12px 40px rgba(0,0,0,0.1);
|
|
30
|
+
--toggle-bg: #f1f5f9;
|
|
31
|
+
--badge-bg: #dbeafe;
|
|
32
|
+
--badge-text: #1e40af;
|
|
33
|
+
--success-bg: #dcfce7;
|
|
34
|
+
--success-text: #166534;
|
|
35
|
+
--error-bg: #fee2e2;
|
|
36
|
+
--error-text: #991b1b;
|
|
37
|
+
--details-bg: #f9fafc;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
[data-theme="dark"] {
|
|
41
|
+
--bg: #1a1a1a;
|
|
42
|
+
--text: #d4d4d4;
|
|
43
|
+
--text-secondary: #a0a0a0;
|
|
44
|
+
--text-muted: #808080;
|
|
45
|
+
--border: #3a3a3a;
|
|
46
|
+
--card-bg: #2a2a2a;
|
|
47
|
+
--card-border: #3a3a3a;
|
|
48
|
+
--input-bg: #1e1e1e;
|
|
49
|
+
--input-border: #3a3a3a;
|
|
50
|
+
--input-focus: #60a5fa;
|
|
51
|
+
--btn-bg: #3b82f6;
|
|
52
|
+
--btn-hover: #2563eb;
|
|
53
|
+
--btn-text: #ffffff;
|
|
54
|
+
--heading: #e0e0e0;
|
|
55
|
+
--shadow-sm: 0 1px 3px rgba(0,0,0,0.3);
|
|
56
|
+
--shadow-md: 0 4px 12px rgba(0,0,0,0.4);
|
|
57
|
+
--shadow-lg: 0 12px 40px rgba(0,0,0,0.5);
|
|
58
|
+
--toggle-bg: #333333;
|
|
59
|
+
--badge-bg: #2a2a2a;
|
|
60
|
+
--badge-text: #a0a0a0;
|
|
61
|
+
--success-bg: #1a3a2a;
|
|
62
|
+
--success-text: #48bb78;
|
|
63
|
+
--error-bg: #3a1a1a;
|
|
64
|
+
--error-text: #fc8181;
|
|
65
|
+
--details-bg: #2a2a2a;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
* {
|
|
69
|
+
margin: 0;
|
|
70
|
+
padding: 0;
|
|
71
|
+
box-sizing: border-box;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
body {
|
|
75
|
+
background: var(--bg);
|
|
76
|
+
font-family: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
|
|
77
|
+
min-height: 100vh;
|
|
78
|
+
color: var(--text);
|
|
79
|
+
transition: background 0.3s ease, color 0.3s ease;
|
|
80
|
+
display: flex;
|
|
81
|
+
align-items: center;
|
|
82
|
+
justify-content: center;
|
|
83
|
+
padding: 1.5rem;
|
|
84
|
+
position: relative;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/* Background decoration */
|
|
88
|
+
.bg-decoration {
|
|
89
|
+
position: fixed;
|
|
90
|
+
inset: 0;
|
|
91
|
+
pointer-events: none;
|
|
92
|
+
overflow: hidden;
|
|
93
|
+
z-index: 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.bg-decoration .circle {
|
|
97
|
+
position: absolute;
|
|
98
|
+
border-radius: 50%;
|
|
99
|
+
background: radial-gradient(circle, rgba(37, 99, 235, 0.05) 0%, transparent 70%);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.bg-decoration .circle:nth-child(1) {
|
|
103
|
+
width: 400px;
|
|
104
|
+
height: 400px;
|
|
105
|
+
top: -100px;
|
|
106
|
+
right: -100px;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.bg-decoration .circle:nth-child(2) {
|
|
110
|
+
width: 300px;
|
|
111
|
+
height: 300px;
|
|
112
|
+
bottom: -50px;
|
|
113
|
+
left: -50px;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.login-container {
|
|
117
|
+
width: 100%;
|
|
118
|
+
max-width: 440px;
|
|
119
|
+
animation: fadeIn 0.6s ease;
|
|
120
|
+
position: relative;
|
|
121
|
+
z-index: 1;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@keyframes fadeIn {
|
|
125
|
+
from {
|
|
126
|
+
opacity: 0;
|
|
127
|
+
transform: translateY(20px);
|
|
128
|
+
}
|
|
129
|
+
to {
|
|
130
|
+
opacity: 1;
|
|
131
|
+
transform: translateY(0);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/* Login Card */
|
|
136
|
+
.login-card {
|
|
137
|
+
background: var(--card-bg);
|
|
138
|
+
border: 1px solid var(--card-border);
|
|
139
|
+
border-radius: 1.3rem;
|
|
140
|
+
padding: 2.5rem 2rem;
|
|
141
|
+
box-shadow: var(--shadow-lg);
|
|
142
|
+
transition: all 0.3s ease;
|
|
143
|
+
backdrop-filter: blur(10px);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.login-card:hover {
|
|
147
|
+
border-color: var(--input-focus);
|
|
148
|
+
box-shadow: var(--shadow-lg), 0 0 0 1px var(--input-focus);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/* Header with theme toggle */
|
|
152
|
+
.login-header {
|
|
153
|
+
display: flex;
|
|
154
|
+
justify-content: space-between;
|
|
155
|
+
align-items: center;
|
|
156
|
+
margin-bottom: 1.5rem;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.theme-toggle {
|
|
160
|
+
background: var(--toggle-bg);
|
|
161
|
+
border: 1px solid var(--border);
|
|
162
|
+
color: var(--text);
|
|
163
|
+
width: 36px;
|
|
164
|
+
height: 36px;
|
|
165
|
+
border-radius: 0.6rem;
|
|
166
|
+
display: flex;
|
|
167
|
+
align-items: center;
|
|
168
|
+
justify-content: center;
|
|
169
|
+
cursor: pointer;
|
|
170
|
+
font-size: 1rem;
|
|
171
|
+
transition: all 0.2s ease;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.theme-toggle:hover {
|
|
175
|
+
background: var(--input-focus);
|
|
176
|
+
color: white;
|
|
177
|
+
border-color: var(--input-focus);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/* Logo Section */
|
|
181
|
+
.logo-section {
|
|
182
|
+
text-align: center;
|
|
183
|
+
margin-bottom: 2rem;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.logo-wrapper {
|
|
187
|
+
display: inline-flex;
|
|
188
|
+
align-items: center;
|
|
189
|
+
justify-content: center;
|
|
190
|
+
width: 80px;
|
|
191
|
+
height: 80px;
|
|
192
|
+
background: white;
|
|
193
|
+
border: 2px solid var(--border);
|
|
194
|
+
border-radius: 1.2rem;
|
|
195
|
+
margin-bottom: 1.2rem;
|
|
196
|
+
transition: all 0.3s ease;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.logo-wrapper:hover {
|
|
200
|
+
border-color: var(--input-focus);
|
|
201
|
+
box-shadow: 0 0 30px rgba(37, 99, 235, 0.1);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.logo-wrapper img {
|
|
205
|
+
width: 50px;
|
|
206
|
+
height: auto;
|
|
207
|
+
object-fit: contain;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.login-title {
|
|
211
|
+
font-size: 1.8rem;
|
|
212
|
+
font-weight: 700;
|
|
213
|
+
color: var(--heading);
|
|
214
|
+
letter-spacing: -0.3px;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.login-subtitle {
|
|
218
|
+
color: var(--text-secondary);
|
|
219
|
+
font-size: 0.9rem;
|
|
220
|
+
margin-top: 0.3rem;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.login-subtitle i {
|
|
224
|
+
color: var(--input-focus);
|
|
225
|
+
margin-right: 0.3rem;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/* Form Styles */
|
|
229
|
+
.form-group {
|
|
230
|
+
margin-bottom: 1.2rem;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.form-group label {
|
|
234
|
+
display: block;
|
|
235
|
+
margin-bottom: 0.5rem;
|
|
236
|
+
font-size: 0.85rem;
|
|
237
|
+
font-weight: 600;
|
|
238
|
+
color: var(--text-secondary);
|
|
239
|
+
letter-spacing: 0.3px;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.form-group label i {
|
|
243
|
+
margin-right: 0.5rem;
|
|
244
|
+
color: var(--input-focus);
|
|
245
|
+
font-size: 0.8rem;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.input-wrapper {
|
|
249
|
+
position: relative;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.input-wrapper .input-icon {
|
|
253
|
+
position: absolute;
|
|
254
|
+
left: 1rem;
|
|
255
|
+
top: 50%;
|
|
256
|
+
transform: translateY(-50%);
|
|
257
|
+
color: var(--text-muted);
|
|
258
|
+
font-size: 0.9rem;
|
|
259
|
+
transition: color 0.3s ease;
|
|
260
|
+
pointer-events: none;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.input-wrapper input {
|
|
264
|
+
width: 100%;
|
|
265
|
+
padding: 0.9rem 1rem 0.9rem 2.8rem;
|
|
266
|
+
background: var(--input-bg);
|
|
267
|
+
color: var(--text);
|
|
268
|
+
border: 1px solid var(--input-border);
|
|
269
|
+
border-radius: 0.8rem;
|
|
270
|
+
font-size: 0.95rem;
|
|
271
|
+
font-family: 'Inter', sans-serif;
|
|
272
|
+
transition: all 0.3s ease;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.input-wrapper input::placeholder {
|
|
276
|
+
color: var(--text-muted);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.input-wrapper input:focus {
|
|
280
|
+
outline: none;
|
|
281
|
+
border-color: var(--input-focus);
|
|
282
|
+
background: var(--card-bg);
|
|
283
|
+
box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.1);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.input-wrapper input:focus ~ .input-icon {
|
|
287
|
+
color: var(--input-focus);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.input-wrapper input:disabled {
|
|
291
|
+
opacity: 0.5;
|
|
292
|
+
cursor: not-allowed;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.password-toggle {
|
|
296
|
+
position: absolute;
|
|
297
|
+
right: 1rem;
|
|
298
|
+
top: 50%;
|
|
299
|
+
transform: translateY(-50%);
|
|
300
|
+
background: none;
|
|
301
|
+
border: none;
|
|
302
|
+
color: var(--text-muted);
|
|
303
|
+
cursor: pointer;
|
|
304
|
+
font-size: 0.9rem;
|
|
305
|
+
transition: color 0.3s ease;
|
|
306
|
+
padding: 0.2rem;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.password-toggle:hover {
|
|
310
|
+
color: var(--text);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/* Login Button */
|
|
314
|
+
.login-btn {
|
|
315
|
+
width: 100%;
|
|
316
|
+
background: var(--btn-bg);
|
|
317
|
+
color: var(--btn-text);
|
|
318
|
+
border: none;
|
|
319
|
+
border-radius: 0.8rem;
|
|
320
|
+
padding: 0.9rem;
|
|
321
|
+
font-size: 0.95rem;
|
|
322
|
+
font-weight: 600;
|
|
323
|
+
font-family: 'Inter', sans-serif;
|
|
324
|
+
cursor: pointer;
|
|
325
|
+
transition: all 0.3s ease;
|
|
326
|
+
display: flex;
|
|
327
|
+
align-items: center;
|
|
328
|
+
justify-content: center;
|
|
329
|
+
gap: 0.6rem;
|
|
330
|
+
margin-top: 0.5rem;
|
|
331
|
+
position: relative;
|
|
332
|
+
overflow: hidden;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.login-btn::before {
|
|
336
|
+
content: '';
|
|
337
|
+
position: absolute;
|
|
338
|
+
inset: 0;
|
|
339
|
+
background: linear-gradient(135deg, rgba(255,255,255,0.1) 0%, transparent 50%);
|
|
340
|
+
pointer-events: none;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
.login-btn:hover:not(:disabled) {
|
|
344
|
+
background: var(--btn-hover);
|
|
345
|
+
transform: translateY(-2px);
|
|
346
|
+
box-shadow: 0 8px 24px rgba(37, 99, 235, 0.3);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.login-btn:active:not(:disabled) {
|
|
350
|
+
transform: translateY(0);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
.login-btn:disabled {
|
|
354
|
+
opacity: 0.6;
|
|
355
|
+
cursor: not-allowed;
|
|
356
|
+
transform: none;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
.login-btn i {
|
|
360
|
+
font-size: 1rem;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/* Change Password Button - New */
|
|
364
|
+
.change-pwd-btn {
|
|
365
|
+
width: 100%;
|
|
366
|
+
background: var(--toggle-bg);
|
|
367
|
+
color: var(--text);
|
|
368
|
+
border: 1px solid var(--border);
|
|
369
|
+
border-radius: 0.8rem;
|
|
370
|
+
padding: 0.85rem;
|
|
371
|
+
font-size: 0.9rem;
|
|
372
|
+
font-weight: 500;
|
|
373
|
+
font-family: 'Inter', sans-serif;
|
|
374
|
+
cursor: pointer;
|
|
375
|
+
transition: all 0.3s ease;
|
|
376
|
+
display: flex;
|
|
377
|
+
align-items: center;
|
|
378
|
+
justify-content: center;
|
|
379
|
+
gap: 0.6rem;
|
|
380
|
+
margin-top: 0.6rem;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.change-pwd-btn:hover {
|
|
384
|
+
background: var(--border);
|
|
385
|
+
border-color: var(--input-focus);
|
|
386
|
+
transform: translateY(-2px);
|
|
387
|
+
box-shadow: var(--shadow-md);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
.change-pwd-btn i {
|
|
391
|
+
color: var(--input-focus);
|
|
392
|
+
font-size: 0.95rem;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/* Message */
|
|
396
|
+
.message {
|
|
397
|
+
margin-top: 1rem;
|
|
398
|
+
padding: 0.8rem 1rem;
|
|
399
|
+
border-radius: 0.8rem;
|
|
400
|
+
text-align: center;
|
|
401
|
+
font-size: 0.85rem;
|
|
402
|
+
display: none;
|
|
403
|
+
animation: slideDown 0.3s ease;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
.message.show {
|
|
407
|
+
display: block;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
@keyframes slideDown {
|
|
411
|
+
from {
|
|
412
|
+
opacity: 0;
|
|
413
|
+
transform: translateY(-10px);
|
|
414
|
+
}
|
|
415
|
+
to {
|
|
416
|
+
opacity: 1;
|
|
417
|
+
transform: translateY(0);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
.message.success {
|
|
422
|
+
background: var(--success-bg);
|
|
423
|
+
color: var(--success-text);
|
|
424
|
+
border: 1px solid var(--success-text);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
.message.success i {
|
|
428
|
+
margin-right: 0.5rem;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
.message.error {
|
|
432
|
+
background: var(--error-bg);
|
|
433
|
+
color: var(--error-text);
|
|
434
|
+
border: 1px solid var(--error-text);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
.message.error i {
|
|
438
|
+
margin-right: 0.5rem;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
.message.info {
|
|
442
|
+
background: var(--badge-bg);
|
|
443
|
+
color: var(--badge-text);
|
|
444
|
+
border: 1px solid var(--badge-text);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
.message.info i {
|
|
448
|
+
margin-right: 0.5rem;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/* Decorative elements */
|
|
452
|
+
.decorative-line {
|
|
453
|
+
display: flex;
|
|
454
|
+
align-items: center;
|
|
455
|
+
gap: 1rem;
|
|
456
|
+
margin: 1.5rem 0 1rem;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
.decorative-line::before,
|
|
460
|
+
.decorative-line::after {
|
|
461
|
+
content: '';
|
|
462
|
+
flex: 1;
|
|
463
|
+
height: 1px;
|
|
464
|
+
background: linear-gradient(to right, transparent, var(--border), transparent);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
.decorative-line span {
|
|
468
|
+
color: var(--text-muted);
|
|
469
|
+
font-size: 0.7rem;
|
|
470
|
+
text-transform: uppercase;
|
|
471
|
+
letter-spacing: 0.5px;
|
|
472
|
+
font-weight: 600;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/* Footer */
|
|
476
|
+
.footer {
|
|
477
|
+
text-align: center;
|
|
478
|
+
margin-top: 1.8rem;
|
|
479
|
+
padding-top: 1.5rem;
|
|
480
|
+
border-top: 1px solid var(--border);
|
|
481
|
+
color: var(--text-muted);
|
|
482
|
+
font-size: 0.8rem;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
.footer a {
|
|
486
|
+
color: var(--input-focus);
|
|
487
|
+
text-decoration: none;
|
|
488
|
+
font-weight: 500;
|
|
489
|
+
transition: color 0.3s ease;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
.footer a:hover {
|
|
493
|
+
color: var(--btn-hover);
|
|
494
|
+
text-decoration: underline;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
.footer i {
|
|
498
|
+
margin: 0 0.3rem;
|
|
499
|
+
font-size: 0.7rem;
|
|
500
|
+
color: var(--text-muted);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/* Badge */
|
|
504
|
+
.badge {
|
|
505
|
+
display: inline-block;
|
|
506
|
+
background: var(--badge-bg);
|
|
507
|
+
color: var(--badge-text);
|
|
508
|
+
padding: 0.2rem 0.6rem;
|
|
509
|
+
border-radius: 30px;
|
|
510
|
+
font-size: 0.65rem;
|
|
511
|
+
font-weight: 700;
|
|
512
|
+
border: 1px solid var(--border);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/* Loading spinner for button */
|
|
516
|
+
.spinner {
|
|
517
|
+
display: inline-block;
|
|
518
|
+
width: 16px;
|
|
519
|
+
height: 16px;
|
|
520
|
+
border: 2px solid rgba(255, 255, 255, 0.3);
|
|
521
|
+
border-top-color: #fff;
|
|
522
|
+
border-radius: 50%;
|
|
523
|
+
animation: spin 0.6s linear infinite;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
@keyframes spin {
|
|
527
|
+
to {
|
|
528
|
+
transform: rotate(360deg);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/* Shake animation for error */
|
|
533
|
+
@keyframes shake {
|
|
534
|
+
0%, 100% { transform: translateX(0); }
|
|
535
|
+
10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); }
|
|
536
|
+
20%, 40%, 60%, 80% { transform: translateX(5px); }
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
.shake {
|
|
540
|
+
animation: shake 0.5s ease;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/* Responsive */
|
|
544
|
+
@media (max-width: 480px) {
|
|
545
|
+
body {
|
|
546
|
+
padding: 1rem;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
.login-card {
|
|
550
|
+
padding: 1.8rem 1.2rem;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
.login-title {
|
|
554
|
+
font-size: 1.5rem;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
.logo-wrapper {
|
|
558
|
+
width: 64px;
|
|
559
|
+
height: 64px;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
.logo-wrapper img {
|
|
563
|
+
width: 40px;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
.input-wrapper input {
|
|
567
|
+
padding: 0.8rem 1rem 0.8rem 2.5rem;
|
|
568
|
+
font-size: 0.9rem;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
.login-header {
|
|
572
|
+
margin-bottom: 1rem;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
</style>
|
|
576
|
+
</head>
|
|
577
|
+
<body>
|
|
578
|
+
|
|
579
|
+
<!-- Background Decoration -->
|
|
580
|
+
<div class="bg-decoration">
|
|
581
|
+
<div class="circle"></div>
|
|
582
|
+
<div class="circle"></div>
|
|
583
|
+
</div>
|
|
584
|
+
|
|
585
|
+
<div class="login-container">
|
|
586
|
+
<div class="login-card">
|
|
587
|
+
|
|
588
|
+
<!-- Header with Theme Toggle -->
|
|
589
|
+
<div class="login-header">
|
|
590
|
+
<div></div>
|
|
591
|
+
<button class="theme-toggle" id="themeToggle" title="Toggle dark/light mode">
|
|
592
|
+
<i class="fas fa-moon"></i>
|
|
593
|
+
</button>
|
|
594
|
+
</div>
|
|
595
|
+
|
|
596
|
+
<!-- Logo Section -->
|
|
597
|
+
<div class="logo-section">
|
|
598
|
+
<div class="logo-wrapper">
|
|
599
|
+
<img src="/imgs/logo-square.png" alt="Logo" onerror="this.style.display='none';this.parentElement.innerHTML='<i class=\'fas fa-cube\' style=\'font-size:2rem;color:var(--input-focus);\'></i>'">
|
|
600
|
+
</div>
|
|
601
|
+
<h1 class="login-title">Welcome Back</h1>
|
|
602
|
+
<p class="login-subtitle">
|
|
603
|
+
<i class="fas fa-shield-alt"></i>
|
|
604
|
+
Sign in to your admin dashboard
|
|
605
|
+
</p>
|
|
606
|
+
<div style="margin-top: 0.5rem;">
|
|
607
|
+
<span class="badge"><i class="fas fa-lock"></i> Secure Access</span>
|
|
608
|
+
</div>
|
|
609
|
+
</div>
|
|
610
|
+
|
|
611
|
+
<!-- Login Form -->
|
|
612
|
+
<form id="loginForm" autocomplete="off">
|
|
613
|
+
<div class="form-group">
|
|
614
|
+
<label for="username">
|
|
615
|
+
<i class="fas fa-user"></i> Username
|
|
616
|
+
</label>
|
|
617
|
+
<div class="input-wrapper">
|
|
618
|
+
<input
|
|
619
|
+
type="text"
|
|
620
|
+
id="username"
|
|
621
|
+
placeholder="Enter your username"
|
|
622
|
+
required
|
|
623
|
+
autocomplete="username"
|
|
624
|
+
>
|
|
625
|
+
<i class="fas fa-user input-icon"></i>
|
|
626
|
+
</div>
|
|
627
|
+
</div>
|
|
628
|
+
|
|
629
|
+
<div class="form-group">
|
|
630
|
+
<label for="password">
|
|
631
|
+
<i class="fas fa-lock"></i> Password
|
|
632
|
+
</label>
|
|
633
|
+
<div class="input-wrapper">
|
|
634
|
+
<input
|
|
635
|
+
type="password"
|
|
636
|
+
id="password"
|
|
637
|
+
placeholder="Enter your password"
|
|
638
|
+
required
|
|
639
|
+
autocomplete="current-password"
|
|
640
|
+
>
|
|
641
|
+
<i class="fas fa-key input-icon"></i>
|
|
642
|
+
<button type="button" class="password-toggle" id="togglePassword" aria-label="Toggle password visibility">
|
|
643
|
+
<i class="fas fa-eye"></i>
|
|
644
|
+
</button>
|
|
645
|
+
</div>
|
|
646
|
+
</div>
|
|
647
|
+
|
|
648
|
+
<button type="submit" class="login-btn" id="loginBtn">
|
|
649
|
+
<i class="fas fa-sign-in-alt"></i>
|
|
650
|
+
<span>Sign In</span>
|
|
651
|
+
</button>
|
|
652
|
+
|
|
653
|
+
<div id="message" class="message"></div>
|
|
654
|
+
</form>
|
|
655
|
+
|
|
656
|
+
<!-- Change Password Button - NEW -->
|
|
657
|
+
<button class="change-pwd-btn" id="changePwdBtn">
|
|
658
|
+
<i class="fas fa-key"></i>
|
|
659
|
+
<span>Change Password</span>
|
|
660
|
+
<i class="fas fa-arrow-right" style="font-size: 0.8rem; margin-left: auto;"></i>
|
|
661
|
+
</button>
|
|
662
|
+
|
|
663
|
+
<!-- Decorative Line -->
|
|
664
|
+
<div class="decorative-line">
|
|
665
|
+
<span>secure admin access</span>
|
|
666
|
+
</div>
|
|
667
|
+
|
|
668
|
+
<!-- Footer -->
|
|
669
|
+
<div class="footer">
|
|
670
|
+
<i class="fas fa-lock"></i>
|
|
671
|
+
Read the documentation to get login credentials
|
|
672
|
+
<i class="fas fa-chevron-right"></i>
|
|
673
|
+
<a href="/docs">docs</a>
|
|
674
|
+
</div>
|
|
675
|
+
|
|
676
|
+
</div>
|
|
677
|
+
</div>
|
|
678
|
+
|
|
679
|
+
<script>
|
|
680
|
+
(function() {
|
|
681
|
+
// ===== THEME TOGGLE =====
|
|
682
|
+
const html = document.documentElement;
|
|
683
|
+
const themeToggle = document.getElementById('themeToggle');
|
|
684
|
+
const savedTheme = localStorage.getItem('tinnybackend-theme');
|
|
685
|
+
|
|
686
|
+
if (savedTheme) {
|
|
687
|
+
html.setAttribute('data-theme', savedTheme);
|
|
688
|
+
} else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
|
689
|
+
html.setAttribute('data-theme', 'dark');
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function updateThemeUI(theme) {
|
|
693
|
+
const icon = themeToggle.querySelector('i');
|
|
694
|
+
if (theme === 'dark') {
|
|
695
|
+
icon.classList.remove('fa-moon');
|
|
696
|
+
icon.classList.add('fa-sun');
|
|
697
|
+
} else {
|
|
698
|
+
icon.classList.remove('fa-sun');
|
|
699
|
+
icon.classList.add('fa-moon');
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
updateThemeUI(html.getAttribute('data-theme'));
|
|
704
|
+
|
|
705
|
+
themeToggle.addEventListener('click', () => {
|
|
706
|
+
const currentTheme = html.getAttribute('data-theme');
|
|
707
|
+
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
|
708
|
+
html.setAttribute('data-theme', newTheme);
|
|
709
|
+
localStorage.setItem('tinnybackend-theme', newTheme);
|
|
710
|
+
updateThemeUI(newTheme);
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
if (window.matchMedia) {
|
|
714
|
+
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
|
|
715
|
+
if (!localStorage.getItem('tinnybackend-theme')) {
|
|
716
|
+
const newTheme = e.matches ? 'dark' : 'light';
|
|
717
|
+
html.setAttribute('data-theme', newTheme);
|
|
718
|
+
updateThemeUI(newTheme);
|
|
719
|
+
}
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// ===== LOGIN FORM =====
|
|
724
|
+
const loginForm = document.getElementById('loginForm');
|
|
725
|
+
const usernameInput = document.getElementById('username');
|
|
726
|
+
const passwordInput = document.getElementById('password');
|
|
727
|
+
const loginBtn = document.getElementById('loginBtn');
|
|
728
|
+
const messageDiv = document.getElementById('message');
|
|
729
|
+
const togglePassword = document.getElementById('togglePassword');
|
|
730
|
+
const changePwdBtn = document.getElementById('changePwdBtn');
|
|
731
|
+
|
|
732
|
+
// Change Password Button - Navigate to /passwd
|
|
733
|
+
changePwdBtn.addEventListener('click', () => {
|
|
734
|
+
window.location.href = '/passwd';
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
// Toggle password visibility
|
|
738
|
+
togglePassword.addEventListener('click', function() {
|
|
739
|
+
const type = passwordInput.getAttribute('type') === 'password' ? 'text' : 'password';
|
|
740
|
+
passwordInput.setAttribute('type', type);
|
|
741
|
+
this.querySelector('i').classList.toggle('fa-eye');
|
|
742
|
+
this.querySelector('i').classList.toggle('fa-eye-slash');
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
// Show message function
|
|
746
|
+
function showMessage(text, type = 'error') {
|
|
747
|
+
messageDiv.textContent = '';
|
|
748
|
+
messageDiv.className = 'message show ' + type;
|
|
749
|
+
|
|
750
|
+
const icon = document.createElement('i');
|
|
751
|
+
if (type === 'success') icon.className = 'fas fa-check-circle';
|
|
752
|
+
else if (type === 'info') icon.className = 'fas fa-info-circle';
|
|
753
|
+
else icon.className = 'fas fa-exclamation-circle';
|
|
754
|
+
|
|
755
|
+
messageDiv.appendChild(icon);
|
|
756
|
+
messageDiv.appendChild(document.createTextNode(' ' + text));
|
|
757
|
+
|
|
758
|
+
if (type === 'error') {
|
|
759
|
+
messageDiv.classList.add('shake');
|
|
760
|
+
setTimeout(() => {
|
|
761
|
+
messageDiv.classList.remove('shake');
|
|
762
|
+
}, 500);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function hideMessage() {
|
|
767
|
+
messageDiv.className = 'message';
|
|
768
|
+
messageDiv.textContent = '';
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function setLoading(isLoading) {
|
|
772
|
+
if (isLoading) {
|
|
773
|
+
loginBtn.disabled = true;
|
|
774
|
+
loginBtn.innerHTML = '<span class="spinner"></span> Signing in...';
|
|
775
|
+
} else {
|
|
776
|
+
loginBtn.disabled = false;
|
|
777
|
+
loginBtn.innerHTML = '<i class="fas fa-sign-in-alt"></i> <span>Sign In</span>';
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// Form submission
|
|
782
|
+
loginForm.addEventListener('submit', async (e) => {
|
|
783
|
+
e.preventDefault();
|
|
784
|
+
hideMessage();
|
|
785
|
+
|
|
786
|
+
const username = usernameInput.value.trim();
|
|
787
|
+
const password = passwordInput.value.trim();
|
|
788
|
+
|
|
789
|
+
if (!username || !password) {
|
|
790
|
+
showMessage('Please enter both username and password', 'error');
|
|
791
|
+
if (!username) usernameInput.focus();
|
|
792
|
+
else passwordInput.focus();
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
setLoading(true);
|
|
797
|
+
|
|
798
|
+
try {
|
|
799
|
+
const response = await fetch('/api/login', {
|
|
800
|
+
method: 'POST',
|
|
801
|
+
headers: {
|
|
802
|
+
'Content-Type': 'application/json',
|
|
803
|
+
},
|
|
804
|
+
body: JSON.stringify({ username, password }),
|
|
805
|
+
credentials: 'same-origin'
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
const contentType = response.headers.get('content-type');
|
|
809
|
+
|
|
810
|
+
if (contentType && contentType.includes('text/html')) {
|
|
811
|
+
showMessage('Login successful! Redirecting to dashboard...', 'success');
|
|
812
|
+
usernameInput.disabled = true;
|
|
813
|
+
passwordInput.disabled = true;
|
|
814
|
+
window.location.href = '/admin/';
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
try {
|
|
819
|
+
const data = await response.json();
|
|
820
|
+
|
|
821
|
+
if (response.status === 403 || data.Error === 'Access Denied') {
|
|
822
|
+
showMessage('Invalid username or password. Please try again.', 'error');
|
|
823
|
+
passwordInput.value = '';
|
|
824
|
+
passwordInput.focus();
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
if (response.ok && data.success !== false) {
|
|
829
|
+
showMessage('Login successful! Redirecting to dashboard...', 'success');
|
|
830
|
+
usernameInput.disabled = true;
|
|
831
|
+
passwordInput.disabled = true;
|
|
832
|
+
setTimeout(() => {
|
|
833
|
+
window.location.href = '/admin/';
|
|
834
|
+
}, 800);
|
|
835
|
+
} else {
|
|
836
|
+
showMessage(data.message || 'Login failed. Please try again.', 'error');
|
|
837
|
+
passwordInput.value = '';
|
|
838
|
+
passwordInput.focus();
|
|
839
|
+
}
|
|
840
|
+
} catch (jsonError) {
|
|
841
|
+
if (response.redirected) {
|
|
842
|
+
showMessage('Login successful! Redirecting to dashboard...', 'success');
|
|
843
|
+
window.location.href = '/admin/';
|
|
844
|
+
} else {
|
|
845
|
+
if (response.status === 403) {
|
|
846
|
+
showMessage('Invalid username or password. Please try again.', 'error');
|
|
847
|
+
passwordInput.value = '';
|
|
848
|
+
passwordInput.focus();
|
|
849
|
+
} else {
|
|
850
|
+
showMessage('Login failed. Please try again.', 'error');
|
|
851
|
+
passwordInput.value = '';
|
|
852
|
+
passwordInput.focus();
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
} catch (error) {
|
|
857
|
+
console.error('Login error:', error);
|
|
858
|
+
showMessage('Network error. Please check your connection and try again.', 'error');
|
|
859
|
+
} finally {
|
|
860
|
+
setLoading(false);
|
|
861
|
+
}
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
usernameInput.addEventListener('input', hideMessage);
|
|
865
|
+
passwordInput.addEventListener('input', hideMessage);
|
|
866
|
+
|
|
867
|
+
usernameInput.focus();
|
|
868
|
+
|
|
869
|
+
passwordInput.addEventListener('keydown', (e) => {
|
|
870
|
+
if (e.key === 'Enter') {
|
|
871
|
+
e.preventDefault();
|
|
872
|
+
loginForm.dispatchEvent(new Event('submit'));
|
|
873
|
+
}
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
if (window.performance && window.performance.navigation.type === 1) {
|
|
877
|
+
sessionStorage.removeItem('login_attempt');
|
|
878
|
+
}
|
|
879
|
+
})();
|
|
880
|
+
</script>
|
|
881
|
+
|
|
882
|
+
</body>
|
|
883
|
+
</html>
|