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.
Files changed (61) hide show
  1. package/.eslintignore +1 -0
  2. package/.github/workflows/semantic-release.yml +27 -0
  3. package/README.md +318 -0
  4. package/admin/jsconfig.json +10 -0
  5. package/admin/src/components/Initializer/index.jsx +20 -0
  6. package/admin/src/components/Initializer.jsx +18 -0
  7. package/admin/src/components/LazyComponentLoader.jsx +27 -0
  8. package/admin/src/components/PluginIcon/index.jsx +6 -0
  9. package/admin/src/components/PluginIcon.jsx +5 -0
  10. package/admin/src/index.js +101 -0
  11. package/admin/src/pages/App/index.jsx +50 -0
  12. package/admin/src/pages/App.jsx +15 -0
  13. package/admin/src/pages/HomePage/index.js +2 -0
  14. package/admin/src/pages/HomePage/index.jsx +228 -0
  15. package/admin/src/pages/HomePage.jsx +655 -0
  16. package/admin/src/pages/Settings/index.jsx +1289 -0
  17. package/admin/src/pages/Settings/utils/api.js +13 -0
  18. package/admin/src/pages/Settings/utils/index.js +5 -0
  19. package/admin/src/pages/Settings/utils/layout.js +100 -0
  20. package/admin/src/pages/Settings/utils/schema.js +18 -0
  21. package/admin/src/pages/Tokens/index.jsx +2250 -0
  22. package/admin/src/permissions.js +7 -0
  23. package/admin/src/pluginId.js +3 -0
  24. package/admin/src/routes.js +40 -0
  25. package/admin/src/translations/de.json +188 -0
  26. package/admin/src/translations/en.json +189 -0
  27. package/admin/src/utils/getRequestURL.js +5 -0
  28. package/admin/src/utils/getTrad.js +17 -0
  29. package/admin/src/utils/getTranslation.js +3 -0
  30. package/admin/src/utils/index.js +4 -0
  31. package/build.js +75 -0
  32. package/package.json +59 -0
  33. package/server/bootstrap.js +127 -0
  34. package/server/controllers/settings.js +122 -0
  35. package/server/jsconfig.json +10 -0
  36. package/server/services/store.js +35 -0
  37. package/server/src/bootstrap.js +110 -0
  38. package/server/src/config/index.js +6 -0
  39. package/server/src/content-types/index.js +7 -0
  40. package/server/src/content-types/token/index.js +5 -0
  41. package/server/src/content-types/token/schema.json +47 -0
  42. package/server/src/controllers/auth.js +211 -0
  43. package/server/src/controllers/controller.js +213 -0
  44. package/server/src/controllers/index.js +16 -0
  45. package/server/src/controllers/jwt.js +261 -0
  46. package/server/src/controllers/tokens.js +654 -0
  47. package/server/src/destroy.js +5 -0
  48. package/server/src/index.js +33 -0
  49. package/server/src/middlewares/index.js +3 -0
  50. package/server/src/policies/index.js +3 -0
  51. package/server/src/register.js +5 -0
  52. package/server/src/routes/admin.js +160 -0
  53. package/server/src/routes/content-api.js +27 -0
  54. package/server/src/routes/index.js +9 -0
  55. package/server/src/services/index.js +11 -0
  56. package/server/src/services/magic-link.js +356 -0
  57. package/server/src/services/service.js +13 -0
  58. package/server/utils/index.js +14 -0
  59. package/strapi-admin.js +82 -0
  60. package/strapi-server.js +4 -0
  61. package/vite.config.js +36 -0
@@ -0,0 +1,213 @@
1
+ 'use strict';
2
+
3
+ module.exports = {
4
+ async getSettings(ctx) {
5
+ try {
6
+ const pluginStore = strapi.store({
7
+ environment: '',
8
+ type: 'plugin',
9
+ name: 'magic-link',
10
+ });
11
+
12
+ const settings = await pluginStore.get({ key: 'settings' });
13
+
14
+ // Stelle sicher, dass alle Boolean-Werte korrekt formatiert sind
15
+ const processedSettings = { ...settings };
16
+
17
+ // Entferne verschachtelte settings-Objekte, falls vorhanden
18
+ if (processedSettings && processedSettings.settings) {
19
+ delete processedSettings.settings;
20
+ }
21
+
22
+ // Verarbeite alle Eigenschaften, die als Boolean behandelt werden sollten
23
+ const booleanFields = [
24
+ 'enabled', 'createUserIfNotExists', 'stays_valid', 'verify_email',
25
+ 'welcome_email', 'use_jwt_token', 'allow_magic_links_on_public_registration',
26
+ 'store_login_info', 'use_email_designer'
27
+ ];
28
+
29
+ booleanFields.forEach(field => {
30
+ if (field in processedSettings) {
31
+ // Konvertiere zu echten Boolean-Werten
32
+ processedSettings[field] = !!processedSettings[field];
33
+ }
34
+ });
35
+
36
+ // Ensure store_login_info has a value
37
+ if (processedSettings.store_login_info === undefined) {
38
+ processedSettings.store_login_info = true;
39
+ }
40
+
41
+ // Ensure email designer settings have values
42
+ if (processedSettings.use_email_designer === undefined) {
43
+ processedSettings.use_email_designer = false;
44
+ }
45
+
46
+ if (processedSettings.email_designer_template_id === undefined) {
47
+ processedSettings.email_designer_template_id = '';
48
+ }
49
+
50
+ // Korrigiere den Login-Pfad, wenn er den alten Wert hat
51
+ if (processedSettings.login_path === '/passwordless-login' || processedSettings.login_path === '/api/magic-link/login') {
52
+ processedSettings.login_path = '/magic-link/login';
53
+ }
54
+
55
+ // Check if Email Designer plugin is installed
56
+ const isEmailDesignerInstalled = !!strapi.plugin('email-designer-5');
57
+
58
+ ctx.send({
59
+ settings: processedSettings,
60
+ emailDesignerInstalled: isEmailDesignerInstalled
61
+ });
62
+ } catch (error) {
63
+ ctx.throw(500, error);
64
+ }
65
+ },
66
+
67
+ async updateSettings(ctx) {
68
+ const { body } = ctx.request;
69
+
70
+ try {
71
+ // Stelle sicher, dass alle Boolean-Werte korrekt formatiert sind
72
+ const processedBody = { ...body };
73
+
74
+ // Entferne verschachtelte settings-Objekte, falls vorhanden
75
+ if (processedBody && processedBody.settings) {
76
+ delete processedBody.settings;
77
+ }
78
+
79
+ // Verarbeite alle Eigenschaften, die als Boolean behandelt werden sollten
80
+ const booleanFields = [
81
+ 'enabled', 'createUserIfNotExists', 'stays_valid', 'verify_email',
82
+ 'welcome_email', 'use_jwt_token', 'allow_magic_links_on_public_registration',
83
+ 'store_login_info', 'use_email_designer'
84
+ ];
85
+
86
+ booleanFields.forEach(field => {
87
+ if (field in processedBody) {
88
+ // Konvertiere verschiedene Formate zu echten Boolean-Werten
89
+ if (typeof processedBody[field] === 'string') {
90
+ processedBody[field] = processedBody[field] === 'true';
91
+ } else if (typeof processedBody[field] === 'object' && processedBody[field]?.type === 'boolean') {
92
+ processedBody[field] = !!processedBody[field].value;
93
+ } else {
94
+ processedBody[field] = !!processedBody[field];
95
+ }
96
+ }
97
+ });
98
+
99
+ // Ensure store_login_info is included in the stored settings
100
+ if (processedBody.store_login_info === undefined) {
101
+ processedBody.store_login_info = true;
102
+ }
103
+
104
+ const pluginStore = strapi.store({
105
+ environment: '',
106
+ type: 'plugin',
107
+ name: 'magic-link',
108
+ });
109
+
110
+ await pluginStore.set({ key: 'settings', value: processedBody });
111
+ ctx.send({ settings: processedBody });
112
+ } catch (error) {
113
+ ctx.throw(500, error);
114
+ }
115
+ },
116
+
117
+ async index(ctx) {
118
+ ctx.body = { message: 'Welcome to Magic Link plugin!' };
119
+ },
120
+
121
+ /**
122
+ * Setzt alle Magic Link Daten und Einstellungen zurück
123
+ * @param {Object} ctx - Context
124
+ */
125
+ async resetData(ctx) {
126
+ try {
127
+ // Plugin Store für die Einstellungen
128
+ const pluginStore = strapi.store({
129
+ environment: '',
130
+ type: 'plugin',
131
+ name: 'magic-link',
132
+ });
133
+
134
+ // Standardeinstellungen definieren
135
+ const defaultSettings = {
136
+ enabled: true,
137
+ createUserIfNotExists: false,
138
+ stays_valid: false,
139
+ expire_period: 3600,
140
+ token_length: 20,
141
+ max_login_attempts: 5,
142
+ login_path: '/magic-link/login',
143
+ confirmationUrl: process.env.FRONTEND_URL || 'http://localhost:3000',
144
+ store_login_info: true,
145
+ default_role: 'authenticated',
146
+ object: 'Your Magic Link for Login',
147
+ from_name: 'Magic Link Service',
148
+ from_email: 'noreply@example.com',
149
+ message_html: `<!DOCTYPE html>
150
+ <html>
151
+ <head>
152
+ <meta charset="utf-8">
153
+ <meta http-equiv="x-ua-compatible" content="ie=edge">
154
+ <title>Magic Link Login</title>
155
+ </head>
156
+ <body>
157
+ <h2>Magic Link Login</h2>
158
+ <p>Click the link below to log in:</p>
159
+ <p><a href="<%= URL %>?loginToken=<%= CODE %>">Log in to your account</a></p>
160
+ <p>Or use this URL: <%= URL %>?loginToken=<%= CODE %></p>
161
+ <p>This link will expire in 1 hour.</p>
162
+ </body>
163
+ </html>`,
164
+ message_text: `Hello,
165
+
166
+ Click the link below to log in:
167
+
168
+ <%= URL %>?loginToken=<%= CODE %>
169
+
170
+ This link will expire in 1 hour.`,
171
+ jwt_token_expires_in: '30d'
172
+ };
173
+
174
+ // Einstellungen zurücksetzen
175
+ await pluginStore.set({ key: 'settings', value: defaultSettings });
176
+
177
+ // Alle Magic Link Tokens löschen
178
+ await strapi.db.query('plugin::magic-link.token').deleteMany({
179
+ where: {},
180
+ });
181
+
182
+ // JWT Sessions löschen
183
+ try {
184
+ await pluginStore.delete({ key: 'jwt_sessions' });
185
+ } catch (error) {
186
+ console.error('Fehler beim Löschen der JWT Sessions:', error);
187
+ }
188
+
189
+ // Gebannte IPs zurücksetzen
190
+ try {
191
+ await pluginStore.set({ key: 'banned_ips', value: { ips: [] } });
192
+ } catch (error) {
193
+ console.error('Fehler beim Zurücksetzen der gebannten IPs:', error);
194
+ }
195
+
196
+ // Gesperrte JWT-Tokens zurücksetzen
197
+ try {
198
+ await pluginStore.set({ key: 'blocked_jwt_tokens', value: { tokens: [] } });
199
+ } catch (error) {
200
+ console.error('Fehler beim Zurücksetzen der gesperrten JWT-Tokens:', error);
201
+ }
202
+
203
+ // Erfolgreiche Antwort senden
204
+ ctx.send({
205
+ success: true,
206
+ message: 'Alle Magic Link Daten wurden zurückgesetzt.',
207
+ });
208
+ } catch (error) {
209
+ console.error('Fehler beim Zurücksetzen der Magic Link Daten:', error);
210
+ ctx.throw(500, error);
211
+ }
212
+ },
213
+ };
@@ -0,0 +1,16 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Controllers
5
+ */
6
+ const controller = require('./controller');
7
+ const auth = require('./auth');
8
+ const tokens = require('./tokens');
9
+ const jwt = require('./jwt');
10
+
11
+ module.exports = {
12
+ controller,
13
+ auth,
14
+ tokens,
15
+ jwt,
16
+ };
@@ -0,0 +1,261 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * JWT controller für die Verwaltung von JWT-Tokens
5
+ */
6
+
7
+ module.exports = {
8
+ /**
9
+ * Alle aktiven JWT-Sessions abrufen
10
+ * @param {Object} ctx - Koa-Kontext
11
+ */
12
+ async getSessions(ctx) {
13
+ try {
14
+ // Aus dem Plugin-Store auslesen
15
+ const pluginStore = strapi.store({
16
+ environment: '',
17
+ type: 'plugin',
18
+ name: 'magic-link',
19
+ });
20
+
21
+ const storedData = await pluginStore.get({ key: 'jwt_sessions' }) || { sessions: [] };
22
+ const jwtSessions = storedData.sessions || [];
23
+
24
+ // Aktuelles Datum für Ablaufprüfung
25
+ const now = new Date();
26
+
27
+ // Aufbereitung der Daten für die Anzeige
28
+ const sessions = jwtSessions.map(session => ({
29
+ id: session.id,
30
+ userId: session.userId,
31
+ username: session.username,
32
+ email: session.userEmail,
33
+ token: session.jwtToken ? session.jwtToken.substring(0, 30) + '...' : 'N/A', // Nur Teile des Tokens anzeigen
34
+ createdAt: session.createdAt,
35
+ expiresAt: session.expiresAt,
36
+ ipAddress: session.ipAddress,
37
+ userAgent: session.userAgent || 'Unbekannt',
38
+ source: session.source || 'Magic Link Login',
39
+ revoked: session.isRevoked,
40
+ isExpired: new Date(session.expiresAt) < now
41
+ }));
42
+
43
+ ctx.send(sessions);
44
+ } catch (error) {
45
+ console.error('Error fetching JWT sessions:', error);
46
+ ctx.throw(500, error);
47
+ }
48
+ },
49
+
50
+ /**
51
+ * Ein JWT-Token sperren
52
+ * @param {Object} ctx - Koa-Kontext
53
+ */
54
+ async revokeToken(ctx) {
55
+ try {
56
+ const { token, sessionId } = ctx.request.body;
57
+
58
+ // Hole aktuelle JWT-Sessions
59
+ const pluginStore = strapi.store({
60
+ environment: '',
61
+ type: 'plugin',
62
+ name: 'magic-link',
63
+ });
64
+
65
+ const storedData = await pluginStore.get({ key: 'jwt_sessions' }) || { sessions: [] };
66
+ let jwtSessions = storedData.sessions || [];
67
+
68
+ // Möglichkeit 1: Sperren über sessionId
69
+ if (sessionId) {
70
+ // Finde die Session anhand der ID
71
+ const sessionIndex = jwtSessions.findIndex(s => s.id === sessionId);
72
+
73
+ if (sessionIndex === -1) {
74
+ return ctx.badRequest('Session not found');
75
+ }
76
+
77
+ // Markiere Session als gesperrt
78
+ jwtSessions[sessionIndex].isRevoked = true;
79
+ jwtSessions[sessionIndex].revokedAt = new Date().toISOString();
80
+ jwtSessions[sessionIndex].revokeReason = 'Manually revoked from admin UI';
81
+
82
+ // Sperrung auch in der Sperrliste erfassen
83
+ if (jwtSessions[sessionIndex].jwtToken) {
84
+ const { magicLink } = strapi.plugins['magic-link'].services;
85
+ await magicLink.blockJwtToken(
86
+ jwtSessions[sessionIndex].jwtToken,
87
+ jwtSessions[sessionIndex].userId,
88
+ 'Manually revoked from admin UI'
89
+ );
90
+ }
91
+
92
+ // Speichere aktualisierte Liste
93
+ await pluginStore.set({ key: 'jwt_sessions', value: { sessions: jwtSessions } });
94
+
95
+ return ctx.send({
96
+ success: true,
97
+ message: 'JWT session revoked successfully'
98
+ });
99
+ }
100
+
101
+ // Möglichkeit 2: Sperren über Token (Legacy)
102
+ if (token) {
103
+ const { magicLink } = strapi.plugins['magic-link'].services;
104
+ const result = await magicLink.blockJwtToken(
105
+ token,
106
+ ctx.request.body.userId || 'unknown',
107
+ 'Manually revoked from admin UI'
108
+ );
109
+
110
+ // Auch alle Sessions mit diesem Token sperren
111
+ const tokenPrefix = token.substring(0, 30);
112
+
113
+ jwtSessions = jwtSessions.map(session => {
114
+ // Prüfe, ob der Token-Anfang übereinstimmt
115
+ if (session.jwtToken && session.jwtToken.startsWith(tokenPrefix)) {
116
+ return {
117
+ ...session,
118
+ isRevoked: true,
119
+ revokedAt: new Date().toISOString(),
120
+ revokeReason: 'Manually revoked from admin UI via token'
121
+ };
122
+ }
123
+ return session;
124
+ });
125
+
126
+ // Speichere aktualisierte Liste
127
+ await pluginStore.set({ key: 'jwt_sessions', value: { sessions: jwtSessions } });
128
+
129
+ return ctx.send({
130
+ success: true,
131
+ message: "Die Session wurde erfolgreich gesperrt."
132
+ });
133
+ }
134
+
135
+ return ctx.badRequest('Token or sessionId is required');
136
+ } catch (error) {
137
+ console.error('Error revoking JWT token:', error);
138
+ ctx.throw(500, error);
139
+ }
140
+ },
141
+
142
+ /**
143
+ * Ein JWT-Token entsperren
144
+ * @param {Object} ctx - Koa-Kontext
145
+ */
146
+ async unrevokeToken(ctx) {
147
+ try {
148
+ const { sessionId, userId } = ctx.request.body;
149
+
150
+ if (!sessionId) {
151
+ return ctx.badRequest('Session ID is required');
152
+ }
153
+
154
+ // Hole aktuelle JWT-Sessions
155
+ const pluginStore = strapi.store({
156
+ environment: '',
157
+ type: 'plugin',
158
+ name: 'magic-link',
159
+ });
160
+
161
+ const storedData = await pluginStore.get({ key: 'jwt_sessions' }) || { sessions: [] };
162
+ let jwtSessions = storedData.sessions || [];
163
+
164
+ // Finde die Session anhand der ID
165
+ const sessionIndex = jwtSessions.findIndex(s => s.id === sessionId);
166
+
167
+ if (sessionIndex === -1) {
168
+ return ctx.badRequest('Session not found');
169
+ }
170
+
171
+ // Prüfe, ob die Session abgelaufen ist
172
+ const expiresAt = new Date(jwtSessions[sessionIndex].expiresAt);
173
+ const now = new Date();
174
+
175
+ if (expiresAt < now) {
176
+ return ctx.badRequest('Cannot unrevoke an expired session');
177
+ }
178
+
179
+ // Session entsperren
180
+ jwtSessions[sessionIndex].isRevoked = false;
181
+ jwtSessions[sessionIndex].revokedAt = null;
182
+ jwtSessions[sessionIndex].revokeReason = null;
183
+
184
+ // Token aus der Sperrliste entfernen, falls vorhanden
185
+ if (jwtSessions[sessionIndex].jwtToken) {
186
+ const { magicLink } = strapi.plugins['magic-link'].services;
187
+
188
+ try {
189
+ // JWT von der Blacklist entfernen
190
+ await magicLink.unblockJwtToken(
191
+ jwtSessions[sessionIndex].jwtToken,
192
+ userId || jwtSessions[sessionIndex].userId
193
+ );
194
+ } catch (error) {
195
+ console.warn('Warning: Could not unblock JWT token from blacklist:', error);
196
+ // Fehler ignorieren, da der Token möglicherweise nicht in der Blacklist ist
197
+ }
198
+ }
199
+
200
+ // Speichere aktualisierte Liste
201
+ await pluginStore.set({ key: 'jwt_sessions', value: { sessions: jwtSessions } });
202
+
203
+ return ctx.send({
204
+ success: true,
205
+ message: 'JWT session unrevoked successfully'
206
+ });
207
+ } catch (error) {
208
+ console.error('Error unrevoking JWT token:', error);
209
+ ctx.throw(500, error);
210
+ }
211
+ },
212
+
213
+ /**
214
+ * Alle abgelaufenen und gesperrten Sessions aufräumen
215
+ * @param {Object} ctx - Koa-Kontext
216
+ */
217
+ async cleanupSessions(ctx) {
218
+ try {
219
+ const now = new Date();
220
+
221
+ // Hole aktuelle JWT-Sessions
222
+ const pluginStore = strapi.store({
223
+ environment: '',
224
+ type: 'plugin',
225
+ name: 'magic-link',
226
+ });
227
+
228
+ const storedData = await pluginStore.get({ key: 'jwt_sessions' }) || { sessions: [] };
229
+ let jwtSessions = storedData.sessions || [];
230
+
231
+ // Identifiziere abgelaufene Sessions
232
+ const expiredSessionIds = [];
233
+
234
+ jwtSessions = jwtSessions.map(session => {
235
+ // Prüfe, ob abgelaufen und nicht bereits gesperrt
236
+ if (new Date(session.expiresAt) < now && !session.isRevoked) {
237
+ expiredSessionIds.push(session.id);
238
+ return {
239
+ ...session,
240
+ isRevoked: true,
241
+ revokedAt: now.toISOString(),
242
+ revokeReason: 'Automatically expired'
243
+ };
244
+ }
245
+ return session;
246
+ });
247
+
248
+ // Speichere aktualisierte Liste
249
+ await pluginStore.set({ key: 'jwt_sessions', value: { sessions: jwtSessions } });
250
+
251
+ ctx.send({
252
+ success: true,
253
+ count: expiredSessionIds.length,
254
+ message: `${expiredSessionIds.length} abgelaufene Sessions wurden aufgeräumt.`
255
+ });
256
+ } catch (error) {
257
+ console.error('Error cleaning up sessions:', error);
258
+ ctx.throw(500, error);
259
+ }
260
+ }
261
+ };