Improve the dahboard 4
This commit is contained in:
@@ -13,21 +13,77 @@ import { ApiErrorHandler } from '../utils';
|
||||
* Automatically adds authentication headers to requests
|
||||
*/
|
||||
class AuthInterceptor {
|
||||
static isTokenExpired(token: string): boolean {
|
||||
try {
|
||||
const payload = JSON.parse(atob(token.split('.')[1]));
|
||||
const currentTime = Math.floor(Date.now() / 1000);
|
||||
return payload.exp <= currentTime;
|
||||
} catch (error) {
|
||||
console.warn('Error parsing token:', error);
|
||||
return true; // Treat invalid tokens as expired
|
||||
}
|
||||
}
|
||||
|
||||
static isTokenExpiringSoon(token: string, bufferMinutes: number = 5): boolean {
|
||||
try {
|
||||
const payload = JSON.parse(atob(token.split('.')[1]));
|
||||
const currentTime = Math.floor(Date.now() / 1000);
|
||||
const bufferSeconds = bufferMinutes * 60;
|
||||
return payload.exp <= (currentTime + bufferSeconds);
|
||||
} catch (error) {
|
||||
console.warn('Error parsing token for expiration check:', error);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
static async refreshTokenIfNeeded(): Promise<void> {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
const refreshToken = localStorage.getItem('refresh_token');
|
||||
|
||||
if (!token || !refreshToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If token is expiring within 5 minutes, proactively refresh
|
||||
if (this.isTokenExpiringSoon(token, 5)) {
|
||||
try {
|
||||
const baseURL = (apiClient as any).baseURL || window.location.origin;
|
||||
const response = await fetch(`${baseURL}/api/v1/auth/refresh`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ refresh_token: refreshToken }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
localStorage.setItem('auth_token', data.access_token);
|
||||
if (data.refresh_token) {
|
||||
localStorage.setItem('refresh_token', data.refresh_token);
|
||||
}
|
||||
} else {
|
||||
console.warn('Token refresh failed:', response.status);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Token refresh error:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static setup() {
|
||||
apiClient.addRequestInterceptor({
|
||||
onRequest: async (config: RequestConfig) => {
|
||||
// Proactively refresh token if needed
|
||||
await this.refreshTokenIfNeeded();
|
||||
|
||||
let token = localStorage.getItem('auth_token');
|
||||
|
||||
console.log('🔐 AuthInterceptor: Checking auth token...', token ? 'Found' : 'Missing');
|
||||
if (token) {
|
||||
console.log('🔐 AuthInterceptor: Token preview:', token.substring(0, 20) + '...');
|
||||
}
|
||||
|
||||
// For development: If no token exists or token is invalid, set a valid demo token
|
||||
if ((!token || token === 'demo-development-token') && window.location.hostname === 'localhost') {
|
||||
console.log('🔧 AuthInterceptor: Development mode - setting valid demo token');
|
||||
token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxN2Q1ZTJjZC1hMjk4LTQyNzEtODZjNi01NmEzZGNiNDE0ZWUiLCJ1c2VyX2lkIjoiMTdkNWUyY2QtYTI5OC00MjcxLTg2YzYtNTZhM2RjYjQxNGVlIiwiZW1haWwiOiJ0ZXN0QGRlbW8uY29tIiwidHlwZSI6ImFjY2VzcyIsImV4cCI6MTc1NTI3MzEyNSwiaWF0IjoxNzU1MjcxMzI1LCJpc3MiOiJiYWtlcnktYXV0aCIsImZ1bGxfbmFtZSI6IkRlbW8gVXNlciIsImlzX3ZlcmlmaWVkIjpmYWxzZSwiaXNfYWN0aXZlIjp0cnVlLCJyb2xlIjoidXNlciJ9.RBfzH9L_NKySYkyLzBLYAApnrCFNK4OsGLLO-eCaTSI';
|
||||
localStorage.setItem('auth_token', token);
|
||||
// Check if token is expired
|
||||
if (this.isTokenExpired(token)) {
|
||||
console.warn('Token expired, removing from storage');
|
||||
localStorage.removeItem('auth_token');
|
||||
token = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (token) {
|
||||
@@ -35,9 +91,8 @@ class AuthInterceptor {
|
||||
...config.headers,
|
||||
Authorization: `Bearer ${token}`,
|
||||
};
|
||||
console.log('🔐 AuthInterceptor: Added Authorization header');
|
||||
} else {
|
||||
console.warn('⚠️ AuthInterceptor: No auth token found in localStorage');
|
||||
console.warn('No valid auth token found - authentication required');
|
||||
}
|
||||
|
||||
return config;
|
||||
@@ -48,9 +103,6 @@ class AuthInterceptor {
|
||||
throw error;
|
||||
},
|
||||
});
|
||||
|
||||
// Note: 401 handling is now managed by ErrorRecoveryInterceptor
|
||||
// This allows token refresh to work before redirecting to login
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,8 +231,7 @@ class ErrorRecoveryInterceptor {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.failedQueue.push({ resolve, reject });
|
||||
}).then(token => {
|
||||
originalRequest.headers['Authorization'] = `Bearer ${token}`;
|
||||
return apiClient.request(originalRequest.url, originalRequest);
|
||||
return this.retryRequestWithNewToken(originalRequest, token);
|
||||
}).catch(err => {
|
||||
throw err;
|
||||
});
|
||||
@@ -196,7 +247,7 @@ class ErrorRecoveryInterceptor {
|
||||
throw new Error('No refresh token available');
|
||||
}
|
||||
|
||||
// Attempt to refresh token
|
||||
// Use direct fetch to avoid interceptor recursion
|
||||
const baseURL = (apiClient as any).baseURL || window.location.origin;
|
||||
const response = await fetch(`${baseURL}/api/v1/auth/refresh`, {
|
||||
method: 'POST',
|
||||
@@ -207,22 +258,31 @@ class ErrorRecoveryInterceptor {
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Token refresh failed');
|
||||
throw new Error(`Token refresh failed: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const newToken = data.access_token;
|
||||
|
||||
if (!newToken) {
|
||||
throw new Error('No access token received');
|
||||
}
|
||||
|
||||
localStorage.setItem('auth_token', newToken);
|
||||
|
||||
// Update new refresh token if provided
|
||||
if (data.refresh_token) {
|
||||
localStorage.setItem('refresh_token', data.refresh_token);
|
||||
}
|
||||
|
||||
// Process failed queue
|
||||
this.processQueue(null, newToken);
|
||||
|
||||
// Retry original request
|
||||
originalRequest.headers['Authorization'] = `Bearer ${newToken}`;
|
||||
return apiClient.request(originalRequest.url, originalRequest);
|
||||
// Retry original request with new token
|
||||
return this.retryRequestWithNewToken(originalRequest, newToken);
|
||||
|
||||
} catch (refreshError) {
|
||||
console.warn('Token refresh failed:', refreshError);
|
||||
this.processQueue(refreshError, null);
|
||||
|
||||
// Clear auth data and redirect to login
|
||||
@@ -230,7 +290,8 @@ class ErrorRecoveryInterceptor {
|
||||
localStorage.removeItem('refresh_token');
|
||||
localStorage.removeItem('user_data');
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
// Only redirect if we're not already on the login page
|
||||
if (typeof window !== 'undefined' && !window.location.pathname.includes('/login')) {
|
||||
window.location.href = '/login';
|
||||
}
|
||||
|
||||
@@ -245,6 +306,55 @@ class ErrorRecoveryInterceptor {
|
||||
});
|
||||
}
|
||||
|
||||
private static async retryRequestWithNewToken(originalRequest: any, token: string) {
|
||||
try {
|
||||
// Use direct fetch instead of apiClient to avoid interceptor recursion
|
||||
const url = originalRequest.url || originalRequest.endpoint;
|
||||
const method = originalRequest.method || 'GET';
|
||||
|
||||
const fetchOptions: RequestInit = {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
...originalRequest.headers
|
||||
}
|
||||
};
|
||||
|
||||
// Add body for non-GET requests
|
||||
if (method !== 'GET' && originalRequest.body) {
|
||||
fetchOptions.body = typeof originalRequest.body === 'string'
|
||||
? originalRequest.body
|
||||
: JSON.stringify(originalRequest.body);
|
||||
}
|
||||
|
||||
// Add query parameters if present
|
||||
let fullUrl = url;
|
||||
if (originalRequest.params) {
|
||||
const urlWithParams = new URL(fullUrl, (apiClient as any).baseURL);
|
||||
Object.entries(originalRequest.params).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null) {
|
||||
urlWithParams.searchParams.append(key, String(value));
|
||||
}
|
||||
});
|
||||
fullUrl = urlWithParams.toString();
|
||||
}
|
||||
|
||||
// Retry request with refreshed token
|
||||
|
||||
const response = await fetch(fullUrl, fetchOptions);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Request failed: ${response.status}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (retryError) {
|
||||
console.warn('Request retry failed:', retryError);
|
||||
throw retryError;
|
||||
}
|
||||
}
|
||||
|
||||
private static processQueue(error: any, token: string | null) {
|
||||
this.failedQueue.forEach(({ resolve, reject }) => {
|
||||
if (error) {
|
||||
|
||||
@@ -6,12 +6,13 @@
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { externalService } from '../services/external.service';
|
||||
import type { WeatherData, TrafficData, WeatherForecast } from '../services/external.service';
|
||||
import type { WeatherData, TrafficData, WeatherForecast, HourlyForecast } from '../services/external.service';
|
||||
|
||||
export const useExternal = () => {
|
||||
const [weatherData, setWeatherData] = useState<WeatherData | null>(null);
|
||||
const [trafficData, setTrafficData] = useState<TrafficData | null>(null);
|
||||
const [weatherForecast, setWeatherForecast] = useState<WeatherForecast[]>([]);
|
||||
const [hourlyForecast, setHourlyForecast] = useState<HourlyForecast[]>([]);
|
||||
const [trafficForecast, setTrafficForecast] = useState<TrafficData[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -67,6 +68,32 @@ export const useExternal = () => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Get Hourly Weather Forecast
|
||||
*/
|
||||
const getHourlyWeatherForecast = useCallback(async (
|
||||
tenantId: string,
|
||||
lat: number,
|
||||
lon: number,
|
||||
hours: number = 48
|
||||
): Promise<HourlyForecast[]> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const forecast = await externalService.getHourlyWeatherForecast(tenantId, lat, lon, hours);
|
||||
setHourlyForecast(forecast);
|
||||
|
||||
return forecast;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to get hourly weather forecast';
|
||||
setError(message);
|
||||
throw error;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Get Historical Weather Data
|
||||
*/
|
||||
@@ -194,11 +221,13 @@ export const useExternal = () => {
|
||||
weatherData,
|
||||
trafficData,
|
||||
weatherForecast,
|
||||
hourlyForecast,
|
||||
trafficForecast,
|
||||
isLoading,
|
||||
error,
|
||||
getCurrentWeather,
|
||||
getWeatherForecast,
|
||||
getHourlyWeatherForecast,
|
||||
getHistoricalWeather,
|
||||
getCurrentTraffic,
|
||||
getTrafficForecast,
|
||||
|
||||
@@ -40,6 +40,18 @@ export interface WeatherForecast {
|
||||
wind_speed?: number;
|
||||
}
|
||||
|
||||
export interface HourlyForecast {
|
||||
forecast_datetime: string;
|
||||
generated_at: string;
|
||||
temperature: number;
|
||||
precipitation: number;
|
||||
humidity: number;
|
||||
wind_speed: number;
|
||||
description: string;
|
||||
source: string;
|
||||
hour: number;
|
||||
}
|
||||
|
||||
export class ExternalService {
|
||||
/**
|
||||
* Get Current Weather Data
|
||||
@@ -104,6 +116,43 @@ export class ExternalService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Hourly Weather Forecast (NEW)
|
||||
*/
|
||||
async getHourlyWeatherForecast(
|
||||
tenantId: string,
|
||||
lat: number,
|
||||
lon: number,
|
||||
hours: number = 48
|
||||
): Promise<HourlyForecast[]> {
|
||||
try {
|
||||
console.log(`🕒 Fetching hourly weather forecast from AEMET API for tenant ${tenantId}`, {
|
||||
latitude: lat,
|
||||
longitude: lon,
|
||||
hours: hours
|
||||
});
|
||||
|
||||
const response = await apiClient.post(`/tenants/${tenantId}/weather/hourly-forecast`, {
|
||||
latitude: lat,
|
||||
longitude: lon,
|
||||
hours: hours
|
||||
});
|
||||
|
||||
// Handle response format
|
||||
if (Array.isArray(response)) {
|
||||
return response;
|
||||
} else if (response && response.data) {
|
||||
return response.data;
|
||||
} else {
|
||||
console.warn('Unexpected hourly forecast response format:', response);
|
||||
return [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch hourly forecast from AEMET API:', error);
|
||||
throw new Error(`Hourly forecast unavailable: ${error instanceof Error ? error.message : 'AEMET API connection failed'}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Historical Weather Data
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user