strapi-plugin-magic-link-v5 4.9.4 → 4.11.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.
@@ -138,8 +138,13 @@ const SettingsModern = () => {
138
138
  callback_url: '',
139
139
  allow_magic_links_on_public_registration: false,
140
140
  store_login_info: true,
141
- ui_language: 'en'
141
+ ui_language: 'en',
142
+ rate_limit_enabled: true,
143
+ rate_limit_max_attempts: 5,
144
+ rate_limit_window_minutes: 15
142
145
  });
146
+
147
+ const [rateLimitStats, setRateLimitStats] = useState(null);
143
148
 
144
149
  const loadSettings = useCallback(async () => {
145
150
  setIsLoading(true);
@@ -147,6 +152,16 @@ const SettingsModern = () => {
147
152
  const res = await get('/magic-link/settings');
148
153
  const settingsData = res.data.settings || res.data.data || res.data;
149
154
  setSettings(prev => ({ ...prev, ...settingsData }));
155
+
156
+ // Load rate limit stats
157
+ try {
158
+ const statsRes = await get('/magic-link/rate-limit/stats');
159
+ if (statsRes?.data?.data) {
160
+ setRateLimitStats(statsRes.data.data);
161
+ }
162
+ } catch (error) {
163
+ console.error('Error loading rate limit stats:', error);
164
+ }
150
165
  } catch (error) {
151
166
  if (error.name !== 'AbortError') {
152
167
  console.error('Error loading settings:', error);
@@ -194,6 +209,40 @@ const SettingsModern = () => {
194
209
  const handleLanguageChange = (newLang) => {
195
210
  changeLanguage(newLang);
196
211
  };
212
+
213
+ const handleRateLimitCleanup = async () => {
214
+ try {
215
+ await get('/magic-link/rate-limit/cleanup');
216
+ toggleNotification({
217
+ type: 'success',
218
+ message: formatMessage({ id: getTrad('settings.rateLimit.cleanupSuccess') })
219
+ });
220
+ loadSettings();
221
+ } catch (error) {
222
+ toggleNotification({
223
+ type: 'danger',
224
+ message: formatMessage({ id: getTrad('settings.rateLimit.cleanupError') })
225
+ });
226
+ }
227
+ };
228
+
229
+ const handleRateLimitReset = async () => {
230
+ if (window.confirm(formatMessage({ id: getTrad('settings.rateLimit.resetConfirm') }))) {
231
+ try {
232
+ await get('/magic-link/rate-limit/reset');
233
+ toggleNotification({
234
+ type: 'success',
235
+ message: formatMessage({ id: getTrad('settings.rateLimit.resetSuccess') })
236
+ });
237
+ loadSettings();
238
+ } catch (error) {
239
+ toggleNotification({
240
+ type: 'danger',
241
+ message: formatMessage({ id: getTrad('settings.rateLimit.resetError') })
242
+ });
243
+ }
244
+ }
245
+ };
197
246
 
198
247
  if (isLoading) {
199
248
  return (
@@ -1114,6 +1163,159 @@ ${language === 'de' ? 'Der Link läuft in 1 Stunde ab.' : 'The link expires in 1
1114
1163
  </Box>
1115
1164
  </Accordion.Content>
1116
1165
  </Accordion.Item>
1166
+
1167
+ {/* Rate Limiting */}
1168
+ <Accordion.Item value="ratelimit">
1169
+ <Accordion.Header>
1170
+ <Accordion.Trigger
1171
+ icon={Shield}
1172
+ description={formatMessage({ id: getTrad('settings.section.rateLimit.description') })}
1173
+ >
1174
+ {formatMessage({ id: getTrad('settings.section.rateLimit') })}
1175
+ </Accordion.Trigger>
1176
+ </Accordion.Header>
1177
+ <Accordion.Content>
1178
+ <Box padding={6}>
1179
+ {/* Rate Limit Toggle */}
1180
+ <Box background="neutral100" padding={5} style={{ borderRadius: theme.borderRadius.md, marginBottom: '24px' }}>
1181
+ <Typography variant="sigma" fontWeight="bold" style={{ marginBottom: '8px', display: 'block', textAlign: 'center', color: theme.colors.neutral[700] }}>
1182
+ {formatMessage({ id: getTrad('settings.rateLimit.title') })}
1183
+ </Typography>
1184
+ <Typography variant="pi" textColor="neutral600" style={{ marginBottom: '20px', display: 'block', textAlign: 'center', fontSize: '12px' }}>
1185
+ {formatMessage({ id: getTrad('settings.rateLimit.subtitle') })}
1186
+ </Typography>
1187
+ <Grid.Root gap={4}>
1188
+ <Grid.Item col={12}>
1189
+ <ToggleCard $active={settings.rate_limit_enabled} $statusLabel={settings.rate_limit_enabled ? statusActive : statusInactive}>
1190
+ <Flex direction="column" gap={3}>
1191
+ <Flex justifyContent="center" alignItems="center" style={{ marginBottom: '8px' }}>
1192
+ <Toggle
1193
+ checked={settings.rate_limit_enabled}
1194
+ onChange={(e) => updateSetting('rate_limit_enabled', e.target.checked)}
1195
+ size="L"
1196
+ />
1197
+ </Flex>
1198
+ <Box>
1199
+ <Typography variant="pi" fontWeight="bold" style={{ fontSize: '14px', marginBottom: '6px', display: 'block', textAlign: 'center' }}>
1200
+ {formatMessage({ id: getTrad('settings.rateLimit.enable.title') })}
1201
+ </Typography>
1202
+ <Typography variant="omega" textColor="neutral600" style={{ fontSize: '12px', lineHeight: '1.4', textAlign: 'center' }}>
1203
+ {formatMessage({ id: getTrad('settings.rateLimit.enable.description') })}
1204
+ </Typography>
1205
+ </Box>
1206
+ </Flex>
1207
+ </ToggleCard>
1208
+ </Grid.Item>
1209
+ </Grid.Root>
1210
+ </Box>
1211
+
1212
+ {/* Rate Limit Configuration */}
1213
+ <Typography variant="sigma" fontWeight="bold" style={{ marginBottom: '16px', display: 'block', color: theme.colors.neutral[700] }}>
1214
+ {formatMessage({ id: getTrad('settings.rateLimit.config.title') })}
1215
+ </Typography>
1216
+ <Grid.Root gap={6} style={{ marginBottom: '32px' }}>
1217
+ <Grid.Item col={6} s={12}>
1218
+ <Box>
1219
+ <Typography variant="pi" fontWeight="bold" style={{ marginBottom: '8px', display: 'block' }}>
1220
+ {formatMessage({ id: getTrad('settings.rateLimit.maxAttempts.label') })}
1221
+ </Typography>
1222
+ <NumberInput
1223
+ hint={formatMessage({ id: getTrad('settings.rateLimit.maxAttempts.hint') })}
1224
+ value={settings.rate_limit_max_attempts}
1225
+ onChange={val => updateSetting('rate_limit_max_attempts', val)}
1226
+ min={1}
1227
+ max={100}
1228
+ />
1229
+ </Box>
1230
+ </Grid.Item>
1231
+ <Grid.Item col={6} s={12}>
1232
+ <Box>
1233
+ <Typography variant="pi" fontWeight="bold" style={{ marginBottom: '8px', display: 'block' }}>
1234
+ {formatMessage({ id: getTrad('settings.rateLimit.windowMinutes.label') })}
1235
+ </Typography>
1236
+ <NumberInput
1237
+ hint={formatMessage({ id: getTrad('settings.rateLimit.windowMinutes.hint') })}
1238
+ value={settings.rate_limit_window_minutes}
1239
+ onChange={val => updateSetting('rate_limit_window_minutes', val)}
1240
+ min={1}
1241
+ max={1440}
1242
+ />
1243
+ </Box>
1244
+ </Grid.Item>
1245
+ </Grid.Root>
1246
+
1247
+ {/* Rate Limit Stats */}
1248
+ {rateLimitStats && (
1249
+ <>
1250
+ <Divider style={{ marginBottom: '24px' }} />
1251
+ <Typography variant="sigma" fontWeight="bold" style={{ marginBottom: '16px', display: 'block', color: theme.colors.neutral[700] }}>
1252
+ {formatMessage({ id: getTrad('settings.rateLimit.stats.title') })}
1253
+ </Typography>
1254
+ <Grid.Root gap={4} style={{ marginBottom: '24px' }}>
1255
+ <Grid.Item col={3} s={6} xs={12}>
1256
+ <Box padding={4} background="neutral100" style={{ borderRadius: '8px', textAlign: 'center' }}>
1257
+ <Typography variant="alpha" style={{ color: theme.colors.primary[600] }}>
1258
+ {rateLimitStats.totalEntries || 0}
1259
+ </Typography>
1260
+ <Typography variant="pi" textColor="neutral600" style={{ fontSize: '11px', marginTop: '4px' }}>
1261
+ {formatMessage({ id: getTrad('settings.rateLimit.stats.total') })}
1262
+ </Typography>
1263
+ </Box>
1264
+ </Grid.Item>
1265
+ <Grid.Item col={3} s={6} xs={12}>
1266
+ <Box padding={4} background="danger100" style={{ borderRadius: '8px', textAlign: 'center' }}>
1267
+ <Typography variant="alpha" style={{ color: theme.colors.danger[600] }}>
1268
+ {rateLimitStats.blocked || 0}
1269
+ </Typography>
1270
+ <Typography variant="pi" textColor="neutral600" style={{ fontSize: '11px', marginTop: '4px' }}>
1271
+ {formatMessage({ id: getTrad('settings.rateLimit.stats.blocked') })}
1272
+ </Typography>
1273
+ </Box>
1274
+ </Grid.Item>
1275
+ <Grid.Item col={3} s={6} xs={12}>
1276
+ <Box padding={4} background="primary100" style={{ borderRadius: '8px', textAlign: 'center' }}>
1277
+ <Typography variant="alpha" style={{ color: theme.colors.primary[600] }}>
1278
+ {rateLimitStats.ipLimits || 0}
1279
+ </Typography>
1280
+ <Typography variant="pi" textColor="neutral600" style={{ fontSize: '11px', marginTop: '4px' }}>
1281
+ {formatMessage({ id: getTrad('settings.rateLimit.stats.ipLimits') })}
1282
+ </Typography>
1283
+ </Box>
1284
+ </Grid.Item>
1285
+ <Grid.Item col={3} s={6} xs={12}>
1286
+ <Box padding={4} background="success100" style={{ borderRadius: '8px', textAlign: 'center' }}>
1287
+ <Typography variant="alpha" style={{ color: theme.colors.success[600] }}>
1288
+ {rateLimitStats.emailLimits || 0}
1289
+ </Typography>
1290
+ <Typography variant="pi" textColor="neutral600" style={{ fontSize: '11px', marginTop: '4px' }}>
1291
+ {formatMessage({ id: getTrad('settings.rateLimit.stats.emailLimits') })}
1292
+ </Typography>
1293
+ </Box>
1294
+ </Grid.Item>
1295
+ </Grid.Root>
1296
+
1297
+ {/* Management Buttons */}
1298
+ <Flex gap={3} justifyContent="center">
1299
+ <Button
1300
+ onClick={handleRateLimitCleanup}
1301
+ variant="secondary"
1302
+ size="M"
1303
+ >
1304
+ {formatMessage({ id: getTrad('settings.rateLimit.cleanup') })}
1305
+ </Button>
1306
+ <Button
1307
+ onClick={handleRateLimitReset}
1308
+ variant="danger"
1309
+ size="M"
1310
+ >
1311
+ {formatMessage({ id: getTrad('settings.rateLimit.reset') })}
1312
+ </Button>
1313
+ </Flex>
1314
+ </>
1315
+ )}
1316
+ </Box>
1317
+ </Accordion.Content>
1318
+ </Accordion.Item>
1117
1319
  </Accordion.Root>
1118
1320
 
1119
1321
  {/* Info Footer */}
@@ -172,6 +172,30 @@
172
172
 
173
173
  "magic-link.settings.footer.quickhelp": "💡 Schnell-Hilfe",
174
174
  "magic-link.settings.footer.reminder": "Nicht vergessen: Nach dem Speichern einen Test-Token erstellen und den Magic Link testen!",
175
+
176
+ "magic-link.settings.section.rateLimit": "Sicherheit & Rate Limiting",
177
+ "magic-link.settings.section.rateLimit.description": "Schutz vor Missbrauch und Brute-Force-Angriffen",
178
+ "magic-link.settings.rateLimit.title": "RATE LIMITING",
179
+ "magic-link.settings.rateLimit.subtitle": "Verhindere Missbrauch durch Begrenzung der Anfragen pro IP und E-Mail",
180
+ "magic-link.settings.rateLimit.enable.title": "Rate Limiting aktivieren",
181
+ "magic-link.settings.rateLimit.enable.description": "Schützt vor Brute-Force-Angriffen und Spam durch Begrenzung der Token-Erstellungsanfragen.",
182
+ "magic-link.settings.rateLimit.config.title": "RATE LIMIT KONFIGURATION",
183
+ "magic-link.settings.rateLimit.maxAttempts.label": "Maximale Versuche",
184
+ "magic-link.settings.rateLimit.maxAttempts.hint": "Wie viele Anfragen sind erlaubt, bevor blockiert wird",
185
+ "magic-link.settings.rateLimit.windowMinutes.label": "Zeitfenster (Minuten)",
186
+ "magic-link.settings.rateLimit.windowMinutes.hint": "Zeitraum für die Zählung von Anfragen (1-1440 Minuten)",
187
+ "magic-link.settings.rateLimit.stats.title": "AKTUELLE STATISTIKEN",
188
+ "magic-link.settings.rateLimit.stats.total": "Gesamt Einträge",
189
+ "magic-link.settings.rateLimit.stats.blocked": "Aktuell Blockiert",
190
+ "magic-link.settings.rateLimit.stats.ipLimits": "IP Limits",
191
+ "magic-link.settings.rateLimit.stats.emailLimits": "E-Mail Limits",
192
+ "magic-link.settings.rateLimit.cleanup": "Abgelaufene aufräumen",
193
+ "magic-link.settings.rateLimit.reset": "Alle Limits zurücksetzen",
194
+ "magic-link.settings.rateLimit.cleanupSuccess": "Abgelaufene Einträge erfolgreich aufgeräumt",
195
+ "magic-link.settings.rateLimit.cleanupError": "Fehler beim Aufräumen abgelaufener Einträge",
196
+ "magic-link.settings.rateLimit.resetSuccess": "Alle Rate Limits erfolgreich zurückgesetzt",
197
+ "magic-link.settings.rateLimit.resetError": "Fehler beim Zurücksetzen der Rate Limits",
198
+ "magic-link.settings.rateLimit.resetConfirm": "Sind Sie sicher, dass Sie alle Rate Limits zurücksetzen möchten? Dies ermöglicht es allen blockierten IPs und E-Mails, wieder Anfragen zu stellen.",
175
199
 
176
200
  "magic-link.tokens.title": "Magic Link Tokens",
177
201
  "magic-link.tokens.page.title": "Token-Verwaltung",
@@ -37,6 +37,8 @@
37
37
  "magic-link.settings.section.email.description": "Configure email templates and sender information",
38
38
  "magic-link.settings.section.advanced": "Advanced Settings",
39
39
  "magic-link.settings.section.advanced.description": "Additional configuration options",
40
+ "magic-link.settings.section.rateLimit": "Security & Rate Limiting",
41
+ "magic-link.settings.section.rateLimit.description": "Protect against abuse and brute-force attacks",
40
42
 
41
43
  "magic-link.settings.features.title": "MAIN FEATURES",
42
44
  "magic-link.settings.features.subtitle": "Enable or disable the main features of the Magic Link plugin",
@@ -172,6 +174,28 @@
172
174
 
173
175
  "magic-link.settings.footer.quickhelp": "💡 Quick Help",
174
176
  "magic-link.settings.footer.reminder": "Don't forget: After saving, create a test token and test the Magic Link!",
177
+
178
+ "magic-link.settings.rateLimit.title": "RATE LIMITING",
179
+ "magic-link.settings.rateLimit.subtitle": "Prevent abuse by limiting the number of requests per IP and email",
180
+ "magic-link.settings.rateLimit.enable.title": "Enable Rate Limiting",
181
+ "magic-link.settings.rateLimit.enable.description": "Protects against brute-force attacks and spam by limiting token creation requests.",
182
+ "magic-link.settings.rateLimit.config.title": "RATE LIMIT CONFIGURATION",
183
+ "magic-link.settings.rateLimit.maxAttempts.label": "Maximum Attempts",
184
+ "magic-link.settings.rateLimit.maxAttempts.hint": "How many requests are allowed before blocking",
185
+ "magic-link.settings.rateLimit.windowMinutes.label": "Time Window (Minutes)",
186
+ "magic-link.settings.rateLimit.windowMinutes.hint": "Time period for counting requests (1-1440 minutes)",
187
+ "magic-link.settings.rateLimit.stats.title": "CURRENT STATISTICS",
188
+ "magic-link.settings.rateLimit.stats.total": "Total Entries",
189
+ "magic-link.settings.rateLimit.stats.blocked": "Currently Blocked",
190
+ "magic-link.settings.rateLimit.stats.ipLimits": "IP Limits",
191
+ "magic-link.settings.rateLimit.stats.emailLimits": "Email Limits",
192
+ "magic-link.settings.rateLimit.cleanup": "Cleanup Expired",
193
+ "magic-link.settings.rateLimit.reset": "Reset All Limits",
194
+ "magic-link.settings.rateLimit.cleanupSuccess": "Expired entries cleaned up successfully",
195
+ "magic-link.settings.rateLimit.cleanupError": "Error cleaning up expired entries",
196
+ "magic-link.settings.rateLimit.resetSuccess": "All rate limits reset successfully",
197
+ "magic-link.settings.rateLimit.resetError": "Error resetting rate limits",
198
+ "magic-link.settings.rateLimit.resetConfirm": "Are you sure you want to reset all rate limits? This will allow all blocked IPs and emails to make requests again.",
175
199
 
176
200
  "magic-link.tokens.title": "Magic Link Tokens",
177
201
  "magic-link.tokens.page.title": "Token Management",
@@ -172,6 +172,30 @@
172
172
 
173
173
  "magic-link.settings.footer.quickhelp": "💡 Ayuda rápida",
174
174
  "magic-link.settings.footer.reminder": "No olvide: ¡Después de guardar, cree un token de prueba y pruebe el Magic Link!",
175
+
176
+ "magic-link.settings.section.rateLimit": "Seguridad y limitación de velocidad",
177
+ "magic-link.settings.section.rateLimit.description": "Protección contra abusos y ataques de fuerza bruta",
178
+ "magic-link.settings.rateLimit.title": "LIMITACIÓN DE VELOCIDAD",
179
+ "magic-link.settings.rateLimit.subtitle": "Previene abusos limitando el número de solicitudes por IP y correo",
180
+ "magic-link.settings.rateLimit.enable.title": "Activar limitación de velocidad",
181
+ "magic-link.settings.rateLimit.enable.description": "Protege contra ataques de fuerza bruta y spam limitando las solicitudes de creación de tokens.",
182
+ "magic-link.settings.rateLimit.config.title": "CONFIGURACIÓN DE LÍMITES",
183
+ "magic-link.settings.rateLimit.maxAttempts.label": "Intentos máximos",
184
+ "magic-link.settings.rateLimit.maxAttempts.hint": "Cuántas solicitudes se permiten antes del bloqueo",
185
+ "magic-link.settings.rateLimit.windowMinutes.label": "Ventana de tiempo (Minutos)",
186
+ "magic-link.settings.rateLimit.windowMinutes.hint": "Período de tiempo para contar solicitudes (1-1440 minutos)",
187
+ "magic-link.settings.rateLimit.stats.title": "ESTADÍSTICAS ACTUALES",
188
+ "magic-link.settings.rateLimit.stats.total": "Entradas totales",
189
+ "magic-link.settings.rateLimit.stats.blocked": "Actualmente bloqueados",
190
+ "magic-link.settings.rateLimit.stats.ipLimits": "Límites IP",
191
+ "magic-link.settings.rateLimit.stats.emailLimits": "Límites email",
192
+ "magic-link.settings.rateLimit.cleanup": "Limpiar expirados",
193
+ "magic-link.settings.rateLimit.reset": "Restablecer todos los límites",
194
+ "magic-link.settings.rateLimit.cleanupSuccess": "Entradas expiradas limpiadas correctamente",
195
+ "magic-link.settings.rateLimit.cleanupError": "Error al limpiar entradas expiradas",
196
+ "magic-link.settings.rateLimit.resetSuccess": "Todos los límites restablecidos correctamente",
197
+ "magic-link.settings.rateLimit.resetError": "Error al restablecer límites",
198
+ "magic-link.settings.rateLimit.resetConfirm": "¿Está seguro de que desea restablecer todos los límites de velocidad? Esto permitirá que todas las IPs y correos bloqueados realicen solicitudes nuevamente.",
175
199
 
176
200
  "magic-link.tokens.title": "Tokens Magic Link",
177
201
  "magic-link.tokens.page.title": "Gestión de tokens",
@@ -172,6 +172,30 @@
172
172
 
173
173
  "magic-link.settings.footer.quickhelp": "💡 Aide rapide",
174
174
  "magic-link.settings.footer.reminder": "N'oubliez pas : Après l'enregistrement, créez un jeton de test et testez le Magic Link !",
175
+
176
+ "magic-link.settings.section.rateLimit": "Sécurité & Limitation de débit",
177
+ "magic-link.settings.section.rateLimit.description": "Protection contre les abus et les attaques par force brute",
178
+ "magic-link.settings.rateLimit.title": "LIMITATION DE DÉBIT",
179
+ "magic-link.settings.rateLimit.subtitle": "Prévenez les abus en limitant le nombre de requêtes par IP et email",
180
+ "magic-link.settings.rateLimit.enable.title": "Activer la limitation de débit",
181
+ "magic-link.settings.rateLimit.enable.description": "Protège contre les attaques par force brute et le spam en limitant les requêtes de création de jetons.",
182
+ "magic-link.settings.rateLimit.config.title": "CONFIGURATION DE LA LIMITATION",
183
+ "magic-link.settings.rateLimit.maxAttempts.label": "Tentatives maximales",
184
+ "magic-link.settings.rateLimit.maxAttempts.hint": "Combien de requêtes sont autorisées avant le blocage",
185
+ "magic-link.settings.rateLimit.windowMinutes.label": "Fenêtre de temps (Minutes)",
186
+ "magic-link.settings.rateLimit.windowMinutes.hint": "Période de temps pour compter les requêtes (1-1440 minutes)",
187
+ "magic-link.settings.rateLimit.stats.title": "STATISTIQUES ACTUELLES",
188
+ "magic-link.settings.rateLimit.stats.total": "Total d'entrées",
189
+ "magic-link.settings.rateLimit.stats.blocked": "Actuellement bloqués",
190
+ "magic-link.settings.rateLimit.stats.ipLimits": "Limites IP",
191
+ "magic-link.settings.rateLimit.stats.emailLimits": "Limites email",
192
+ "magic-link.settings.rateLimit.cleanup": "Nettoyer expirés",
193
+ "magic-link.settings.rateLimit.reset": "Réinitialiser toutes les limites",
194
+ "magic-link.settings.rateLimit.cleanupSuccess": "Entrées expirées nettoyées avec succès",
195
+ "magic-link.settings.rateLimit.cleanupError": "Erreur lors du nettoyage des entrées expirées",
196
+ "magic-link.settings.rateLimit.resetSuccess": "Toutes les limites réinitialisées avec succès",
197
+ "magic-link.settings.rateLimit.resetError": "Erreur lors de la réinitialisation des limites",
198
+ "magic-link.settings.rateLimit.resetConfirm": "Êtes-vous sûr de vouloir réinitialiser toutes les limites de débit ? Cela permettra à toutes les IPs et emails bloqués de faire à nouveau des requêtes.",
175
199
 
176
200
  "magic-link.tokens.title": "Jetons Magic Link",
177
201
  "magic-link.tokens.page.title": "Gestion des jetons",
@@ -172,6 +172,30 @@
172
172
 
173
173
  "magic-link.settings.footer.quickhelp": "💡 Ajuda rápida",
174
174
  "magic-link.settings.footer.reminder": "Não esqueça: Após salvar, crie um token de teste e teste o Magic Link!",
175
+
176
+ "magic-link.settings.section.rateLimit": "Segurança e limitação de taxa",
177
+ "magic-link.settings.section.rateLimit.description": "Proteção contra abusos e ataques de força bruta",
178
+ "magic-link.settings.rateLimit.title": "LIMITAÇÃO DE TAXA",
179
+ "magic-link.settings.rateLimit.subtitle": "Previne abusos limitando o número de solicitações por IP e e-mail",
180
+ "magic-link.settings.rateLimit.enable.title": "Ativar limitação de taxa",
181
+ "magic-link.settings.rateLimit.enable.description": "Protege contra ataques de força bruta e spam limitando solicitações de criação de tokens.",
182
+ "magic-link.settings.rateLimit.config.title": "CONFIGURAÇÃO DE LIMITES",
183
+ "magic-link.settings.rateLimit.maxAttempts.label": "Tentativas máximas",
184
+ "magic-link.settings.rateLimit.maxAttempts.hint": "Quantas solicitações são permitidas antes do bloqueio",
185
+ "magic-link.settings.rateLimit.windowMinutes.label": "Janela de tempo (Minutos)",
186
+ "magic-link.settings.rateLimit.windowMinutes.hint": "Período de tempo para contar solicitações (1-1440 minutos)",
187
+ "magic-link.settings.rateLimit.stats.title": "ESTATÍSTICAS ATUAIS",
188
+ "magic-link.settings.rateLimit.stats.total": "Total de entradas",
189
+ "magic-link.settings.rateLimit.stats.blocked": "Atualmente bloqueados",
190
+ "magic-link.settings.rateLimit.stats.ipLimits": "Limites IP",
191
+ "magic-link.settings.rateLimit.stats.emailLimits": "Limites email",
192
+ "magic-link.settings.rateLimit.cleanup": "Limpar expirados",
193
+ "magic-link.settings.rateLimit.reset": "Redefinir todos os limites",
194
+ "magic-link.settings.rateLimit.cleanupSuccess": "Entradas expiradas limpas com sucesso",
195
+ "magic-link.settings.rateLimit.cleanupError": "Erro ao limpar entradas expiradas",
196
+ "magic-link.settings.rateLimit.resetSuccess": "Todos os limites redefinidos com sucesso",
197
+ "magic-link.settings.rateLimit.resetError": "Erro ao redefinir limites",
198
+ "magic-link.settings.rateLimit.resetConfirm": "Tem certeza de que deseja redefinir todos os limites de taxa? Isso permitirá que todos os IPs e e-mails bloqueados façam solicitações novamente.",
175
199
 
176
200
  "magic-link.tokens.title": "Tokens Magic Link",
177
201
  "magic-link.tokens.page.title": "Gerenciamento de tokens",
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "4.9.4",
2
+ "version": "4.11.0",
3
3
  "keywords": [],
4
4
  "type": "commonjs",
5
5
  "exports": {
@@ -70,6 +70,10 @@ Thanks.`,
70
70
  jwt_token_expires_in: '30d',
71
71
  callback_url: serverUrl,
72
72
  allow_magic_links_on_public_registration: false,
73
+ // Rate Limiting Settings
74
+ rate_limit_enabled: true,
75
+ rate_limit_max_attempts: 5,
76
+ rate_limit_window_minutes: 15,
73
77
  };
74
78
 
75
79
  await pluginStore.set({ key: 'settings', value });
@@ -119,6 +123,19 @@ Thanks.`,
119
123
  return next();
120
124
  });
121
125
 
126
+ // Initialize Rate Limiter Cleanup Job
127
+ const rateLimiter = strapi.plugin('magic-link').service('rate-limiter');
128
+
129
+ // Initial cleanup
130
+ setTimeout(() => {
131
+ rateLimiter.cleanupExpired();
132
+ }, 5000);
133
+
134
+ // Cleanup every 30 minutes
135
+ setInterval(() => {
136
+ rateLimiter.cleanupExpired();
137
+ }, 30 * 60 * 1000);
138
+
122
139
  // Initialize License Guard
123
140
  try {
124
141
  const licenseGuardService = strapi.plugin('magic-link').service('license-guard');
@@ -160,6 +160,7 @@ module.exports = {
160
160
  async sendLink(ctx) {
161
161
  // Strapi v5 pattern für Service Zugriff
162
162
  const magicLink = strapi.plugin('magic-link').service('magic-link');
163
+ const rateLimiter = strapi.plugin('magic-link').service('rate-limiter');
163
164
 
164
165
  const isEnabled = await magicLink.isEnabled();
165
166
 
@@ -178,6 +179,22 @@ module.exports = {
178
179
  if (email && !isEmail) {
179
180
  return ctx.badRequest('wrong.email');
180
181
  }
182
+
183
+ // Rate limiting check - both IP and email
184
+ const ipAddress = ctx.request.ip;
185
+ const ipCheck = await rateLimiter.checkRateLimit(ipAddress, 'ip');
186
+
187
+ if (!ipCheck.allowed) {
188
+ return ctx.tooManyRequests(`Too many requests. Please try again in ${ipCheck.retryAfter} seconds.`);
189
+ }
190
+
191
+ if (email) {
192
+ const emailCheck = await rateLimiter.checkRateLimit(email, 'email');
193
+
194
+ if (!emailCheck.allowed) {
195
+ return ctx.tooManyRequests(`Too many requests for this email. Please try again in ${emailCheck.retryAfter} seconds.`);
196
+ }
197
+ }
181
198
 
182
199
  let user;
183
200
  try {
@@ -8,6 +8,7 @@ const auth = require('./auth');
8
8
  const tokens = require('./tokens');
9
9
  const jwt = require('./jwt');
10
10
  const license = require('./license');
11
+ const rateLimit = require('./rate-limit');
11
12
 
12
13
  module.exports = {
13
14
  controller,
@@ -15,4 +16,5 @@ module.exports = {
15
16
  tokens,
16
17
  jwt,
17
18
  license,
19
+ rateLimit,
18
20
  };
@@ -0,0 +1,71 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Rate Limit Controller
5
+ */
6
+
7
+ module.exports = {
8
+ /**
9
+ * Get rate limit statistics
10
+ */
11
+ async getStats(ctx) {
12
+ try {
13
+ const rateLimiter = strapi.plugin('magic-link').service('rate-limiter');
14
+ const stats = await rateLimiter.getStats();
15
+
16
+ if (!stats) {
17
+ return ctx.badRequest('Failed to get rate limit stats');
18
+ }
19
+
20
+ ctx.send({
21
+ success: true,
22
+ data: stats,
23
+ });
24
+ } catch (error) {
25
+ strapi.log.error('Error getting rate limit stats:', error);
26
+ ctx.throw(500, error);
27
+ }
28
+ },
29
+
30
+ /**
31
+ * Cleanup expired rate limit entries
32
+ */
33
+ async cleanup(ctx) {
34
+ try {
35
+ const rateLimiter = strapi.plugin('magic-link').service('rate-limiter');
36
+ const result = await rateLimiter.cleanupExpired();
37
+
38
+ ctx.send({
39
+ success: true,
40
+ message: `Cleaned up ${result.cleaned} expired entries`,
41
+ data: result,
42
+ });
43
+ } catch (error) {
44
+ strapi.log.error('Error cleaning up rate limits:', error);
45
+ ctx.throw(500, error);
46
+ }
47
+ },
48
+
49
+ /**
50
+ * Reset all rate limits
51
+ */
52
+ async reset(ctx) {
53
+ try {
54
+ const pluginStore = strapi.store({
55
+ type: 'plugin',
56
+ name: 'magic-link',
57
+ });
58
+
59
+ await pluginStore.set({ key: 'rate_limits', value: { limits: {} } });
60
+
61
+ ctx.send({
62
+ success: true,
63
+ message: 'All rate limits have been reset',
64
+ });
65
+ } catch (error) {
66
+ strapi.log.error('Error resetting rate limits:', error);
67
+ ctx.throw(500, error);
68
+ }
69
+ },
70
+ };
71
+
@@ -94,6 +94,21 @@ module.exports = {
94
94
  return ctx.badRequest('Email is required');
95
95
  }
96
96
 
97
+ // Rate limiting check
98
+ const rateLimiter = strapi.plugin('magic-link').service('rate-limiter');
99
+ const ipAddress = ctx.request.ip;
100
+ const ipCheck = await rateLimiter.checkRateLimit(ipAddress, 'ip');
101
+
102
+ if (!ipCheck.allowed) {
103
+ return ctx.tooManyRequests(`Too many token creation requests. Please try again in ${ipCheck.retryAfter} seconds.`);
104
+ }
105
+
106
+ const emailCheck = await rateLimiter.checkRateLimit(email, 'email');
107
+
108
+ if (!emailCheck.allowed) {
109
+ return ctx.tooManyRequests(`Too many requests for this email. Please try again in ${emailCheck.retryAfter} seconds.`);
110
+ }
111
+
97
112
  // Überprüfe, ob die Plugin-Einstellungen das Erstellen neuer Benutzer erlauben
98
113
  const pluginStore = strapi.store({
99
114
  type: 'plugin',
@@ -214,5 +214,31 @@ module.exports = {
214
214
  policies: [],
215
215
  },
216
216
  },
217
+
218
+ // Rate Limiting
219
+ {
220
+ method: 'GET',
221
+ path: '/rate-limit/stats',
222
+ handler: 'rateLimit.getStats',
223
+ config: {
224
+ policies: [],
225
+ },
226
+ },
227
+ {
228
+ method: 'POST',
229
+ path: '/rate-limit/cleanup',
230
+ handler: 'rateLimit.cleanup',
231
+ config: {
232
+ policies: [],
233
+ },
234
+ },
235
+ {
236
+ method: 'POST',
237
+ path: '/rate-limit/reset',
238
+ handler: 'rateLimit.reset',
239
+ config: {
240
+ policies: [],
241
+ },
242
+ },
217
243
  ],
218
244
  };
@@ -4,6 +4,7 @@ const service = require('./service');
4
4
  const magicLink = require('./magic-link');
5
5
  const store = require('../../services/store');
6
6
  const licenseGuard = require('./license-guard');
7
+ const rateLimiter = require('./rate-limiter');
7
8
 
8
9
  module.exports = {
9
10
  service,
@@ -12,4 +13,6 @@ module.exports = {
12
13
  store,
13
14
  'license-guard': licenseGuard,
14
15
  licenseGuard, // Alias für Kompatibilität
16
+ 'rate-limiter': rateLimiter,
17
+ rateLimiter, // Alias für Kompatibilität
15
18
  };
@@ -0,0 +1,186 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Rate Limiter Service
5
+ * Prevents abuse by limiting token creation requests per IP and email
6
+ */
7
+
8
+ module.exports = ({ strapi }) => ({
9
+ /**
10
+ * Check if request should be rate limited
11
+ * @param {string} identifier - IP address or email
12
+ * @param {string} type - 'ip' or 'email'
13
+ * @returns {Promise<{allowed: boolean, retryAfter: number}>}
14
+ */
15
+ async checkRateLimit(identifier, type = 'ip') {
16
+ try {
17
+ const pluginStore = strapi.store({
18
+ type: 'plugin',
19
+ name: 'magic-link',
20
+ });
21
+
22
+ // Get settings for rate limit configuration
23
+ const settings = await pluginStore.get({ key: 'settings' });
24
+
25
+ // Check if rate limiting is enabled
26
+ if (settings?.rate_limit_enabled === false) {
27
+ return { allowed: true, retryAfter: 0 };
28
+ }
29
+
30
+ const maxAttempts = settings?.rate_limit_max_attempts || 5;
31
+ const windowMinutes = settings?.rate_limit_window_minutes || 15;
32
+
33
+ // Get current rate limit data
34
+ const rateLimitData = (await pluginStore.get({ key: 'rate_limits' })) || { limits: {} };
35
+
36
+ const key = `${type}_${identifier}`;
37
+ const now = Date.now();
38
+ const windowMs = windowMinutes * 60 * 1000;
39
+
40
+ // Get or create limit entry
41
+ let limitEntry = rateLimitData.limits[key];
42
+
43
+ if (!limitEntry) {
44
+ // First request
45
+ limitEntry = {
46
+ count: 1,
47
+ firstRequest: now,
48
+ lastRequest: now,
49
+ };
50
+ rateLimitData.limits[key] = limitEntry;
51
+ await pluginStore.set({ key: 'rate_limits', value: rateLimitData });
52
+
53
+ return { allowed: true, retryAfter: 0 };
54
+ }
55
+
56
+ // Check if window has expired
57
+ const timeSinceFirst = now - limitEntry.firstRequest;
58
+
59
+ if (timeSinceFirst > windowMs) {
60
+ // Window expired, reset counter
61
+ limitEntry = {
62
+ count: 1,
63
+ firstRequest: now,
64
+ lastRequest: now,
65
+ };
66
+ rateLimitData.limits[key] = limitEntry;
67
+ await pluginStore.set({ key: 'rate_limits', value: rateLimitData });
68
+
69
+ return { allowed: true, retryAfter: 0 };
70
+ }
71
+
72
+ // Check if limit exceeded
73
+ if (limitEntry.count >= maxAttempts) {
74
+ const timeRemaining = windowMs - timeSinceFirst;
75
+ const retryAfterSeconds = Math.ceil(timeRemaining / 1000);
76
+
77
+ strapi.log.warn(`⚠️ Rate limit exceeded for ${type}: ${identifier} (${limitEntry.count}/${maxAttempts} requests)`);
78
+
79
+ return {
80
+ allowed: false,
81
+ retryAfter: retryAfterSeconds,
82
+ };
83
+ }
84
+
85
+ // Increment counter
86
+ limitEntry.count++;
87
+ limitEntry.lastRequest = now;
88
+ rateLimitData.limits[key] = limitEntry;
89
+ await pluginStore.set({ key: 'rate_limits', value: rateLimitData });
90
+
91
+ return { allowed: true, retryAfter: 0 };
92
+ } catch (error) {
93
+ strapi.log.error('Error checking rate limit:', error);
94
+ // On error, allow request (fail open for availability)
95
+ return { allowed: true, retryAfter: 0 };
96
+ }
97
+ },
98
+
99
+ /**
100
+ * Clean up expired rate limit entries
101
+ */
102
+ async cleanupExpired() {
103
+ try {
104
+ const pluginStore = strapi.store({
105
+ type: 'plugin',
106
+ name: 'magic-link',
107
+ });
108
+
109
+ const settings = await pluginStore.get({ key: 'settings' });
110
+ const windowMinutes = settings?.rate_limit_window_minutes || 15;
111
+ const windowMs = windowMinutes * 60 * 1000;
112
+
113
+ const rateLimitData = (await pluginStore.get({ key: 'rate_limits' })) || { limits: {} };
114
+ const now = Date.now();
115
+
116
+ let cleaned = 0;
117
+
118
+ // Remove expired entries
119
+ Object.keys(rateLimitData.limits).forEach(key => {
120
+ const entry = rateLimitData.limits[key];
121
+ const timeSinceFirst = now - entry.firstRequest;
122
+
123
+ if (timeSinceFirst > windowMs) {
124
+ delete rateLimitData.limits[key];
125
+ cleaned++;
126
+ }
127
+ });
128
+
129
+ if (cleaned > 0) {
130
+ await pluginStore.set({ key: 'rate_limits', value: rateLimitData });
131
+ strapi.log.info(`🧹 Cleaned up ${cleaned} expired rate limit entries`);
132
+ }
133
+
134
+ return { cleaned };
135
+ } catch (error) {
136
+ strapi.log.error('Error cleaning up rate limits:', error);
137
+ return { cleaned: 0 };
138
+ }
139
+ },
140
+
141
+ /**
142
+ * Get rate limit stats
143
+ */
144
+ async getStats() {
145
+ try {
146
+ const pluginStore = strapi.store({
147
+ type: 'plugin',
148
+ name: 'magic-link',
149
+ });
150
+
151
+ const rateLimitData = (await pluginStore.get({ key: 'rate_limits' })) || { limits: {} };
152
+ const settings = await pluginStore.get({ key: 'settings' });
153
+ const maxAttempts = settings?.rate_limit_max_attempts || 5;
154
+ const windowMinutes = settings?.rate_limit_window_minutes || 15;
155
+
156
+ const stats = {
157
+ totalEntries: Object.keys(rateLimitData.limits).length,
158
+ maxAttempts,
159
+ windowMinutes,
160
+ ipLimits: 0,
161
+ emailLimits: 0,
162
+ blocked: 0,
163
+ };
164
+
165
+ Object.keys(rateLimitData.limits).forEach(key => {
166
+ const entry = rateLimitData.limits[key];
167
+
168
+ if (key.startsWith('ip_')) {
169
+ stats.ipLimits++;
170
+ } else if (key.startsWith('email_')) {
171
+ stats.emailLimits++;
172
+ }
173
+
174
+ if (entry.count >= maxAttempts) {
175
+ stats.blocked++;
176
+ }
177
+ });
178
+
179
+ return stats;
180
+ } catch (error) {
181
+ strapi.log.error('Error getting rate limit stats:', error);
182
+ return null;
183
+ }
184
+ },
185
+ });
186
+