apply security audit
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import React, { createContext, useCallback, useContext, useState, useEffect, ReactNode } from 'react';
|
||||
import { AuthContextType, AuthState, User } from '../types/auth.types';
|
||||
import { authService } from '../services/auth';
|
||||
import { getToken, setToken, removeToken, getUser, setUser, removeUser } from '../utils/storage';
|
||||
import { getToken, setToken, removeToken, getUser, setUser, removeUser, setRefreshToken, removeRefreshToken } from '../utils/storage';
|
||||
import { useNotification } from './NotificationContext';
|
||||
import { setAuthInitialized } from '../services/api';
|
||||
|
||||
@@ -71,10 +71,11 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
||||
try {
|
||||
setState((prev) => ({ ...prev, isLoading: true }));
|
||||
|
||||
const { token, user } = await authService.handleCallback(code);
|
||||
const { token, refreshToken, user } = await authService.handleCallback(code);
|
||||
|
||||
// Save to localStorage
|
||||
setToken(token);
|
||||
setRefreshToken(refreshToken);
|
||||
setUser(user);
|
||||
|
||||
// Update state
|
||||
@@ -115,6 +116,7 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
||||
|
||||
// Clear local state
|
||||
removeToken();
|
||||
removeRefreshToken();
|
||||
removeUser();
|
||||
setState({
|
||||
user: null,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Container,
|
||||
Typography,
|
||||
@@ -10,35 +9,16 @@ import {
|
||||
Switch,
|
||||
Divider,
|
||||
Box,
|
||||
Button,
|
||||
ToggleButtonGroup,
|
||||
ToggleButton,
|
||||
} from '@mui/material';
|
||||
import { Settings as SettingsIcon, Notifications, Palette, Language, Save, SettingsBrightness, LightMode, DarkMode } from '@mui/icons-material';
|
||||
import { Settings as SettingsIcon, Notifications, Palette, Language, SettingsBrightness, LightMode, DarkMode } from '@mui/icons-material';
|
||||
import DashboardLayout from '../components/dashboard/DashboardLayout';
|
||||
import { useNotification } from '../contexts/NotificationContext';
|
||||
import { useThemeMode } from '../contexts/ThemeContext';
|
||||
|
||||
function Settings() {
|
||||
const notification = useNotification();
|
||||
const { themeMode, setThemeMode } = useThemeMode();
|
||||
|
||||
// Settings state
|
||||
const [emailNotifications, setEmailNotifications] = useState(true);
|
||||
const [alarmNotifications, setAlarmNotifications] = useState(true);
|
||||
const [maintenanceReminders, setMaintenanceReminders] = useState(false);
|
||||
const [systemNotifications, setSystemNotifications] = useState(true);
|
||||
const [compactView, setCompactView] = useState(true);
|
||||
const [animations, setAnimations] = useState(true);
|
||||
|
||||
const handleSaveSettings = () => {
|
||||
try {
|
||||
// In a real application, save settings to backend
|
||||
notification.showSuccess('Einstellungen erfolgreich gespeichert');
|
||||
} catch (error) {
|
||||
notification.showError('Fehler beim Speichern der Einstellungen');
|
||||
}
|
||||
};
|
||||
return (
|
||||
<DashboardLayout>
|
||||
<Container maxWidth="lg">
|
||||
@@ -58,41 +38,24 @@ function Settings() {
|
||||
<Divider sx={{ mb: 2 }} />
|
||||
<FormGroup>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={emailNotifications}
|
||||
onChange={(e) => setEmailNotifications(e.target.checked)}
|
||||
/>
|
||||
}
|
||||
control={<Switch checked={false} disabled />}
|
||||
label="E-Mail-Benachrichtigungen"
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={alarmNotifications}
|
||||
onChange={(e) => setAlarmNotifications(e.target.checked)}
|
||||
/>
|
||||
}
|
||||
control={<Switch checked={false} disabled />}
|
||||
label="Einsatz-Alarme"
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={maintenanceReminders}
|
||||
onChange={(e) => setMaintenanceReminders(e.target.checked)}
|
||||
/>
|
||||
}
|
||||
control={<Switch checked={false} disabled />}
|
||||
label="Wartungserinnerungen"
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={systemNotifications}
|
||||
onChange={(e) => setSystemNotifications(e.target.checked)}
|
||||
/>
|
||||
}
|
||||
control={<Switch checked={false} disabled />}
|
||||
label="System-Benachrichtigungen"
|
||||
/>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ mt: 1 }}>
|
||||
(Bald verfügbar)
|
||||
</Typography>
|
||||
</FormGroup>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -109,23 +72,16 @@ function Settings() {
|
||||
<Divider sx={{ mb: 2 }} />
|
||||
<FormGroup>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={compactView}
|
||||
onChange={(e) => setCompactView(e.target.checked)}
|
||||
/>
|
||||
}
|
||||
control={<Switch checked={false} disabled />}
|
||||
label="Kompakte Ansicht"
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={animations}
|
||||
onChange={(e) => setAnimations(e.target.checked)}
|
||||
/>
|
||||
}
|
||||
control={<Switch checked={false} disabled />}
|
||||
label="Animationen"
|
||||
/>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ mt: 1 }}>
|
||||
(Bald verfügbar)
|
||||
</Typography>
|
||||
<Box sx={{ mt: 1 }}>
|
||||
<Typography variant="body2" sx={{ mb: 1 }}>
|
||||
Farbschema
|
||||
@@ -204,18 +160,6 @@ function Settings() {
|
||||
wird in zukünftigen Updates implementiert.
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
size="large"
|
||||
startIcon={<Save />}
|
||||
onClick={handleSaveSettings}
|
||||
>
|
||||
Einstellungen speichern
|
||||
</Button>
|
||||
</Box>
|
||||
</Container>
|
||||
</DashboardLayout>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,27 @@
|
||||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios';
|
||||
import { API_URL } from '../utils/config';
|
||||
import { getToken, removeToken, removeUser } from '../utils/storage';
|
||||
import { getToken, setToken, removeToken, removeUser, getRefreshToken, removeRefreshToken } from '../utils/storage';
|
||||
|
||||
let authInitialized = false;
|
||||
let isRedirectingToLogin = false;
|
||||
|
||||
let isRefreshing = false;
|
||||
let failedQueue: Array<{
|
||||
resolve: (token: string) => void;
|
||||
reject: (error: any) => void;
|
||||
}> = [];
|
||||
|
||||
function processQueue(error: any, token: string | null = null) {
|
||||
failedQueue.forEach((prom) => {
|
||||
if (error) {
|
||||
prom.reject(error);
|
||||
} else {
|
||||
prom.resolve(token!);
|
||||
}
|
||||
});
|
||||
failedQueue = [];
|
||||
}
|
||||
|
||||
export function setAuthInitialized(value: boolean): void {
|
||||
authInitialized = value;
|
||||
if (value === true) {
|
||||
@@ -54,15 +71,71 @@ class ApiService {
|
||||
(response) => response,
|
||||
async (error: AxiosError) => {
|
||||
if (error.response?.status === 401) {
|
||||
if (authInitialized && !isRedirectingToLogin) {
|
||||
isRedirectingToLogin = true;
|
||||
// Clear tokens and redirect to login
|
||||
console.warn('Unauthorized request, redirecting to login');
|
||||
removeToken();
|
||||
removeUser();
|
||||
window.location.href = '/login';
|
||||
const originalRequest = error.config as AxiosRequestConfig & { _retry?: boolean };
|
||||
|
||||
// Don't attempt refresh during auth initialization or if already retried
|
||||
if (!authInitialized || originalRequest._retry) {
|
||||
if (authInitialized && !isRedirectingToLogin) {
|
||||
isRedirectingToLogin = true;
|
||||
removeToken();
|
||||
removeRefreshToken();
|
||||
removeUser();
|
||||
window.location.href = '/login';
|
||||
}
|
||||
return Promise.reject(this.handleError(error));
|
||||
}
|
||||
|
||||
if (isRefreshing) {
|
||||
return new Promise<AxiosResponse>((resolve, reject) => {
|
||||
failedQueue.push({
|
||||
resolve: (token: string) => {
|
||||
originalRequest.headers = { ...originalRequest.headers, Authorization: `Bearer ${token}` };
|
||||
resolve(this.axiosInstance.request(originalRequest));
|
||||
},
|
||||
reject: (err: any) => {
|
||||
reject(err);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
originalRequest._retry = true;
|
||||
isRefreshing = true;
|
||||
|
||||
const refreshToken = getRefreshToken();
|
||||
if (!refreshToken) {
|
||||
isRefreshing = false;
|
||||
if (!isRedirectingToLogin) {
|
||||
isRedirectingToLogin = true;
|
||||
removeToken();
|
||||
removeRefreshToken();
|
||||
removeUser();
|
||||
window.location.href = '/login';
|
||||
}
|
||||
return Promise.reject(this.handleError(error));
|
||||
}
|
||||
|
||||
try {
|
||||
// Use a raw axios call (not the intercepted instance) to avoid loops
|
||||
const response = await axios.post(`${API_URL}/api/auth/refresh`, { refreshToken });
|
||||
const newToken = response.data.data.accessToken;
|
||||
setToken(newToken);
|
||||
processQueue(null, newToken);
|
||||
originalRequest.headers = { ...originalRequest.headers, Authorization: `Bearer ${newToken}` };
|
||||
return this.axiosInstance.request(originalRequest);
|
||||
} catch (refreshError) {
|
||||
processQueue(refreshError, null);
|
||||
if (!isRedirectingToLogin) {
|
||||
isRedirectingToLogin = true;
|
||||
removeToken();
|
||||
removeRefreshToken();
|
||||
removeUser();
|
||||
window.location.href = '/login';
|
||||
}
|
||||
return Promise.reject(this.handleError(error));
|
||||
} finally {
|
||||
isRefreshing = false;
|
||||
}
|
||||
// During initialization, silently reject without redirecting
|
||||
}
|
||||
|
||||
// Retry on 429 (Too Many Requests) with exponential backoff
|
||||
|
||||
@@ -6,6 +6,7 @@ const REDIRECT_URI = `${window.location.origin}/auth/callback`;
|
||||
|
||||
export interface AuthCallbackResponse {
|
||||
token: string;
|
||||
refreshToken: string;
|
||||
user: User;
|
||||
}
|
||||
|
||||
@@ -51,6 +52,7 @@ export const authService = {
|
||||
});
|
||||
return {
|
||||
token: response.data.data.accessToken,
|
||||
refreshToken: response.data.data.refreshToken,
|
||||
user: mapBackendUser(response.data.data.user),
|
||||
};
|
||||
},
|
||||
|
||||
@@ -28,6 +28,33 @@ export const removeToken = (): void => {
|
||||
}
|
||||
};
|
||||
|
||||
const REFRESH_TOKEN_KEY = 'auth_refresh_token';
|
||||
|
||||
export const getRefreshToken = (): string | null => {
|
||||
try {
|
||||
return localStorage.getItem(REFRESH_TOKEN_KEY);
|
||||
} catch (error) {
|
||||
console.error('Error getting refresh token from localStorage:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const setRefreshToken = (token: string): void => {
|
||||
try {
|
||||
localStorage.setItem(REFRESH_TOKEN_KEY, token);
|
||||
} catch (error) {
|
||||
console.error('Error setting refresh token in localStorage:', error);
|
||||
}
|
||||
};
|
||||
|
||||
export const removeRefreshToken = (): void => {
|
||||
try {
|
||||
localStorage.removeItem(REFRESH_TOKEN_KEY);
|
||||
} catch (error) {
|
||||
console.error('Error removing refresh token from localStorage:', error);
|
||||
}
|
||||
};
|
||||
|
||||
export const getUser = (): User | null => {
|
||||
try {
|
||||
const userStr = localStorage.getItem(USER_KEY);
|
||||
|
||||
Reference in New Issue
Block a user