strapi-plugin-magic-link-v5 4.4.1 → 4.4.2
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/package.json
CHANGED
|
@@ -61,12 +61,18 @@ module.exports = {
|
|
|
61
61
|
orderBy: { createdAt: 'desc' },
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
+
// SQLite speichert Booleans als 0/1 - konvertiere zu echten Booleans
|
|
65
|
+
const normalizedTokens = tokens.map(token => ({
|
|
66
|
+
...token,
|
|
67
|
+
is_active: !!token.is_active // Konvertiere 0/1 zu false/true
|
|
68
|
+
}));
|
|
69
|
+
|
|
64
70
|
// Berechne den Sicherheitswert
|
|
65
71
|
const securityScore = await this.calculateSecurityScore();
|
|
66
72
|
|
|
67
73
|
// Füge den Sicherheitswert als Metadaten hinzu
|
|
68
74
|
return {
|
|
69
|
-
data:
|
|
75
|
+
data: normalizedTokens,
|
|
70
76
|
meta: {
|
|
71
77
|
securityScore
|
|
72
78
|
}
|
|
@@ -1,807 +0,0 @@
|
|
|
1
|
-
import React, { useState, useEffect } from 'react';
|
|
2
|
-
import styled, { keyframes } from 'styled-components';
|
|
3
|
-
import {
|
|
4
|
-
Main,
|
|
5
|
-
Typography,
|
|
6
|
-
Box,
|
|
7
|
-
Flex,
|
|
8
|
-
Button,
|
|
9
|
-
Loader,
|
|
10
|
-
EmptyStateLayout,
|
|
11
|
-
Table,
|
|
12
|
-
Thead,
|
|
13
|
-
Tbody,
|
|
14
|
-
Tr,
|
|
15
|
-
Td,
|
|
16
|
-
Th,
|
|
17
|
-
Badge,
|
|
18
|
-
IconButton,
|
|
19
|
-
Grid,
|
|
20
|
-
Tooltip,
|
|
21
|
-
Modal,
|
|
22
|
-
TextInput,
|
|
23
|
-
Field,
|
|
24
|
-
Checkbox,
|
|
25
|
-
} from '@strapi/design-system';
|
|
26
|
-
import { useFetchClient, useNotification } from '@strapi/strapi/admin';
|
|
27
|
-
import {
|
|
28
|
-
Plus,
|
|
29
|
-
ArrowLeft,
|
|
30
|
-
ArrowRight,
|
|
31
|
-
Key,
|
|
32
|
-
Shield,
|
|
33
|
-
Earth,
|
|
34
|
-
PaintBrush,
|
|
35
|
-
ChartBubble,
|
|
36
|
-
Server,
|
|
37
|
-
Clock,
|
|
38
|
-
Eye,
|
|
39
|
-
Trash,
|
|
40
|
-
Mail,
|
|
41
|
-
Cross,
|
|
42
|
-
WarningCircle,
|
|
43
|
-
Check,
|
|
44
|
-
Pencil,
|
|
45
|
-
User,
|
|
46
|
-
Calendar,
|
|
47
|
-
Lock,
|
|
48
|
-
ArrowClockwise
|
|
49
|
-
} from '@strapi/icons';
|
|
50
|
-
import CreateTokenModal from './CreateTokenModal';
|
|
51
|
-
|
|
52
|
-
// ================ ANIMATIONEN ================
|
|
53
|
-
const fadeIn = keyframes`
|
|
54
|
-
from { opacity: 0; transform: translateY(20px); }
|
|
55
|
-
to { opacity: 1; transform: translateY(0); }
|
|
56
|
-
`;
|
|
57
|
-
|
|
58
|
-
const pulse = keyframes`
|
|
59
|
-
0%, 100% { transform: scale(1); }
|
|
60
|
-
50% { transform: scale(1.05); }
|
|
61
|
-
`;
|
|
62
|
-
|
|
63
|
-
const shimmer = keyframes`
|
|
64
|
-
0% { background-position: -1000px 0; }
|
|
65
|
-
100% { background-position: 1000px 0; }
|
|
66
|
-
`;
|
|
67
|
-
|
|
68
|
-
const rotate = keyframes`
|
|
69
|
-
from { transform: rotate(0deg); }
|
|
70
|
-
to { transform: rotate(360deg); }
|
|
71
|
-
`;
|
|
72
|
-
|
|
73
|
-
// ================ STYLED COMPONENTS ================
|
|
74
|
-
const PageWrapper = styled(Box)`
|
|
75
|
-
animation: ${fadeIn} 0.6s ease;
|
|
76
|
-
`;
|
|
77
|
-
|
|
78
|
-
const GradientHeader = styled(Box)`
|
|
79
|
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
80
|
-
border-radius: 24px;
|
|
81
|
-
padding: 40px;
|
|
82
|
-
margin-bottom: 32px;
|
|
83
|
-
position: relative;
|
|
84
|
-
overflow: hidden;
|
|
85
|
-
box-shadow: 0 20px 40px -12px rgba(102, 126, 234, 0.35);
|
|
86
|
-
|
|
87
|
-
&::before {
|
|
88
|
-
content: '';
|
|
89
|
-
position: absolute;
|
|
90
|
-
top: 0;
|
|
91
|
-
left: -100%;
|
|
92
|
-
width: 200%;
|
|
93
|
-
height: 100%;
|
|
94
|
-
background: linear-gradient(
|
|
95
|
-
90deg,
|
|
96
|
-
transparent,
|
|
97
|
-
rgba(255, 255, 255, 0.1),
|
|
98
|
-
transparent
|
|
99
|
-
);
|
|
100
|
-
animation: ${shimmer} 3s infinite;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
&::after {
|
|
104
|
-
content: '✨';
|
|
105
|
-
position: absolute;
|
|
106
|
-
top: 20px;
|
|
107
|
-
right: 20px;
|
|
108
|
-
font-size: 48px;
|
|
109
|
-
opacity: 0.1;
|
|
110
|
-
animation: ${pulse} 2s infinite;
|
|
111
|
-
}
|
|
112
|
-
`;
|
|
113
|
-
|
|
114
|
-
const HeaderTitle = styled(Typography)`
|
|
115
|
-
color: white;
|
|
116
|
-
font-size: 36px;
|
|
117
|
-
font-weight: 700;
|
|
118
|
-
margin-bottom: 8px;
|
|
119
|
-
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
120
|
-
`;
|
|
121
|
-
|
|
122
|
-
const HeaderSubtitle = styled(Typography)`
|
|
123
|
-
color: rgba(255, 255, 255, 0.9);
|
|
124
|
-
font-size: 16px;
|
|
125
|
-
font-weight: 400;
|
|
126
|
-
`;
|
|
127
|
-
|
|
128
|
-
const GlassButton = styled(Button)`
|
|
129
|
-
background: rgba(255, 255, 255, 0.2);
|
|
130
|
-
backdrop-filter: blur(10px);
|
|
131
|
-
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
132
|
-
color: white;
|
|
133
|
-
font-weight: 600;
|
|
134
|
-
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
135
|
-
|
|
136
|
-
&:hover {
|
|
137
|
-
background: rgba(255, 255, 255, 0.3);
|
|
138
|
-
transform: translateY(-2px);
|
|
139
|
-
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
&:active {
|
|
143
|
-
transform: translateY(0);
|
|
144
|
-
}
|
|
145
|
-
`;
|
|
146
|
-
|
|
147
|
-
const StatsGrid = styled(Grid.Root)`
|
|
148
|
-
margin-bottom: 32px;
|
|
149
|
-
`;
|
|
150
|
-
|
|
151
|
-
const StatsCard = styled(Box)`
|
|
152
|
-
background: rgba(255, 255, 255, 0.95);
|
|
153
|
-
backdrop-filter: blur(20px);
|
|
154
|
-
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
155
|
-
border-radius: 20px;
|
|
156
|
-
padding: 28px;
|
|
157
|
-
position: relative;
|
|
158
|
-
overflow: hidden;
|
|
159
|
-
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
160
|
-
animation: ${fadeIn} 0.6s ease backwards;
|
|
161
|
-
animation-delay: ${props => props.$delay || '0s'};
|
|
162
|
-
|
|
163
|
-
&:hover {
|
|
164
|
-
transform: translateY(-8px) scale(1.02);
|
|
165
|
-
box-shadow:
|
|
166
|
-
0 20px 40px -12px rgba(0, 0, 0, 0.15),
|
|
167
|
-
0 0 0 1px rgba(102, 126, 234, 0.1);
|
|
168
|
-
|
|
169
|
-
.icon-wrapper {
|
|
170
|
-
transform: rotate(-5deg) scale(1.1);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
.stat-value {
|
|
174
|
-
transform: scale(1.1);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
&::before {
|
|
179
|
-
content: '';
|
|
180
|
-
position: absolute;
|
|
181
|
-
top: -50%;
|
|
182
|
-
right: -50%;
|
|
183
|
-
width: 200%;
|
|
184
|
-
height: 200%;
|
|
185
|
-
background: radial-gradient(
|
|
186
|
-
circle,
|
|
187
|
-
${props => props.$gradientColor || 'rgba(102, 126, 234, 0.08)'} 0%,
|
|
188
|
-
transparent 70%
|
|
189
|
-
);
|
|
190
|
-
pointer-events: none;
|
|
191
|
-
}
|
|
192
|
-
`;
|
|
193
|
-
|
|
194
|
-
const IconWrapper = styled(Box)`
|
|
195
|
-
padding: 18px;
|
|
196
|
-
border-radius: 18px;
|
|
197
|
-
display: inline-flex;
|
|
198
|
-
transition: all 0.3s ease;
|
|
199
|
-
background: ${props => props.$bgColor || 'rgba(102, 126, 234, 0.1)'};
|
|
200
|
-
|
|
201
|
-
svg {
|
|
202
|
-
width: 32px;
|
|
203
|
-
height: 32px;
|
|
204
|
-
color: ${props => props.$iconColor || '#667eea'};
|
|
205
|
-
}
|
|
206
|
-
`;
|
|
207
|
-
|
|
208
|
-
const StatValue = styled(Typography)`
|
|
209
|
-
font-size: 42px;
|
|
210
|
-
font-weight: 700;
|
|
211
|
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
212
|
-
-webkit-background-clip: text;
|
|
213
|
-
-webkit-text-fill-color: transparent;
|
|
214
|
-
transition: transform 0.3s ease;
|
|
215
|
-
margin: 16px 0 8px;
|
|
216
|
-
`;
|
|
217
|
-
|
|
218
|
-
const TabContainer = styled(Flex)`
|
|
219
|
-
margin-bottom: 32px;
|
|
220
|
-
padding: 8px;
|
|
221
|
-
background: rgba(255, 255, 255, 0.5);
|
|
222
|
-
backdrop-filter: blur(10px);
|
|
223
|
-
border-radius: 16px;
|
|
224
|
-
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.07);
|
|
225
|
-
`;
|
|
226
|
-
|
|
227
|
-
const TabButton = styled(Button)`
|
|
228
|
-
border-radius: 12px;
|
|
229
|
-
font-weight: 600;
|
|
230
|
-
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
231
|
-
position: relative;
|
|
232
|
-
overflow: hidden;
|
|
233
|
-
|
|
234
|
-
${props => props.$active && `
|
|
235
|
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
236
|
-
color: white;
|
|
237
|
-
transform: scale(1.05);
|
|
238
|
-
box-shadow: 0 8px 16px -4px rgba(102, 126, 234, 0.4);
|
|
239
|
-
|
|
240
|
-
&:hover {
|
|
241
|
-
transform: scale(1.08);
|
|
242
|
-
}
|
|
243
|
-
`}
|
|
244
|
-
|
|
245
|
-
${props => !props.$active && `
|
|
246
|
-
background: transparent;
|
|
247
|
-
color: #4a5568;
|
|
248
|
-
|
|
249
|
-
&:hover {
|
|
250
|
-
background: rgba(102, 126, 234, 0.08);
|
|
251
|
-
transform: translateY(-2px);
|
|
252
|
-
}
|
|
253
|
-
`}
|
|
254
|
-
`;
|
|
255
|
-
|
|
256
|
-
const StyledTable = styled(Table)`
|
|
257
|
-
border-radius: 20px;
|
|
258
|
-
overflow: hidden;
|
|
259
|
-
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.07);
|
|
260
|
-
background: white;
|
|
261
|
-
|
|
262
|
-
thead {
|
|
263
|
-
background: linear-gradient(135deg, #f7f9fc 0%, #f1f3f8 100%);
|
|
264
|
-
|
|
265
|
-
th {
|
|
266
|
-
font-weight: 600;
|
|
267
|
-
color: #2d3748;
|
|
268
|
-
border-bottom: 2px solid #e2e8f0;
|
|
269
|
-
padding: 16px;
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
tbody tr {
|
|
274
|
-
transition: all 0.2s ease;
|
|
275
|
-
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
|
|
276
|
-
|
|
277
|
-
&:hover {
|
|
278
|
-
background: linear-gradient(90deg,
|
|
279
|
-
rgba(102, 126, 234, 0.02) 0%,
|
|
280
|
-
rgba(102, 126, 234, 0.04) 50%,
|
|
281
|
-
rgba(102, 126, 234, 0.02) 100%
|
|
282
|
-
);
|
|
283
|
-
transform: translateX(4px);
|
|
284
|
-
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.04);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
td {
|
|
288
|
-
padding: 16px;
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
`;
|
|
292
|
-
|
|
293
|
-
const AnimatedBadge = styled(Badge)`
|
|
294
|
-
animation: ${fadeIn} 0.4s ease;
|
|
295
|
-
transition: all 0.2s ease;
|
|
296
|
-
font-weight: 600;
|
|
297
|
-
|
|
298
|
-
&:hover {
|
|
299
|
-
transform: scale(1.1);
|
|
300
|
-
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
|
301
|
-
}
|
|
302
|
-
`;
|
|
303
|
-
|
|
304
|
-
const EmptyStateWrapper = styled(Box)`
|
|
305
|
-
background: linear-gradient(135deg, #f7f9fc 0%, #ffffff 100%);
|
|
306
|
-
border-radius: 24px;
|
|
307
|
-
padding: 80px 40px;
|
|
308
|
-
text-align: center;
|
|
309
|
-
position: relative;
|
|
310
|
-
overflow: hidden;
|
|
311
|
-
|
|
312
|
-
&::before {
|
|
313
|
-
content: '🎯';
|
|
314
|
-
position: absolute;
|
|
315
|
-
top: 30px;
|
|
316
|
-
right: 30px;
|
|
317
|
-
font-size: 60px;
|
|
318
|
-
opacity: 0.05;
|
|
319
|
-
animation: ${rotate} 20s linear infinite;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
&::after {
|
|
323
|
-
content: '✨';
|
|
324
|
-
position: absolute;
|
|
325
|
-
bottom: 30px;
|
|
326
|
-
left: 30px;
|
|
327
|
-
font-size: 60px;
|
|
328
|
-
opacity: 0.05;
|
|
329
|
-
animation: ${pulse} 2s ease infinite;
|
|
330
|
-
}
|
|
331
|
-
`;
|
|
332
|
-
|
|
333
|
-
const LoadingWrapper = styled(Flex)`
|
|
334
|
-
min-height: 400px;
|
|
335
|
-
justify-content: center;
|
|
336
|
-
align-items: center;
|
|
337
|
-
|
|
338
|
-
.loader {
|
|
339
|
-
animation: ${rotate} 1s linear infinite;
|
|
340
|
-
}
|
|
341
|
-
`;
|
|
342
|
-
|
|
343
|
-
const ActionIconButton = styled(IconButton)`
|
|
344
|
-
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
345
|
-
|
|
346
|
-
&:hover {
|
|
347
|
-
transform: scale(1.2) rotate(5deg);
|
|
348
|
-
background: rgba(102, 126, 234, 0.1);
|
|
349
|
-
}
|
|
350
|
-
`;
|
|
351
|
-
|
|
352
|
-
// ================ HAUPTKOMPONENTE ================
|
|
353
|
-
const TokensRedesign = () => {
|
|
354
|
-
const { get, post, del } = useFetchClient();
|
|
355
|
-
const { toggleNotification } = useNotification();
|
|
356
|
-
|
|
357
|
-
// States
|
|
358
|
-
const [tokens, setTokens] = useState([]);
|
|
359
|
-
const [isLoading, setIsLoading] = useState(true);
|
|
360
|
-
const [activeTab, setActiveTab] = useState('magic-links');
|
|
361
|
-
const [selectedTokens, setSelectedTokens] = useState([]);
|
|
362
|
-
const [showCreateModal, setShowCreateModal] = useState(false);
|
|
363
|
-
const [createFormData, setCreateFormData] = useState({
|
|
364
|
-
email: '',
|
|
365
|
-
ttl: 24,
|
|
366
|
-
sendEmail: true
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
// Stats berechnen
|
|
370
|
-
const stats = {
|
|
371
|
-
total: tokens.length,
|
|
372
|
-
active: tokens.filter(t => {
|
|
373
|
-
const isExpired = t.expires_at && new Date(t.expires_at) < new Date();
|
|
374
|
-
return t.is_active && !isExpired;
|
|
375
|
-
}).length,
|
|
376
|
-
expired: tokens.filter(t => {
|
|
377
|
-
const isExpired = t.expires_at && new Date(t.expires_at) < new Date();
|
|
378
|
-
return isExpired;
|
|
379
|
-
}).length,
|
|
380
|
-
used: tokens.filter(t => !t.is_active).length,
|
|
381
|
-
};
|
|
382
|
-
|
|
383
|
-
// Stat Cards Konfiguration
|
|
384
|
-
const statCards = [
|
|
385
|
-
{
|
|
386
|
-
title: 'Alle Tokens',
|
|
387
|
-
value: stats.total,
|
|
388
|
-
icon: Server,
|
|
389
|
-
bgColor: 'rgba(102, 126, 234, 0.1)',
|
|
390
|
-
iconColor: '#667eea',
|
|
391
|
-
delay: '0s'
|
|
392
|
-
},
|
|
393
|
-
{
|
|
394
|
-
title: 'Aktive Tokens',
|
|
395
|
-
value: stats.active,
|
|
396
|
-
icon: ChartBubble,
|
|
397
|
-
bgColor: 'rgba(52, 211, 153, 0.1)',
|
|
398
|
-
iconColor: '#10b981',
|
|
399
|
-
delay: '0.1s'
|
|
400
|
-
},
|
|
401
|
-
{
|
|
402
|
-
title: 'Abgelaufene',
|
|
403
|
-
value: stats.expired,
|
|
404
|
-
icon: Clock,
|
|
405
|
-
bgColor: 'rgba(147, 51, 234, 0.1)',
|
|
406
|
-
iconColor: '#8b5cf6',
|
|
407
|
-
delay: '0.2s'
|
|
408
|
-
},
|
|
409
|
-
{
|
|
410
|
-
title: 'Verwendet',
|
|
411
|
-
value: stats.used,
|
|
412
|
-
icon: Check,
|
|
413
|
-
bgColor: 'rgba(251, 146, 60, 0.1)',
|
|
414
|
-
iconColor: '#f59e0b',
|
|
415
|
-
delay: '0.3s'
|
|
416
|
-
}
|
|
417
|
-
];
|
|
418
|
-
|
|
419
|
-
// Fetch Tokens
|
|
420
|
-
useEffect(() => {
|
|
421
|
-
fetchTokens();
|
|
422
|
-
}, []);
|
|
423
|
-
|
|
424
|
-
const fetchTokens = async () => {
|
|
425
|
-
setIsLoading(true);
|
|
426
|
-
try {
|
|
427
|
-
const response = await get('/magic-link/tokens');
|
|
428
|
-
setTokens(response?.data?.data || response?.data || []);
|
|
429
|
-
} catch (error) {
|
|
430
|
-
console.error('Error fetching tokens:', error);
|
|
431
|
-
toggleNotification({
|
|
432
|
-
type: 'warning',
|
|
433
|
-
message: 'Fehler beim Laden der Tokens'
|
|
434
|
-
});
|
|
435
|
-
} finally {
|
|
436
|
-
setIsLoading(false);
|
|
437
|
-
}
|
|
438
|
-
};
|
|
439
|
-
|
|
440
|
-
const handleRefresh = () => {
|
|
441
|
-
fetchTokens();
|
|
442
|
-
toggleNotification({
|
|
443
|
-
type: 'success',
|
|
444
|
-
message: 'Daten wurden aktualisiert! 🎉'
|
|
445
|
-
});
|
|
446
|
-
};
|
|
447
|
-
|
|
448
|
-
const handleCreateToken = async () => {
|
|
449
|
-
try {
|
|
450
|
-
if (!createFormData.email) {
|
|
451
|
-
toggleNotification({
|
|
452
|
-
type: 'warning',
|
|
453
|
-
message: 'Bitte gib eine E-Mail-Adresse ein'
|
|
454
|
-
});
|
|
455
|
-
return;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
// API-Aufruf zum Erstellen eines neuen Tokens
|
|
459
|
-
const response = await post('/magic-link/tokens', {
|
|
460
|
-
email: createFormData.email,
|
|
461
|
-
send_email: createFormData.sendEmail,
|
|
462
|
-
context: {
|
|
463
|
-
ttl: createFormData.ttl
|
|
464
|
-
}
|
|
465
|
-
});
|
|
466
|
-
|
|
467
|
-
if (response?.data) {
|
|
468
|
-
toggleNotification({
|
|
469
|
-
type: 'success',
|
|
470
|
-
message: `Token erfolgreich erstellt! 🎉 ${createFormData.sendEmail ? 'E-Mail wurde gesendet.' : ''}`,
|
|
471
|
-
});
|
|
472
|
-
|
|
473
|
-
// Modal schließen und Formular zurücksetzen
|
|
474
|
-
setShowCreateModal(false);
|
|
475
|
-
setCreateFormData({
|
|
476
|
-
email: '',
|
|
477
|
-
ttl: 24,
|
|
478
|
-
sendEmail: true
|
|
479
|
-
});
|
|
480
|
-
|
|
481
|
-
// Token-Liste aktualisieren
|
|
482
|
-
await fetchTokens();
|
|
483
|
-
}
|
|
484
|
-
} catch (error) {
|
|
485
|
-
console.error('Error creating token:', error);
|
|
486
|
-
toggleNotification({
|
|
487
|
-
type: 'warning',
|
|
488
|
-
message: error?.response?.data?.error?.message || 'Fehler beim Erstellen des Tokens'
|
|
489
|
-
});
|
|
490
|
-
}
|
|
491
|
-
};
|
|
492
|
-
|
|
493
|
-
const handleDelete = async (tokenId) => {
|
|
494
|
-
try {
|
|
495
|
-
await del(`/magic-link/tokens/${tokenId}`);
|
|
496
|
-
await fetchTokens();
|
|
497
|
-
toggleNotification({
|
|
498
|
-
type: 'success',
|
|
499
|
-
message: 'Token wurde gelöscht! 🗑️'
|
|
500
|
-
});
|
|
501
|
-
} catch (error) {
|
|
502
|
-
toggleNotification({
|
|
503
|
-
type: 'warning',
|
|
504
|
-
message: 'Fehler beim Löschen des Tokens'
|
|
505
|
-
});
|
|
506
|
-
}
|
|
507
|
-
};
|
|
508
|
-
|
|
509
|
-
if (isLoading) {
|
|
510
|
-
return (
|
|
511
|
-
<Main>
|
|
512
|
-
<LoadingWrapper>
|
|
513
|
-
<Box className="loader">
|
|
514
|
-
<Loader>Loading...</Loader>
|
|
515
|
-
</Box>
|
|
516
|
-
</LoadingWrapper>
|
|
517
|
-
</Main>
|
|
518
|
-
);
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
return (
|
|
522
|
-
<Main>
|
|
523
|
-
<PageWrapper paddingBottom={10}>
|
|
524
|
-
{/* Gradient Header */}
|
|
525
|
-
<GradientHeader>
|
|
526
|
-
<Flex justifyContent="space-between" alignItems="center">
|
|
527
|
-
<Box>
|
|
528
|
-
<HeaderTitle as="h1">
|
|
529
|
-
✨ Token-Verwaltung
|
|
530
|
-
</HeaderTitle>
|
|
531
|
-
<HeaderSubtitle>
|
|
532
|
-
Verwalte deine Magic Link Tokens und JWT-Sessions mit Style
|
|
533
|
-
</HeaderSubtitle>
|
|
534
|
-
</Box>
|
|
535
|
-
<Flex gap={2}>
|
|
536
|
-
<GlassButton
|
|
537
|
-
onClick={() => setShowCreateModal(true)}
|
|
538
|
-
startIcon={<PaintBrush />}
|
|
539
|
-
size="L"
|
|
540
|
-
>
|
|
541
|
-
Token erstellen
|
|
542
|
-
</GlassButton>
|
|
543
|
-
<GlassButton
|
|
544
|
-
onClick={handleRefresh}
|
|
545
|
-
startIcon={<ArrowClockwise />}
|
|
546
|
-
size="L"
|
|
547
|
-
>
|
|
548
|
-
Aktualisieren
|
|
549
|
-
</GlassButton>
|
|
550
|
-
<GlassButton
|
|
551
|
-
onClick={() => window.history.back()}
|
|
552
|
-
startIcon={<ArrowLeft />}
|
|
553
|
-
size="L"
|
|
554
|
-
>
|
|
555
|
-
Zurück
|
|
556
|
-
</GlassButton>
|
|
557
|
-
</Flex>
|
|
558
|
-
</Flex>
|
|
559
|
-
</GradientHeader>
|
|
560
|
-
|
|
561
|
-
{/* Tab Navigation */}
|
|
562
|
-
<TabContainer gap={2} justifyContent="center">
|
|
563
|
-
<TabButton
|
|
564
|
-
$active={activeTab === 'magic-links'}
|
|
565
|
-
onClick={() => setActiveTab('magic-links')}
|
|
566
|
-
startIcon={<Key />}
|
|
567
|
-
size="L"
|
|
568
|
-
>
|
|
569
|
-
Magic Link Tokens
|
|
570
|
-
</TabButton>
|
|
571
|
-
<TabButton
|
|
572
|
-
$active={activeTab === 'jwt-sessions'}
|
|
573
|
-
onClick={() => setActiveTab('jwt-sessions')}
|
|
574
|
-
startIcon={<Shield />}
|
|
575
|
-
size="L"
|
|
576
|
-
>
|
|
577
|
-
JWT Sessions
|
|
578
|
-
</TabButton>
|
|
579
|
-
<TabButton
|
|
580
|
-
$active={activeTab === 'ip-bans'}
|
|
581
|
-
onClick={() => setActiveTab('ip-bans')}
|
|
582
|
-
startIcon={<Earth />}
|
|
583
|
-
size="L"
|
|
584
|
-
>
|
|
585
|
-
IP-Sperren
|
|
586
|
-
</TabButton>
|
|
587
|
-
</TabContainer>
|
|
588
|
-
|
|
589
|
-
{/* Statistik Cards */}
|
|
590
|
-
<StatsGrid gap={4}>
|
|
591
|
-
{statCards.map((stat, index) => {
|
|
592
|
-
const IconComponent = stat.icon;
|
|
593
|
-
return (
|
|
594
|
-
<Grid.Item col={3} key={index}>
|
|
595
|
-
<StatsCard
|
|
596
|
-
$gradientColor={stat.bgColor}
|
|
597
|
-
$delay={stat.delay}
|
|
598
|
-
>
|
|
599
|
-
<Flex direction="column" alignItems="center">
|
|
600
|
-
<IconWrapper
|
|
601
|
-
className="icon-wrapper"
|
|
602
|
-
$bgColor={stat.bgColor}
|
|
603
|
-
$iconColor={stat.iconColor}
|
|
604
|
-
>
|
|
605
|
-
<IconComponent />
|
|
606
|
-
</IconWrapper>
|
|
607
|
-
<StatValue className="stat-value">
|
|
608
|
-
{stat.value}
|
|
609
|
-
</StatValue>
|
|
610
|
-
<Typography variant="epsilon" textColor="neutral600">
|
|
611
|
-
{stat.title}
|
|
612
|
-
</Typography>
|
|
613
|
-
</Flex>
|
|
614
|
-
</StatsCard>
|
|
615
|
-
</Grid.Item>
|
|
616
|
-
);
|
|
617
|
-
})}
|
|
618
|
-
</StatsGrid>
|
|
619
|
-
|
|
620
|
-
{/* Tabellen Bereich */}
|
|
621
|
-
{activeTab === 'magic-links' && (
|
|
622
|
-
<Box>
|
|
623
|
-
{tokens.length === 0 ? (
|
|
624
|
-
<EmptyStateWrapper>
|
|
625
|
-
<EmptyStateLayout
|
|
626
|
-
icon={<Eye style={{ width: '64px', height: '64px', color: '#667eea' }} />}
|
|
627
|
-
content={
|
|
628
|
-
<Box>
|
|
629
|
-
<Typography variant="alpha" style={{ marginBottom: '12px' }}>
|
|
630
|
-
Keine Magic Link Tokens vorhanden
|
|
631
|
-
</Typography>
|
|
632
|
-
<Typography variant="omega" textColor="neutral600">
|
|
633
|
-
Erstelle dein erstes Token oder aktualisiere die Ansicht
|
|
634
|
-
</Typography>
|
|
635
|
-
</Box>
|
|
636
|
-
}
|
|
637
|
-
action={
|
|
638
|
-
<Flex gap={3} justifyContent="center">
|
|
639
|
-
<Button
|
|
640
|
-
onClick={() => setShowCreateModal(true)}
|
|
641
|
-
startIcon={<Plus />}
|
|
642
|
-
size="L"
|
|
643
|
-
style={{
|
|
644
|
-
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
|
645
|
-
color: 'white',
|
|
646
|
-
border: 'none',
|
|
647
|
-
fontWeight: '600',
|
|
648
|
-
}}
|
|
649
|
-
>
|
|
650
|
-
Erstes Token erstellen
|
|
651
|
-
</Button>
|
|
652
|
-
<Button
|
|
653
|
-
onClick={handleRefresh}
|
|
654
|
-
variant="secondary"
|
|
655
|
-
size="L"
|
|
656
|
-
>
|
|
657
|
-
Aktualisieren
|
|
658
|
-
</Button>
|
|
659
|
-
</Flex>
|
|
660
|
-
}
|
|
661
|
-
/>
|
|
662
|
-
</EmptyStateWrapper>
|
|
663
|
-
) : (
|
|
664
|
-
<StyledTable>
|
|
665
|
-
<Thead>
|
|
666
|
-
<Tr>
|
|
667
|
-
<Th>
|
|
668
|
-
<Checkbox />
|
|
669
|
-
</Th>
|
|
670
|
-
<Th>Email</Th>
|
|
671
|
-
<Th>Token</Th>
|
|
672
|
-
<Th>Status</Th>
|
|
673
|
-
<Th>Erstellt</Th>
|
|
674
|
-
<Th>Gültig bis</Th>
|
|
675
|
-
<Th>Aktionen</Th>
|
|
676
|
-
</Tr>
|
|
677
|
-
</Thead>
|
|
678
|
-
<Tbody>
|
|
679
|
-
{tokens.map((token) => (
|
|
680
|
-
<Tr key={token.id}>
|
|
681
|
-
<Td>
|
|
682
|
-
<Checkbox
|
|
683
|
-
value={selectedTokens.includes(token.id)}
|
|
684
|
-
onCheckedChange={(value) => {
|
|
685
|
-
if (value) {
|
|
686
|
-
setSelectedTokens([...selectedTokens, token.id]);
|
|
687
|
-
} else {
|
|
688
|
-
setSelectedTokens(selectedTokens.filter(id => id !== token.id));
|
|
689
|
-
}
|
|
690
|
-
}}
|
|
691
|
-
/>
|
|
692
|
-
</Td>
|
|
693
|
-
<Td>
|
|
694
|
-
<Flex alignItems="center" gap={2}>
|
|
695
|
-
<Mail style={{ color: '#667eea' }} />
|
|
696
|
-
<Typography fontWeight="semiBold">
|
|
697
|
-
{token.email}
|
|
698
|
-
</Typography>
|
|
699
|
-
</Flex>
|
|
700
|
-
</Td>
|
|
701
|
-
<Td>
|
|
702
|
-
<Typography variant="pi" textColor="neutral600">
|
|
703
|
-
{token.login_token?.substring(0, 20)}...
|
|
704
|
-
</Typography>
|
|
705
|
-
</Td>
|
|
706
|
-
<Td>
|
|
707
|
-
{token.used ? (
|
|
708
|
-
<AnimatedBadge variant="secondary">
|
|
709
|
-
Verwendet
|
|
710
|
-
</AnimatedBadge>
|
|
711
|
-
) : token.expired ? (
|
|
712
|
-
<AnimatedBadge variant="warning">
|
|
713
|
-
Abgelaufen
|
|
714
|
-
</AnimatedBadge>
|
|
715
|
-
) : (
|
|
716
|
-
<AnimatedBadge variant="success">
|
|
717
|
-
Aktiv
|
|
718
|
-
</AnimatedBadge>
|
|
719
|
-
)}
|
|
720
|
-
</Td>
|
|
721
|
-
<Td>
|
|
722
|
-
<Flex alignItems="center" gap={1}>
|
|
723
|
-
<Calendar style={{ width: '16px', height: '16px', color: '#8b5cf6' }} />
|
|
724
|
-
<Typography variant="pi">
|
|
725
|
-
{new Date(token.created_at).toLocaleDateString('de-DE')}
|
|
726
|
-
</Typography>
|
|
727
|
-
</Flex>
|
|
728
|
-
</Td>
|
|
729
|
-
<Td>
|
|
730
|
-
<Flex alignItems="center" gap={1}>
|
|
731
|
-
<Clock style={{ width: '16px', height: '16px', color: '#f59e0b' }} />
|
|
732
|
-
<Typography variant="pi">
|
|
733
|
-
{new Date(token.expires_at).toLocaleDateString('de-DE')}
|
|
734
|
-
</Typography>
|
|
735
|
-
</Flex>
|
|
736
|
-
</Td>
|
|
737
|
-
<Td>
|
|
738
|
-
<Flex gap={2}>
|
|
739
|
-
<Tooltip content="Details">
|
|
740
|
-
<ActionIconButton
|
|
741
|
-
icon={<Eye />}
|
|
742
|
-
variant="ghost"
|
|
743
|
-
/>
|
|
744
|
-
</Tooltip>
|
|
745
|
-
<Tooltip content="Bearbeiten">
|
|
746
|
-
<ActionIconButton
|
|
747
|
-
icon={<Pencil />}
|
|
748
|
-
variant="ghost"
|
|
749
|
-
/>
|
|
750
|
-
</Tooltip>
|
|
751
|
-
<Tooltip content="Löschen">
|
|
752
|
-
<ActionIconButton
|
|
753
|
-
onClick={() => handleDelete(token.id)}
|
|
754
|
-
icon={<Trash />}
|
|
755
|
-
variant="ghost"
|
|
756
|
-
/>
|
|
757
|
-
</Tooltip>
|
|
758
|
-
</Flex>
|
|
759
|
-
</Td>
|
|
760
|
-
</Tr>
|
|
761
|
-
))}
|
|
762
|
-
</Tbody>
|
|
763
|
-
</StyledTable>
|
|
764
|
-
)}
|
|
765
|
-
</Box>
|
|
766
|
-
)}
|
|
767
|
-
|
|
768
|
-
{/* Andere Tabs */}
|
|
769
|
-
{activeTab === 'jwt-sessions' && (
|
|
770
|
-
<EmptyStateWrapper>
|
|
771
|
-
<Typography variant="alpha">JWT Sessions</Typography>
|
|
772
|
-
<Typography variant="omega" textColor="neutral600" style={{ marginTop: '12px' }}>
|
|
773
|
-
Kommt bald... 🚀
|
|
774
|
-
</Typography>
|
|
775
|
-
</EmptyStateWrapper>
|
|
776
|
-
)}
|
|
777
|
-
|
|
778
|
-
{activeTab === 'ip-bans' && (
|
|
779
|
-
<EmptyStateWrapper>
|
|
780
|
-
<Typography variant="alpha">IP-Sperren</Typography>
|
|
781
|
-
<Typography variant="omega" textColor="neutral600" style={{ marginTop: '12px' }}>
|
|
782
|
-
Kommt bald... 🔒
|
|
783
|
-
</Typography>
|
|
784
|
-
</EmptyStateWrapper>
|
|
785
|
-
)}
|
|
786
|
-
|
|
787
|
-
{/* Create Modal */}
|
|
788
|
-
<CreateTokenModal
|
|
789
|
-
isOpen={showCreateModal}
|
|
790
|
-
onClose={() => {
|
|
791
|
-
setShowCreateModal(false);
|
|
792
|
-
setCreateFormData({
|
|
793
|
-
email: '',
|
|
794
|
-
ttl: 24,
|
|
795
|
-
sendEmail: true
|
|
796
|
-
});
|
|
797
|
-
}}
|
|
798
|
-
onSubmit={handleCreateToken}
|
|
799
|
-
formData={createFormData}
|
|
800
|
-
setFormData={setCreateFormData}
|
|
801
|
-
/>
|
|
802
|
-
</PageWrapper>
|
|
803
|
-
</Main>
|
|
804
|
-
);
|
|
805
|
-
};
|
|
806
|
-
|
|
807
|
-
export default TokensRedesign;
|