strapi-plugin-magic-link-v5 4.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/.eslintignore +1 -0
- package/.github/workflows/semantic-release.yml +27 -0
- package/README.md +318 -0
- package/admin/jsconfig.json +10 -0
- package/admin/src/components/Initializer/index.jsx +20 -0
- package/admin/src/components/Initializer.jsx +18 -0
- package/admin/src/components/LazyComponentLoader.jsx +27 -0
- package/admin/src/components/PluginIcon/index.jsx +6 -0
- package/admin/src/components/PluginIcon.jsx +5 -0
- package/admin/src/index.js +101 -0
- package/admin/src/pages/App/index.jsx +50 -0
- package/admin/src/pages/App.jsx +15 -0
- package/admin/src/pages/HomePage/index.js +2 -0
- package/admin/src/pages/HomePage/index.jsx +228 -0
- package/admin/src/pages/HomePage.jsx +655 -0
- package/admin/src/pages/Settings/index.jsx +1289 -0
- package/admin/src/pages/Settings/utils/api.js +13 -0
- package/admin/src/pages/Settings/utils/index.js +5 -0
- package/admin/src/pages/Settings/utils/layout.js +100 -0
- package/admin/src/pages/Settings/utils/schema.js +18 -0
- package/admin/src/pages/Tokens/index.jsx +2250 -0
- package/admin/src/permissions.js +7 -0
- package/admin/src/pluginId.js +3 -0
- package/admin/src/routes.js +40 -0
- package/admin/src/translations/de.json +188 -0
- package/admin/src/translations/en.json +189 -0
- package/admin/src/utils/getRequestURL.js +5 -0
- package/admin/src/utils/getTrad.js +17 -0
- package/admin/src/utils/getTranslation.js +3 -0
- package/admin/src/utils/index.js +4 -0
- package/build.js +75 -0
- package/package.json +59 -0
- package/server/bootstrap.js +127 -0
- package/server/controllers/settings.js +122 -0
- package/server/jsconfig.json +10 -0
- package/server/services/store.js +35 -0
- package/server/src/bootstrap.js +110 -0
- package/server/src/config/index.js +6 -0
- package/server/src/content-types/index.js +7 -0
- package/server/src/content-types/token/index.js +5 -0
- package/server/src/content-types/token/schema.json +47 -0
- package/server/src/controllers/auth.js +211 -0
- package/server/src/controllers/controller.js +213 -0
- package/server/src/controllers/index.js +16 -0
- package/server/src/controllers/jwt.js +261 -0
- package/server/src/controllers/tokens.js +654 -0
- package/server/src/destroy.js +5 -0
- package/server/src/index.js +33 -0
- package/server/src/middlewares/index.js +3 -0
- package/server/src/policies/index.js +3 -0
- package/server/src/register.js +5 -0
- package/server/src/routes/admin.js +160 -0
- package/server/src/routes/content-api.js +27 -0
- package/server/src/routes/index.js +9 -0
- package/server/src/services/index.js +11 -0
- package/server/src/services/magic-link.js +356 -0
- package/server/src/services/service.js +13 -0
- package/server/utils/index.js +14 -0
- package/strapi-admin.js +82 -0
- package/strapi-server.js +4 -0
- package/vite.config.js +36 -0
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hilfsfunktion zum Senden einer Standard-E-Mail ohne Email Designer
|
|
5
|
+
* @param {Object} user - Der Benutzer, an den die E-Mail gesendet wird
|
|
6
|
+
* @param {string} magicLink - Der vollständige Magic-Link-URL
|
|
7
|
+
* @param {Object} settings - Die Plugin-Einstellungen
|
|
8
|
+
* @param {string} token - Der generierte Token-Wert
|
|
9
|
+
*/
|
|
10
|
+
const sendStandardEmail = async (user, magicLink, settings, token) => {
|
|
11
|
+
try {
|
|
12
|
+
// HTML- und Text-Versionen der Nachricht vorbereiten
|
|
13
|
+
let htmlMessage = settings.message_html || '';
|
|
14
|
+
let textMessage = settings.message_text || '';
|
|
15
|
+
|
|
16
|
+
// Ersetze Platzhalter in den Nachrichten
|
|
17
|
+
htmlMessage = htmlMessage
|
|
18
|
+
.replace(/{link}/g, magicLink)
|
|
19
|
+
.replace(/<%= URL %>/g, settings.confirmationUrl || strapi.config.server.url)
|
|
20
|
+
.replace(/<%= CODE %>/g, token)
|
|
21
|
+
.replace(/{username}/g, user.username || '')
|
|
22
|
+
.replace(/{email}/g, user.email || '');
|
|
23
|
+
|
|
24
|
+
textMessage = textMessage
|
|
25
|
+
.replace(/{link}/g, magicLink)
|
|
26
|
+
.replace(/<%= URL %>/g, settings.confirmationUrl || strapi.config.server.url)
|
|
27
|
+
.replace(/<%= CODE %>/g, token)
|
|
28
|
+
.replace(/{username}/g, user.username || '')
|
|
29
|
+
.replace(/{email}/g, user.email || '');
|
|
30
|
+
|
|
31
|
+
// Sende die Email
|
|
32
|
+
await strapi.plugins.email.services.email.send({
|
|
33
|
+
to: user.email,
|
|
34
|
+
from: settings.from_email ? `${settings.from_name} <${settings.from_email}>` : undefined,
|
|
35
|
+
replyTo: settings.response_email || undefined,
|
|
36
|
+
subject: settings.object,
|
|
37
|
+
html: htmlMessage,
|
|
38
|
+
text: textMessage,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
strapi.log.info(`Standard Magic Link Email an ${user.email} gesendet`);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
strapi.log.error('Fehler beim Senden der Standard-E-Mail:', error);
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Tokens controller
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
module.exports = {
|
|
53
|
+
/**
|
|
54
|
+
* Get all tokens
|
|
55
|
+
* @param {Object} ctx - The request context
|
|
56
|
+
*/
|
|
57
|
+
async find(ctx) {
|
|
58
|
+
try {
|
|
59
|
+
// Query all tokens
|
|
60
|
+
const tokens = await strapi.db.query('plugin::magic-link.token').findMany({
|
|
61
|
+
orderBy: { createdAt: 'desc' },
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Berechne den Sicherheitswert
|
|
65
|
+
const securityScore = await this.calculateSecurityScore();
|
|
66
|
+
|
|
67
|
+
// Füge den Sicherheitswert als Metadaten hinzu
|
|
68
|
+
return {
|
|
69
|
+
data: tokens,
|
|
70
|
+
meta: {
|
|
71
|
+
securityScore
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
} catch (error) {
|
|
75
|
+
ctx.throw(500, error);
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Create a new token
|
|
81
|
+
* @param {Object} ctx - The request context
|
|
82
|
+
*/
|
|
83
|
+
async create(ctx) {
|
|
84
|
+
try {
|
|
85
|
+
const { email, send_email = true, context = {} } = ctx.request.body;
|
|
86
|
+
|
|
87
|
+
if (!email) {
|
|
88
|
+
return ctx.badRequest('Email is required');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Überprüfe, ob die Plugin-Einstellungen das Erstellen neuer Benutzer erlauben
|
|
92
|
+
const pluginStore = strapi.store({
|
|
93
|
+
environment: '',
|
|
94
|
+
type: 'plugin',
|
|
95
|
+
name: 'magic-link',
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const settings = await pluginStore.get({ key: 'settings' });
|
|
99
|
+
|
|
100
|
+
// Find the user
|
|
101
|
+
let user = await strapi.db.query('plugin::users-permissions.user').findOne({
|
|
102
|
+
where: { email },
|
|
103
|
+
select: ['id', 'username', 'email'],
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// If user doesn't exist and create_new_user is not enabled, return error
|
|
107
|
+
if (!user && !settings.create_new_user) {
|
|
108
|
+
return ctx.badRequest('User does not exist and automatic user creation is disabled');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// If user doesn't exist, create a new one
|
|
112
|
+
if (!user && settings.create_new_user) {
|
|
113
|
+
// Generate a random username based on the email
|
|
114
|
+
const username = email.split('@')[0] + Math.floor(Math.random() * 10000);
|
|
115
|
+
|
|
116
|
+
// Create a random password
|
|
117
|
+
const password = Math.random().toString(36).substring(2, 15) +
|
|
118
|
+
Math.random().toString(36).substring(2, 15);
|
|
119
|
+
|
|
120
|
+
// Get the default role (authenticated)
|
|
121
|
+
const defaultRole = await strapi
|
|
122
|
+
.query('plugin::users-permissions.role')
|
|
123
|
+
.findOne({ where: { type: 'authenticated' } });
|
|
124
|
+
|
|
125
|
+
if (!defaultRole) {
|
|
126
|
+
return ctx.badRequest('Authenticated role not found');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Create the user
|
|
130
|
+
user = await strapi.plugins['users-permissions'].services.user.add({
|
|
131
|
+
username,
|
|
132
|
+
email,
|
|
133
|
+
password,
|
|
134
|
+
provider: 'magic-link',
|
|
135
|
+
confirmed: true, // Auto-confirm the user
|
|
136
|
+
blocked: false,
|
|
137
|
+
role: defaultRole.id,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
strapi.log.info(`Created new user with email: ${email}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Generate random token (16 characters)
|
|
144
|
+
const tokenValue = Math.random().toString(36).substring(2, 10) +
|
|
145
|
+
Math.random().toString(36).substring(2, 10);
|
|
146
|
+
|
|
147
|
+
// Set expiration (24 hours from now)
|
|
148
|
+
const expiresAt = new Date();
|
|
149
|
+
expiresAt.setHours(expiresAt.getHours() + 24);
|
|
150
|
+
|
|
151
|
+
// Erweitere den Kontext mit Ablaufdatum und Benutzerinformationen
|
|
152
|
+
const enrichedContext = {
|
|
153
|
+
...typeof context === 'string' ? JSON.parse(context) : context,
|
|
154
|
+
expires_at: expiresAt.toISOString(),
|
|
155
|
+
expiry_formatted: new Intl.DateTimeFormat('de-DE', {
|
|
156
|
+
year: 'numeric',
|
|
157
|
+
month: '2-digit',
|
|
158
|
+
day: '2-digit',
|
|
159
|
+
hour: '2-digit',
|
|
160
|
+
minute: '2-digit'
|
|
161
|
+
}).format(expiresAt),
|
|
162
|
+
user: {
|
|
163
|
+
id: user.id,
|
|
164
|
+
email: user.email,
|
|
165
|
+
username: user.username
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// Create the token
|
|
170
|
+
const token = await strapi.db.query('plugin::magic-link.token').create({
|
|
171
|
+
data: {
|
|
172
|
+
token: tokenValue,
|
|
173
|
+
email: user.email,
|
|
174
|
+
user_id: user.id,
|
|
175
|
+
expires_at: expiresAt,
|
|
176
|
+
is_active: true,
|
|
177
|
+
ip_address: null, // Wird beim Verwenden gesetzt
|
|
178
|
+
user_agent: null, // Wird beim Verwenden gesetzt
|
|
179
|
+
context: enrichedContext, // Verwende den angereicherten Kontext
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// Sende eine E-Mail mit dem Magic-Link nur wenn send_email true ist
|
|
184
|
+
if (send_email) {
|
|
185
|
+
try {
|
|
186
|
+
// Lade die Einstellungen aus dem Plugin-Store, falls nicht bereits geladen
|
|
187
|
+
if (!settings) {
|
|
188
|
+
settings = await pluginStore.get({ key: 'settings' });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (settings && settings.enabled && settings.from_email && settings.object) {
|
|
192
|
+
// Erstelle die Magic-Link-URL
|
|
193
|
+
const baseUrl = settings.confirmationUrl || strapi.config.server.url;
|
|
194
|
+
const magicLink = `${baseUrl}?token=${tokenValue}`;
|
|
195
|
+
|
|
196
|
+
// Prüfen, ob wir Email Designer verwenden sollen
|
|
197
|
+
if (settings.use_email_designer && settings.email_designer_template_id) {
|
|
198
|
+
// Prüfen, ob das Email Designer Plugin verfügbar ist
|
|
199
|
+
if (strapi.plugin('email-designer-5')) {
|
|
200
|
+
// Konvertiere die Template-ID zu einer Zahl
|
|
201
|
+
const templateId = parseInt(settings.email_designer_template_id, 10);
|
|
202
|
+
|
|
203
|
+
// Stelle sicher, dass die Template-ID eine gültige Zahl ist
|
|
204
|
+
if (!isNaN(templateId) && templateId > 0) {
|
|
205
|
+
// Email mit dem Email Designer Plugin versenden
|
|
206
|
+
try {
|
|
207
|
+
// Die korrekte Email Designer 5 API verwenden
|
|
208
|
+
await strapi
|
|
209
|
+
.plugin('email-designer-5')
|
|
210
|
+
.service('email')
|
|
211
|
+
.sendTemplatedEmail(
|
|
212
|
+
{
|
|
213
|
+
to: user.email,
|
|
214
|
+
from: settings.from_email ? `${settings.from_name} <${settings.from_email}>` : undefined,
|
|
215
|
+
replyTo: settings.response_email || undefined,
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
templateReferenceId: templateId,
|
|
219
|
+
subject: settings.object, // Optional: überschreibt den Betreff der Vorlage
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
// Variablen für die Vorlage
|
|
223
|
+
user: {
|
|
224
|
+
username: user.username,
|
|
225
|
+
email: user.email,
|
|
226
|
+
},
|
|
227
|
+
magicLink: magicLink,
|
|
228
|
+
token: tokenValue,
|
|
229
|
+
expiresAt: expiresAt.toISOString(),
|
|
230
|
+
}
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
strapi.log.info(`Magic Link Email mit Email Designer (Template ID: ${templateId}) an ${user.email} gesendet`);
|
|
234
|
+
} catch (emailDesignerError) {
|
|
235
|
+
strapi.log.error('Fehler bei Email Designer:', emailDesignerError);
|
|
236
|
+
strapi.log.info('Fallback auf Standard-Email-Versand...');
|
|
237
|
+
await sendStandardEmail(user, magicLink, settings, tokenValue);
|
|
238
|
+
}
|
|
239
|
+
} else {
|
|
240
|
+
strapi.log.warn(`Ungültige Email Designer Template ID: '${settings.email_designer_template_id}', verwende Standard-Email`);
|
|
241
|
+
// Fallback auf Standard-Email wenn die Template-ID ungültig ist
|
|
242
|
+
await sendStandardEmail(user, magicLink, settings, tokenValue);
|
|
243
|
+
}
|
|
244
|
+
} else {
|
|
245
|
+
strapi.log.warn('Email Designer Plugin ist aktiviert, aber nicht installiert');
|
|
246
|
+
|
|
247
|
+
// Fallback auf Standard-Email
|
|
248
|
+
await sendStandardEmail(user, magicLink, settings, tokenValue);
|
|
249
|
+
}
|
|
250
|
+
} else {
|
|
251
|
+
// Standard-Email-Versand
|
|
252
|
+
await sendStandardEmail(user, magicLink, settings, tokenValue);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
strapi.log.info(`Magic Link Token erstellt und E-Mail an ${user.email} gesendet`);
|
|
256
|
+
} else {
|
|
257
|
+
strapi.log.info(`Magic Link Token erstellt, aber keine E-Mail-Einstellungen konfiguriert`);
|
|
258
|
+
}
|
|
259
|
+
} catch (emailError) {
|
|
260
|
+
strapi.log.error('Fehler beim Senden der Magic Link Email:', emailError);
|
|
261
|
+
// Wir werfen keinen Fehler, da der Token trotzdem erstellt wurde
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Entferne den Token-Wert aus der Antwort aus Sicherheitsgründen
|
|
266
|
+
const { token: _, ...safeToken } = token;
|
|
267
|
+
|
|
268
|
+
return safeToken;
|
|
269
|
+
} catch (error) {
|
|
270
|
+
strapi.log.error('Error creating token:', error);
|
|
271
|
+
ctx.throw(500, error);
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Block a token
|
|
277
|
+
* @param {Object} ctx - The request context
|
|
278
|
+
*/
|
|
279
|
+
async block(ctx) {
|
|
280
|
+
try {
|
|
281
|
+
const { id } = ctx.params;
|
|
282
|
+
|
|
283
|
+
// Check if token exists
|
|
284
|
+
const token = await strapi.db.query('plugin::magic-link.token').findOne({
|
|
285
|
+
where: { id },
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
if (!token) {
|
|
289
|
+
return ctx.notFound('Token not found');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Update the token to be inactive
|
|
293
|
+
const updatedToken = await strapi.db.query('plugin::magic-link.token').update({
|
|
294
|
+
where: { id },
|
|
295
|
+
data: {
|
|
296
|
+
is_active: false,
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
return updatedToken;
|
|
301
|
+
} catch (error) {
|
|
302
|
+
ctx.throw(500, error);
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Delete a token
|
|
308
|
+
* @param {Object} ctx - The request context
|
|
309
|
+
*/
|
|
310
|
+
async delete(ctx) {
|
|
311
|
+
try {
|
|
312
|
+
const { id } = ctx.params;
|
|
313
|
+
|
|
314
|
+
// Check if token exists
|
|
315
|
+
const token = await strapi.db.query('plugin::magic-link.token').findOne({
|
|
316
|
+
where: { id },
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
if (!token) {
|
|
320
|
+
return ctx.notFound('Token not found');
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Delete the token
|
|
324
|
+
await strapi.db.query('plugin::magic-link.token').delete({
|
|
325
|
+
where: { id },
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
return { success: true };
|
|
329
|
+
} catch (error) {
|
|
330
|
+
strapi.log.error('Error deleting token:', error);
|
|
331
|
+
ctx.throw(500, error);
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Activate a token
|
|
337
|
+
* @param {Object} ctx - The request context
|
|
338
|
+
*/
|
|
339
|
+
async activate(ctx) {
|
|
340
|
+
try {
|
|
341
|
+
const { id } = ctx.params;
|
|
342
|
+
|
|
343
|
+
// Prüfe, ob Token existiert
|
|
344
|
+
const token = await strapi.db.query('plugin::magic-link.token').findOne({
|
|
345
|
+
where: { id },
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
if (!token) {
|
|
349
|
+
return ctx.notFound('Token nicht gefunden');
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Aktualisiere den Token auf aktiv
|
|
353
|
+
const updatedToken = await strapi.db.query('plugin::magic-link.token').update({
|
|
354
|
+
where: { id },
|
|
355
|
+
data: {
|
|
356
|
+
is_active: true,
|
|
357
|
+
},
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
return updatedToken;
|
|
361
|
+
} catch (error) {
|
|
362
|
+
strapi.log.error('Fehler beim Aktivieren des Tokens:', error);
|
|
363
|
+
ctx.throw(500, error);
|
|
364
|
+
}
|
|
365
|
+
},
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Extend token validity
|
|
369
|
+
* @param {Object} ctx - The request context
|
|
370
|
+
*/
|
|
371
|
+
async extend(ctx) {
|
|
372
|
+
try {
|
|
373
|
+
const { id } = ctx.params;
|
|
374
|
+
const { days } = ctx.request.body;
|
|
375
|
+
|
|
376
|
+
// Prüfe, ob Token existiert
|
|
377
|
+
const token = await strapi.db.query('plugin::magic-link.token').findOne({
|
|
378
|
+
where: { id },
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
if (!token) {
|
|
382
|
+
return ctx.notFound('Token nicht gefunden');
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Verarbeite die Anzahl der Tage
|
|
386
|
+
const daysToAdd = parseInt(days) || 7; // Standard: 7 Tage
|
|
387
|
+
|
|
388
|
+
// Berechne das neue Ablaufdatum
|
|
389
|
+
let newExpiryDate;
|
|
390
|
+
|
|
391
|
+
// Wenn der Token bereits abgelaufen ist, vom aktuellen Datum ausgehen
|
|
392
|
+
if (new Date(token.expires_at) < new Date()) {
|
|
393
|
+
newExpiryDate = new Date();
|
|
394
|
+
} else {
|
|
395
|
+
// Sonst vom aktuellen Ablaufdatum ausgehen
|
|
396
|
+
newExpiryDate = new Date(token.expires_at);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Tage hinzufügen
|
|
400
|
+
newExpiryDate.setDate(newExpiryDate.getDate() + daysToAdd);
|
|
401
|
+
|
|
402
|
+
// Aktualisiere den Token mit dem neuen Ablaufdatum
|
|
403
|
+
const updatedToken = await strapi.db.query('plugin::magic-link.token').update({
|
|
404
|
+
where: { id },
|
|
405
|
+
data: {
|
|
406
|
+
expires_at: newExpiryDate,
|
|
407
|
+
},
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
return updatedToken;
|
|
411
|
+
} catch (error) {
|
|
412
|
+
strapi.log.error('Fehler beim Verlängern des Tokens:', error);
|
|
413
|
+
ctx.throw(500, error);
|
|
414
|
+
}
|
|
415
|
+
},
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Find a user by email
|
|
419
|
+
* @param {Object} ctx - The request context
|
|
420
|
+
*/
|
|
421
|
+
async findUserByEmail(ctx) {
|
|
422
|
+
try {
|
|
423
|
+
const { email } = ctx.query;
|
|
424
|
+
|
|
425
|
+
if (!email) {
|
|
426
|
+
return ctx.badRequest('Email is required');
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Find the user
|
|
430
|
+
const user = await strapi.db.query('plugin::users-permissions.user').findOne({
|
|
431
|
+
where: { email },
|
|
432
|
+
select: ['id', 'username', 'email', 'confirmed', 'blocked', 'documentId', 'UUID'],
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
if (!user) {
|
|
436
|
+
return ctx.notFound('User not found');
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return user;
|
|
440
|
+
} catch (error) {
|
|
441
|
+
ctx.throw(500, error);
|
|
442
|
+
}
|
|
443
|
+
},
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Ban an IP address
|
|
447
|
+
* @param {Object} ctx - The request context
|
|
448
|
+
*/
|
|
449
|
+
async banIP(ctx) {
|
|
450
|
+
try {
|
|
451
|
+
const { data } = ctx.request.body;
|
|
452
|
+
|
|
453
|
+
if (!data || !data.ip) {
|
|
454
|
+
return ctx.badRequest('IP address is required');
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const ipAddress = data.ip;
|
|
458
|
+
|
|
459
|
+
// Get plugin store to save banned IPs
|
|
460
|
+
const pluginStore = strapi.store({
|
|
461
|
+
environment: '',
|
|
462
|
+
type: 'plugin',
|
|
463
|
+
name: 'magic-link',
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
// Get current banned IPs or initialize empty array
|
|
467
|
+
const bannedIPs = await pluginStore.get({ key: 'banned_ips' }) || { ips: [] };
|
|
468
|
+
|
|
469
|
+
// Add new IP to the list if not already present
|
|
470
|
+
if (!bannedIPs.ips.includes(ipAddress)) {
|
|
471
|
+
bannedIPs.ips.push(ipAddress);
|
|
472
|
+
|
|
473
|
+
// Save updated banned IPs list
|
|
474
|
+
await pluginStore.set({ key: 'banned_ips', value: bannedIPs });
|
|
475
|
+
|
|
476
|
+
// Deactivate all tokens associated with this IP
|
|
477
|
+
await strapi.db.query('plugin::magic-link.token').updateMany({
|
|
478
|
+
where: { ip_address: ipAddress },
|
|
479
|
+
data: { is_active: false },
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return { success: true, message: `IP ${ipAddress} has been banned` };
|
|
484
|
+
} catch (error) {
|
|
485
|
+
strapi.log.error('Error banning IP:', error);
|
|
486
|
+
ctx.throw(500, error);
|
|
487
|
+
}
|
|
488
|
+
},
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Ruft die Liste der gesperrten IP-Adressen ab
|
|
492
|
+
* @param {Object} ctx - The request context
|
|
493
|
+
*/
|
|
494
|
+
async getBannedIPs(ctx) {
|
|
495
|
+
try {
|
|
496
|
+
// Get plugin store to retrieve banned IPs
|
|
497
|
+
const pluginStore = strapi.store({
|
|
498
|
+
environment: '',
|
|
499
|
+
type: 'plugin',
|
|
500
|
+
name: 'magic-link',
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
// Get current banned IPs or initialize empty array
|
|
504
|
+
const bannedIPs = await pluginStore.get({ key: 'banned_ips' }) || { ips: [] };
|
|
505
|
+
|
|
506
|
+
return bannedIPs;
|
|
507
|
+
} catch (error) {
|
|
508
|
+
strapi.log.error('Error fetching banned IPs:', error);
|
|
509
|
+
ctx.throw(500, error);
|
|
510
|
+
}
|
|
511
|
+
},
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Entsperrt eine IP-Adresse
|
|
515
|
+
* @param {Object} ctx - The request context
|
|
516
|
+
*/
|
|
517
|
+
async unbanIP(ctx) {
|
|
518
|
+
try {
|
|
519
|
+
const { data } = ctx.request.body;
|
|
520
|
+
|
|
521
|
+
if (!data || !data.ip) {
|
|
522
|
+
return ctx.badRequest('IP address is required');
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const ipAddress = data.ip;
|
|
526
|
+
|
|
527
|
+
// Get plugin store
|
|
528
|
+
const pluginStore = strapi.store({
|
|
529
|
+
environment: '',
|
|
530
|
+
type: 'plugin',
|
|
531
|
+
name: 'magic-link',
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
// Get current banned IPs
|
|
535
|
+
const bannedIPs = await pluginStore.get({ key: 'banned_ips' }) || { ips: [] };
|
|
536
|
+
|
|
537
|
+
// Remove IP from the banned list
|
|
538
|
+
bannedIPs.ips = bannedIPs.ips.filter(ip => ip !== ipAddress);
|
|
539
|
+
|
|
540
|
+
// Save updated banned IPs list
|
|
541
|
+
await pluginStore.set({ key: 'banned_ips', value: bannedIPs });
|
|
542
|
+
|
|
543
|
+
return { success: true, message: `IP ${ipAddress} has been unbanned` };
|
|
544
|
+
} catch (error) {
|
|
545
|
+
strapi.log.error('Error unbanning IP:', error);
|
|
546
|
+
ctx.throw(500, error);
|
|
547
|
+
}
|
|
548
|
+
},
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Berechnet einen Sicherheitswert basierend auf verschiedenen Plugin-Metriken
|
|
552
|
+
* @returns {Promise<number>} Sicherheitswert zwischen 0 und 100
|
|
553
|
+
*/
|
|
554
|
+
async calculateSecurityScore() {
|
|
555
|
+
try {
|
|
556
|
+
let score = 0;
|
|
557
|
+
const maxScore = 100;
|
|
558
|
+
|
|
559
|
+
// Hol die Plugin-Einstellungen
|
|
560
|
+
const pluginStore = strapi.store({
|
|
561
|
+
environment: '',
|
|
562
|
+
type: 'plugin',
|
|
563
|
+
name: 'magic-link',
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
const settings = await pluginStore.get({ key: 'settings' }) || {};
|
|
567
|
+
|
|
568
|
+
// 1. Bewerte Token-Lebensdauer: max. 20 Punkte
|
|
569
|
+
// (Kürzere Lebensdauer ist sicherer)
|
|
570
|
+
let tokenLifetimePoints = 0;
|
|
571
|
+
const tokenLifetime = settings.token_lifetime || 24; // Standardwert: 24 Stunden
|
|
572
|
+
if (tokenLifetime <= 1) tokenLifetimePoints = 20; // 1 Stunde oder weniger
|
|
573
|
+
else if (tokenLifetime <= 6) tokenLifetimePoints = 15; // 6 Stunden oder weniger
|
|
574
|
+
else if (tokenLifetime <= 12) tokenLifetimePoints = 10; // 12 Stunden oder weniger
|
|
575
|
+
else if (tokenLifetime <= 24) tokenLifetimePoints = 5; // 24 Stunden oder weniger
|
|
576
|
+
|
|
577
|
+
// 2. Bewerte JWT-Lebensdauer: max. 20 Punkte
|
|
578
|
+
let jwtLifetimePoints = 0;
|
|
579
|
+
const jwtLifetime = settings.jwt_token_expires_in || '30d';
|
|
580
|
+
const jwtDays = jwtLifetime.endsWith('d') ? parseInt(jwtLifetime) :
|
|
581
|
+
jwtLifetime.endsWith('h') ? parseInt(jwtLifetime) / 24 : 30;
|
|
582
|
+
|
|
583
|
+
if (jwtDays <= 1) jwtLifetimePoints = 20; // 1 Tag oder weniger
|
|
584
|
+
else if (jwtDays <= 7) jwtLifetimePoints = 15; // 1 Woche oder weniger
|
|
585
|
+
else if (jwtDays <= 14) jwtLifetimePoints = 10; // 2 Wochen oder weniger
|
|
586
|
+
else if (jwtDays <= 30) jwtLifetimePoints = 5; // 1 Monat oder weniger
|
|
587
|
+
|
|
588
|
+
// 3. Bewerte Sicherheitseinstellungen: max. 30 Punkte
|
|
589
|
+
let configPoints = 0;
|
|
590
|
+
|
|
591
|
+
// Ist das Auto-Create-User Feature deaktiviert? (sicherer)
|
|
592
|
+
if (settings.create_new_user === false) configPoints += 10;
|
|
593
|
+
|
|
594
|
+
// Ist das Email-Send Feature aktiviert? (sicherer)
|
|
595
|
+
if (settings.enabled === true) configPoints += 5;
|
|
596
|
+
|
|
597
|
+
// Ist Remember Me deaktiviert? (sicherer)
|
|
598
|
+
if (settings.remember_me === false) configPoints += 5;
|
|
599
|
+
|
|
600
|
+
// Ist stays_valid deaktiviert? (sicherer)
|
|
601
|
+
if (settings.stays_valid === false) configPoints += 10;
|
|
602
|
+
|
|
603
|
+
// 4. Bewerte Token-Status: max. 15 Punkte
|
|
604
|
+
let tokenStatusPoints = 0;
|
|
605
|
+
|
|
606
|
+
// Hole alle Tokens
|
|
607
|
+
const tokens = await strapi.db.query('plugin::magic-link.token').findMany({});
|
|
608
|
+
|
|
609
|
+
// Berechne Verhältnis von aktiven zu inaktiven Tokens
|
|
610
|
+
const activeTokens = tokens.filter(token => token.is_active).length;
|
|
611
|
+
const inactiveTokens = tokens.filter(token => !token.is_active).length;
|
|
612
|
+
const tokenRatio = tokens.length > 0 ? inactiveTokens / tokens.length : 0;
|
|
613
|
+
|
|
614
|
+
// Bewerte basierend auf dem Verhältnis (mehr inaktive = höhere Sicherheit)
|
|
615
|
+
if (tokenRatio >= 0.8) tokenStatusPoints = 15;
|
|
616
|
+
else if (tokenRatio >= 0.6) tokenStatusPoints = 10;
|
|
617
|
+
else if (tokenRatio >= 0.4) tokenStatusPoints = 5;
|
|
618
|
+
|
|
619
|
+
// 5. Bewerte gebannte IPs: max. 15 Punkte
|
|
620
|
+
let bannedIPsPoints = 0;
|
|
621
|
+
|
|
622
|
+
// Hole gebannte IPs
|
|
623
|
+
const bannedIPsData = await pluginStore.get({ key: 'banned_ips' }) || { ips: [] };
|
|
624
|
+
const bannedIPsCount = bannedIPsData.ips?.length || 0;
|
|
625
|
+
|
|
626
|
+
// Bewerte basierend auf Anzahl der gebannten IPs
|
|
627
|
+
if (bannedIPsCount >= 10) bannedIPsPoints = 15;
|
|
628
|
+
else if (bannedIPsCount >= 5) bannedIPsPoints = 10;
|
|
629
|
+
else if (bannedIPsCount >= 1) bannedIPsPoints = 5;
|
|
630
|
+
|
|
631
|
+
// Gesamtpunktzahl berechnen
|
|
632
|
+
score = tokenLifetimePoints + jwtLifetimePoints + configPoints + tokenStatusPoints + bannedIPsPoints;
|
|
633
|
+
|
|
634
|
+
// Stelle sicher, dass der Score im Bereich 0-100 liegt
|
|
635
|
+
return Math.min(Math.max(score, 0), maxScore);
|
|
636
|
+
} catch (error) {
|
|
637
|
+
console.error('Fehler bei der Berechnung des Sicherheitswerts:', error);
|
|
638
|
+
return 85; // Fallback auf den Standardwert
|
|
639
|
+
}
|
|
640
|
+
},
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Get the security score
|
|
644
|
+
* @param {Object} ctx - The request context
|
|
645
|
+
*/
|
|
646
|
+
async getSecurityScore(ctx) {
|
|
647
|
+
try {
|
|
648
|
+
const score = await this.calculateSecurityScore();
|
|
649
|
+
return { score };
|
|
650
|
+
} catch (error) {
|
|
651
|
+
ctx.throw(500, error);
|
|
652
|
+
}
|
|
653
|
+
},
|
|
654
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Application methods
|
|
5
|
+
*/
|
|
6
|
+
const bootstrap = require('./bootstrap');
|
|
7
|
+
const destroy = require('./destroy');
|
|
8
|
+
const register = require('./register');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Plugin server methods
|
|
12
|
+
*/
|
|
13
|
+
const config = require('./config');
|
|
14
|
+
const contentTypes = require('./content-types');
|
|
15
|
+
const controllers = require('./controllers');
|
|
16
|
+
const middlewares = require('./middlewares');
|
|
17
|
+
const policies = require('./policies');
|
|
18
|
+
const routes = require('./routes');
|
|
19
|
+
const services = require('./services');
|
|
20
|
+
|
|
21
|
+
module.exports = {
|
|
22
|
+
bootstrap,
|
|
23
|
+
destroy,
|
|
24
|
+
register,
|
|
25
|
+
|
|
26
|
+
config,
|
|
27
|
+
controllers,
|
|
28
|
+
contentTypes,
|
|
29
|
+
middlewares,
|
|
30
|
+
policies,
|
|
31
|
+
routes,
|
|
32
|
+
services,
|
|
33
|
+
};
|