Skip to content

Commit ab9568c

Browse files
committed
add rate limiting
1 parent a043d4b commit ab9568c

21 files changed

Lines changed: 240 additions & 94 deletions

‎src/App.jsx‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,18 @@ function App() {
6767
const stateValue = useMemo(() => ({ user, setUser }), [user, setUser]);
6868
const alertsManagerRef = useRef();
6969

70+
// Listen for global rate-limit events dispatched by api.js interceptor
71+
useEffect(() => {
72+
const handler = (e) => {
73+
if (alertsManagerRef.current) {
74+
const msg = e.detail?.message || 'Zu viele Anfragen. Bitte warte einen Moment.';
75+
alertsManagerRef.current.showAlert('warning', msg);
76+
}
77+
};
78+
window.addEventListener('api-ratelimited', handler);
79+
return () => window.removeEventListener('api-ratelimited', handler);
80+
}, []);
81+
7082
return (
7183
<UserContext.Provider value={stateValue}>
7284
<AlertsContext.Provider value={alertsManagerRef}>

‎src/Pages/Events/Events.jsx‎

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { useState, useEffect, useReducer } from 'react';
22
import useAuth from '../../context/useAuth';
3-
import api from '../../helper/api';
3+
import api, { extractErrorMessage } from '../../helper/api';
44

55
// Material UI
66
import {
@@ -196,12 +196,9 @@ export default function Events() {
196196
})
197197
.catch(err => {
198198
console.error("Error saving event", err);
199-
const errMsg = typeof err.response?.data === 'string'
200-
? err.response.data
201-
: (err.response?.data?.message || 'Fehler beim Speichern des Events. Bitte Eingaben überprüfen.');
202199
dispatchModal({
203200
type: 'SET_FORM_ERROR',
204-
payload: errMsg
201+
payload: extractErrorMessage(err),
205202
});
206203
});
207204
};
@@ -221,7 +218,7 @@ export default function Events() {
221218
})
222219
.catch(err => {
223220
console.error("Error deleting event", err);
224-
alert(err.response?.data || "Löschen fehlgeschlagen.");
221+
alert(extractErrorMessage(err) || 'Löschen fehlgeschlagen.');
225222
});
226223
};
227224

‎src/Pages/Forum/Forum-Categories.jsx‎

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,27 @@
11
import React, { useReducer, useEffect } from 'react';
22
import { useParams } from 'react-router-dom';
3-
import api from '../../helper/api';
3+
import api, { extractErrorMessage } from '../../helper/api';
44

55
// Mui
66
import Container from '@mui/material/Container';
77
import Box from '@mui/material/Box';
88
import Typography from '@mui/material/Typography';
99
import CircularProgress from '@mui/material/CircularProgress';
10+
import Button from '@mui/material/Button';
11+
import Alert from '@mui/material/Alert';
12+
import RefreshIcon from '@mui/icons-material/Refresh';
1013

1114
// Eigene
1215
import Category from '../../components/Forum/Category';
1316
import ForumBreadcrumbs from '../../components/Forum/ForumBreadcrumbs';
1417

15-
const initialState = { category: [], loading: true };
18+
const initialState = { category: [], loading: true, error: null, errorStatus: null };
1619

1720
function reducer(state, action) {
1821
switch (action.type) {
19-
case 'FETCH_START': return { ...state, loading: true };
20-
case 'FETCH_SUCCESS': return { category: action.payload, loading: false };
21-
case 'FETCH_ERROR': return { ...state, loading: false };
22+
case 'FETCH_START': return { ...state, loading: true, error: null, errorStatus: null };
23+
case 'FETCH_SUCCESS': return { category: action.payload, loading: false, error: null, errorStatus: null };
24+
case 'FETCH_ERROR': return { ...state, loading: false, error: action.payload, errorStatus: action.status };
2225
case 'UPDATE_CATEGORY':
2326
return {
2427
...state,
@@ -31,9 +34,9 @@ function reducer(state, action) {
3134
export default function Forum_Categories() {
3235
const { id } = useParams();
3336
const [state, dispatch] = useReducer(reducer, initialState);
34-
const { category, loading } = state;
37+
const { category, loading, error, errorStatus } = state;
3538

36-
useEffect(() => {
39+
const fetchCategory = () => {
3740
const controller = new AbortController();
3841
dispatch({ type: 'FETCH_START' });
3942

@@ -47,10 +50,19 @@ export default function Forum_Categories() {
4750
.catch(err => {
4851
if (err.code === 'ERR_CANCELED') return;
4952
console.error(err);
50-
dispatch({ type: 'FETCH_ERROR' });
53+
dispatch({
54+
type: 'FETCH_ERROR',
55+
payload: extractErrorMessage(err),
56+
status: err.response?.status || 0,
57+
});
5158
});
5259

5360
return () => controller.abort();
61+
};
62+
63+
useEffect(() => {
64+
const cleanup = fetchCategory();
65+
return cleanup;
5466
}, [id]);
5567

5668
if (loading) {
@@ -61,6 +73,27 @@ export default function Forum_Categories() {
6173
);
6274
}
6375

76+
if (error) {
77+
const isRateLimit = errorStatus === 429;
78+
return (
79+
<Container maxWidth="sm" sx={{ py: 8, textAlign: 'center' }}>
80+
<Alert
81+
severity={isRateLimit ? 'warning' : 'error'}
82+
sx={{ mb: 3 }}
83+
action={
84+
<Button color="inherit" size="small" startIcon={<RefreshIcon />} onClick={fetchCategory}>
85+
Erneut versuchen
86+
</Button>
87+
}
88+
>
89+
{isRateLimit
90+
? `Zu viele Anfragen — bitte warte einen Moment.`
91+
: `Fehler beim Laden der Kategorie: ${error}`}
92+
</Alert>
93+
</Container>
94+
);
95+
}
96+
6497
const handleCategoryUpdate = (updatedCategory) => {
6598
dispatch({ type: 'UPDATE_CATEGORY', payload: updatedCategory });
6699
};

‎src/Pages/Forum/Forum-Post.jsx‎

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { useEffect, useState, use, useCallback, useReducer } from 'react';
22
import { useParams, useNavigate } from 'react-router-dom';
3-
import api from '../../helper/api';
3+
import api, { extractErrorMessage } from '../../helper/api';
44

55
// Mui
66
import Container from '@mui/material/Container';
@@ -206,10 +206,7 @@ export default function Forum_Post() {
206206
.catch(error => {
207207
console.error("Failed to create poll", error);
208208
const status = error.response?.status || 500;
209-
const rawData = error.response?.data;
210-
const msg = typeof rawData === 'object'
211-
? (rawData.message || rawData.details || JSON.stringify(rawData))
212-
: (rawData || 'Fehler beim Erstellen der Umfrage');
209+
const msg = extractErrorMessage(error);
213210
alertsManagerRef.current.showAlert('error', `${status}: ${msg}`);
214211
})
215212
.finally(() => {
@@ -234,10 +231,7 @@ export default function Forum_Post() {
234231
.catch(error => {
235232
console.error("Failed to delete poll", error);
236233
const status = error.response?.status || 500;
237-
const rawData = error.response?.data;
238-
const msg = typeof rawData === 'object'
239-
? (rawData.message || rawData.details || JSON.stringify(rawData))
240-
: (rawData || 'Fehler beim Löschen der Umfrage');
234+
const msg = extractErrorMessage(error);
241235
alertsManagerRef.current.showAlert('error', `${status}: ${msg}`);
242236
});
243237
};
@@ -265,7 +259,13 @@ export default function Forum_Post() {
265259
})
266260
.catch(error => {
267261
console.error("Error fetching post/answers", error);
268-
alertsManagerRef.current.showAlert('error', 'Fehler beim Laden des Posts');
262+
const status = error.response?.status || 0;
263+
const msg = extractErrorMessage(error);
264+
if (status === 429) {
265+
alertsManagerRef.current.showAlert('warning', 'Zu viele Anfragen — bitte warte einen Moment.');
266+
} else {
267+
alertsManagerRef.current.showAlert('error', `Fehler beim Laden des Posts: ${msg}`);
268+
}
269269
dispatch({ type: 'FETCH_FAILURE' });
270270
});
271271
}, [id, alertsManagerRef]);
@@ -293,10 +293,7 @@ export default function Forum_Post() {
293293
.catch(error => {
294294
console.error(error);
295295
const status = error.response?.status || 500;
296-
const rawData = error.response?.data;
297-
const data = typeof rawData === 'object'
298-
? (rawData.message || rawData.details || JSON.stringify(rawData))
299-
: (rawData || 'Fehler beim Senden');
296+
const data = extractErrorMessage(error);
300297
alertsManagerRef.current.showAlert('error', `${status}: ${data}`);
301298
})
302299
.finally(() => {

‎src/Pages/Forum/Forum-Topic.jsx‎

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import React, { useReducer, useState, useEffect } from 'react';
1+
import React, { useReducer, useState, useEffect, useCallback } from 'react';
22
import { useParams, useNavigate } from 'react-router-dom';
3-
import api from '../../helper/api';
3+
import api, { extractErrorMessage } from '../../helper/api';
44
import Container from '@mui/material/Container';
55
import Typography from '@mui/material/Typography';
66
import Box from '@mui/material/Box';
@@ -11,6 +11,8 @@ import Divider from '@mui/material/Divider';
1111
import TableSortLabel from '@mui/material/TableSortLabel';
1212
import Button from '@mui/material/Button';
1313
import CircularProgress from '@mui/material/CircularProgress';
14+
import Alert from '@mui/material/Alert';
15+
import RefreshIcon from '@mui/icons-material/Refresh';
1416

1517
import PersonOutlineIcon from '@mui/icons-material/PersonOutlined';
1618
import ForumIcon from '@mui/icons-material/Forum';
@@ -28,13 +30,13 @@ import { convertTimestamp } from '../../helper/converter';
2830
import ForumBreadcrumbs from '../../components/Forum/ForumBreadcrumbs';
2931
import ForumChips from '../../components/Forum/ForumChips';
3032

31-
const initialFetchState = { posts: [], topicData: null, loading: true };
33+
const initialFetchState = { posts: [], topicData: null, loading: true, error: null, errorStatus: null };
3234

3335
function fetchReducer(state, action) {
3436
switch (action.type) {
35-
case 'FETCH_START': return { ...state, loading: true };
36-
case 'FETCH_SUCCESS': return { posts: action.posts, topicData: action.topicData, loading: false };
37-
case 'FETCH_ERROR': return { ...state, loading: false };
37+
case 'FETCH_START': return { ...state, loading: true, error: null, errorStatus: null };
38+
case 'FETCH_SUCCESS': return { posts: action.posts, topicData: action.topicData, loading: false, error: null, errorStatus: null };
39+
case 'FETCH_ERROR': return { ...state, loading: false, error: action.payload, errorStatus: action.status };
3840
default: return state;
3941
}
4042
}
@@ -43,10 +45,10 @@ export default function Forum_Topic() {
4345
const { id } = useParams();
4446
const navigate = useNavigate();
4547
const [fetchState, dispatch] = useReducer(fetchReducer, initialFetchState);
46-
const { posts, topicData, loading } = fetchState;
48+
const { posts, topicData, loading, error, errorStatus } = fetchState;
4749
const [sort, setSort] = useState({ field: 'creationDate', direction: 'asc' });
4850

49-
useEffect(() => {
51+
const fetchTopicData = useCallback(() => {
5052
const controller = new AbortController();
5153
dispatch({ type: 'FETCH_START' });
5254

@@ -70,14 +72,23 @@ export default function Forum_Topic() {
7072
.catch(error => {
7173
if (error.code === 'ERR_CANCELED') return;
7274
console.error(error);
73-
dispatch({ type: 'FETCH_ERROR' });
75+
dispatch({
76+
type: 'FETCH_ERROR',
77+
payload: extractErrorMessage(error),
78+
status: error.response?.status || 0,
79+
});
7480
});
7581

7682
return () => {
7783
controller.abort();
7884
};
7985
}, [id]);
8086

87+
useEffect(() => {
88+
const cleanup = fetchTopicData();
89+
return cleanup;
90+
}, [fetchTopicData]);
91+
8192
const handleSort = (field) => {
8293
setSort(prev => ({
8394
field,
@@ -102,6 +113,30 @@ export default function Forum_Topic() {
102113
);
103114
}
104115

116+
if (error) {
117+
const isRateLimit = errorStatus === 429;
118+
return (
119+
<Container maxWidth="sm" sx={{ py: 8, textAlign: 'center' }}>
120+
<Alert
121+
severity={isRateLimit ? 'warning' : 'error'}
122+
sx={{ mb: 3 }}
123+
action={
124+
<Button color="inherit" size="small" startIcon={<RefreshIcon />} onClick={fetchTopicData}>
125+
Erneut versuchen
126+
</Button>
127+
}
128+
>
129+
{isRateLimit
130+
? 'Zu viele Anfragen — bitte warte einen Moment.'
131+
: `Fehler beim Laden des Themas: ${error}`}
132+
</Alert>
133+
<Button startIcon={<ArrowBackIcon />} onClick={() => navigate(-1)} sx={{ mt: 1 }}>
134+
Zurück
135+
</Button>
136+
</Container>
137+
);
138+
}
139+
105140
if (!topicData || !topicData.id) {
106141
return (
107142
<Container sx={{ py: 8, textAlign: 'center' }}>

‎src/Pages/Forum/Forum.jsx‎

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { useState, useEffect, use } from 'react';
2-
import api from '../../helper/api';
2+
import api, { extractErrorMessage } from '../../helper/api';
33
import useAuth from '../../context/useAuth';
44

55
// Mui
@@ -12,6 +12,8 @@ import Grid from '@mui/material/Grid';
1212
import Stack from '@mui/material/Stack';
1313
import TableSortLabel from '@mui/material/TableSortLabel';
1414
import Box from '@mui/material/Box';
15+
import Alert from '@mui/material/Alert';
16+
import RefreshIcon from '@mui/icons-material/Refresh';
1517

1618
// Eigene
1719
import Category from '../../components/Forum/Category';
@@ -24,6 +26,8 @@ const Forum = () => {
2426
const alertsManagerRef = use(AlertsContext);
2527
const [categories, setCategories] = useState([]);
2628
const [loading, setloading] = useState(false);
29+
const [error, setError] = useState(null);
30+
const [errorStatus, setErrorStatus] = useState(null);
2731
const [open, setOpen] = useState(false);
2832
const handleOpen = () => { setOpen(true); };
2933
const handleClose = () => { setOpen(false); };
@@ -37,18 +41,26 @@ const Forum = () => {
3741
setUpdateData(prev => !prev);
3842
}
3943

40-
// Get all categories
41-
useEffect(() => {
44+
const fetchCategories = () => {
4245
setloading(true);
46+
setError(null);
47+
setErrorStatus(null);
4348
api.get("/forum/category")
4449
.then(response => {
4550
setCategories(response.data);
4651
setloading(false);
4752
})
4853
.catch(err => {
4954
console.error(err);
55+
setError(extractErrorMessage(err));
56+
setErrorStatus(err.response?.status || 0);
5057
setloading(false);
5158
});
59+
};
60+
61+
// Get all categories
62+
useEffect(() => {
63+
fetchCategories();
5264
}, [updateData]);
5365

5466
const handleDelete = (deletedCategory) => {
@@ -174,15 +186,30 @@ const Forum = () => {
174186
</Box>
175187

176188
<Stack spacing={2} sx={{ mt: { xs: 0, md: 2 } }}>
177-
{categories.length ? (
189+
{error && (
190+
<Alert
191+
severity={errorStatus === 429 ? 'warning' : 'error'}
192+
action={
193+
<Button color="inherit" size="small" startIcon={<RefreshIcon />} onClick={fetchCategories}>
194+
Erneut versuchen
195+
</Button>
196+
}
197+
sx={{ mb: 2 }}
198+
>
199+
{errorStatus === 429
200+
? 'Zu viele Anfragen — bitte warte einen Moment.'
201+
: `Fehler beim Laden der Kategorien: ${error}`}
202+
</Alert>
203+
)}
204+
{!error && categories.length ? (
178205
categoryComponents
179-
) : loading ? (
206+
) : !error && loading ? (
180207
<Skeleton_Category />
181-
) : (
208+
) : !error ? (
182209
<Box sx={{ textAlign: 'center', py: 8, bgcolor: 'background.paper', borderRadius: 2 }}>
183210
<Typography color="text.secondary">Keine Kategorien vorhanden. Erstelle die erste!</Typography>
184211
</Box>
185-
)}
212+
) : null}
186213
</Stack>
187214

188215
<Modal

0 commit comments

Comments
 (0)