Auto-redirect to login on 401 API responses

When a session token expires, API calls return 401 but nothing caught
it — errors were shown as generic dialogs or swallowed. Now both
apiClient and streamingService detect 401 responses and clear auth
state, which causes App.tsx to render the login modal automatically.
This commit is contained in:
Viktor Barzin 2026-02-13 21:16:53 +00:00
parent 3acf8db7af
commit a1829957c1
No known key found for this signature in database
GPG key ID: 0EB088298288D958
4 changed files with 29 additions and 1 deletions

View file

@ -18,6 +18,8 @@ import { Button } from './components/ui/button';
import { Filter } from 'lucide-react';
import type { GeoJSONFeatureCollection, PropertyProperties, PropertyFeature, POI, POITravelFilter } from '@/types';
import { refreshListings, streamListingGeoJSON, fetchUserPOIs, type StreamingProgress } from '@/services';
import { setOnUnauthorized } from '@/services/apiClient';
import { clearPasskeyUser } from './auth/passkeyService';
import { poiMetricPropertyName, injectPoiMetricProperty } from '@/utils/poiUtils';
import { useTaskProgress } from '@/hooks/useTaskProgress';
@ -94,6 +96,13 @@ function App() {
}
}, []);
useEffect(() => {
setOnUnauthorized(() => {
clearPasskeyUser();
setUser(null);
});
}, []);
const handlePasskeyLogin = (passkeyUser: AuthUser) => {
setUser(passkeyUser);
};

View file

@ -3,6 +3,18 @@
import type { AuthUser } from '@/auth/types';
import { ApiError } from '@/types';
let onUnauthorized: (() => void) | null = null;
export function setOnUnauthorized(handler: () => void): void {
onUnauthorized = handler;
}
export function fireUnauthorized(): void {
if (onUnauthorized) {
onUnauthorized();
}
}
export interface RequestOptions {
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
params?: Record<string, string | number | boolean | Date | undefined>;
@ -61,6 +73,9 @@ export async function apiRequest<T>(
const response = await fetch(url, fetchOptions);
if (!response.ok) {
if (response.status === 401) {
fireUnauthorized();
}
throw new ApiError(`Error: ${response.status}`, response.status);
}

View file

@ -5,6 +5,7 @@ import type { PropertyFeature } from '@/types';
import type { ParameterValues } from '@/components/FilterPanel';
import { ApiError } from '@/types';
import { API_ENDPOINTS } from '@/constants';
import { fireUnauthorized } from './apiClient';
/**
* Build query string from parameters object
@ -87,6 +88,9 @@ export async function* streamListingGeoJSON(
});
if (!response.ok) {
if (response.status === 401) {
fireUnauthorized();
}
throw new ApiError(`Error: ${response.status}`, response.status);
}

View file

@ -1 +1 @@
{"root":["./src/App.tsx","./src/AppSidebar.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/auth/authService.ts","./src/auth/config.ts","./src/auth/errors.ts","./src/auth/passkeyService.ts","./src/auth/types.ts","./src/components/ActiveQuery.tsx","./src/components/AlertError.tsx","./src/components/AuthCallback.tsx","./src/components/FilterPanel.tsx","./src/components/Header.tsx","./src/components/HealthIndicator.tsx","./src/components/ListView.tsx","./src/components/LoginModal.tsx","./src/components/Map.tsx","./src/components/POIManager.tsx","./src/components/PropertyCard.tsx","./src/components/Spinner.tsx","./src/components/StatsBar.tsx","./src/components/StreamingProgressBar.tsx","./src/components/TaskIndicator.tsx","./src/components/TaskProgressDrawer.tsx","./src/components/VisualizationCard.tsx","./src/components/ui/DatePicker.tsx","./src/components/ui/accordion.tsx","./src/components/ui/alert-dialog.tsx","./src/components/ui/breadcrumb.tsx","./src/components/ui/button.tsx","./src/components/ui/calendar.tsx","./src/components/ui/checkbox.tsx","./src/components/ui/dialog.tsx","./src/components/ui/form.tsx","./src/components/ui/hover-card.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/popover.tsx","./src/components/ui/progress.tsx","./src/components/ui/range-slider-field.tsx","./src/components/ui/scroll-area.tsx","./src/components/ui/select.tsx","./src/components/ui/separator.tsx","./src/components/ui/sheet.tsx","./src/components/ui/sidebar.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/slider.tsx","./src/components/ui/tabs.tsx","./src/components/ui/tooltip.tsx","./src/constants/colorSchemes.ts","./src/constants/index.ts","./src/hooks/use-mobile.ts","./src/lib/utils.ts","./src/services/apiClient.ts","./src/services/healthService.ts","./src/services/index.ts","./src/services/listingService.ts","./src/services/poiService.ts","./src/services/streamingService.ts","./src/services/taskService.ts","./src/types/index.ts","./src/utils/mapUtils.ts","./src/utils/poiUtils.ts"],"version":"5.8.3"}
{"root":["./src/app.tsx","./src/appsidebar.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/auth/authservice.ts","./src/auth/config.ts","./src/auth/errors.ts","./src/auth/passkeyservice.ts","./src/auth/types.ts","./src/components/activequery.tsx","./src/components/alerterror.tsx","./src/components/authcallback.tsx","./src/components/filterpanel.tsx","./src/components/header.tsx","./src/components/healthindicator.tsx","./src/components/listview.tsx","./src/components/loginmodal.tsx","./src/components/map.tsx","./src/components/poimanager.tsx","./src/components/propertycard.tsx","./src/components/spinner.tsx","./src/components/statsbar.tsx","./src/components/streamingprogressbar.tsx","./src/components/taskindicator.tsx","./src/components/taskprogressdrawer.tsx","./src/components/visualizationcard.tsx","./src/components/ui/datepicker.tsx","./src/components/ui/accordion.tsx","./src/components/ui/alert-dialog.tsx","./src/components/ui/breadcrumb.tsx","./src/components/ui/button.tsx","./src/components/ui/calendar.tsx","./src/components/ui/checkbox.tsx","./src/components/ui/dialog.tsx","./src/components/ui/form.tsx","./src/components/ui/hover-card.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/popover.tsx","./src/components/ui/progress.tsx","./src/components/ui/range-slider-field.tsx","./src/components/ui/scroll-area.tsx","./src/components/ui/select.tsx","./src/components/ui/separator.tsx","./src/components/ui/sheet.tsx","./src/components/ui/sidebar.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/slider.tsx","./src/components/ui/tabs.tsx","./src/components/ui/tooltip.tsx","./src/constants/colorschemes.ts","./src/constants/index.ts","./src/hooks/use-mobile.ts","./src/hooks/usetaskprogress.ts","./src/lib/utils.ts","./src/services/apiclient.ts","./src/services/healthservice.ts","./src/services/index.ts","./src/services/listingservice.ts","./src/services/poiservice.ts","./src/services/streamingservice.ts","./src/services/taskservice.ts","./src/types/index.ts","./src/utils/maputils.ts","./src/utils/poiutils.ts"],"version":"5.8.3"}