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.
- package/admin/src/pages/Settings/SettingsModern.jsx +203 -1
- package/admin/src/translations/de.json +24 -0
- package/admin/src/translations/en.json +24 -0
- package/admin/src/translations/es.json +24 -0
- package/admin/src/translations/fr.json +24 -0
- package/admin/src/translations/pt.json +24 -0
- package/package.json +1 -1
- package/server/src/bootstrap.js +17 -0
- package/server/src/controllers/auth.js +17 -0
- package/server/src/controllers/index.js +2 -0
- package/server/src/controllers/rate-limit.js +71 -0
- package/server/src/controllers/tokens.js +15 -0
- package/server/src/routes/admin.js +26 -0
- package/server/src/services/index.js +3 -0
- package/server/src/services/rate-limiter.js +186 -0
|
@@ -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
package/server/src/bootstrap.js
CHANGED
|
@@ -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
|
+
|