From f4939ef6c587e939e16faa56c25c64479682825f Mon Sep 17 00:00:00 2001 From: thomas Date: Tue, 25 Aug 2026 19:42:37 +0200 Subject: [PATCH] fix(frontend): Reset-Link auswerten, Absturz bei defektem localStorage verhindern MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PasswordReset las den Query-Parameter token nie aus. Wer auf den Link in der Reset-Mail klickte, landete wieder auf Schritt 1 mit leerem Token-Feld — der Mailversand aus d9fecb6 lief damit ins Leere. Der Token wird jetzt aus der URL übernommen und direkt Schritt 2 gezeigt. - "Zurück zum Login" und die Weiterleitung nach dem Zurücksetzen zeigten auf "/" und damit beim Unterpfad-Deployment auf das Portal. - PublicUserList rief JSON.parse(localStorage.getItem(...)) ungeschützt im Render-Pfad auf. Ein beschädigter Eintrag — oder ein Browser, der Site-Data blockiert — ließ die gesamte öffentliche Liste weiß werden. Lesen und Schreiben laufen jetzt über try/catch, und der Wert wird auf Plausibilität geprüft. - Der Typfilter-Effekt feuerte auch beim Mounten, obwohl useUsers bereits selbst lädt: jeder Aufruf der öffentlichen Liste setzte zwei identische Anfragen ab. Der erste Lauf wird jetzt übersprungen. Co-Authored-By: Claude Opus 5 --- .../src/components/auth/PasswordReset.js | 26 +++++++++-- .../src/components/public/PublicUserList.js | 44 ++++++++++++++----- .../src/components/auth/PasswordReset.js | 26 +++++++++-- .../src/components/public/PublicUserList.js | 44 ++++++++++++++----- .../src/components/auth/PasswordReset.js | 26 +++++++++-- .../src/components/public/PublicUserList.js | 44 ++++++++++++++----- 6 files changed, 168 insertions(+), 42 deletions(-) diff --git a/drohnenfuehrer/frontend/src/components/auth/PasswordReset.js b/drohnenfuehrer/frontend/src/components/auth/PasswordReset.js index e08c41c..71a9470 100644 --- a/drohnenfuehrer/frontend/src/components/auth/PasswordReset.js +++ b/drohnenfuehrer/frontend/src/components/auth/PasswordReset.js @@ -1,11 +1,29 @@ import React, { useState } from 'react'; import { requestPasswordReset, resetPassword } from '../../services/passwordReset'; +import { BASE_PATH } from '../../utils/constants'; import './PasswordReset.css'; +// Ziel nach dem Zuruecksetzen bzw. fuer "Zurueck zum Login": die App selbst. +// Ein absolutes "/" landet beim Unterpfad-Deployment auf dem Portal. +const APP_HOME = `${BASE_PATH}/`; + +// Token aus dem Link der Reset-Mail lesen (…/passwort-zuruecksetzen?token=…). +// Ohne das landete jeder, der auf den gemailten Link klickt, wieder auf Schritt 1 +// mit leerem Token-Feld – der Mailversand war damit wirkungslos. +const tokenFromUrl = () => { + if (typeof window === 'undefined') return ''; + try { + return new URLSearchParams(window.location.search).get('token') || ''; + } catch { + return ''; + } +}; + function PasswordReset() { - const [step, setStep] = useState('request'); // 'request' or 'reset' + const initialToken = tokenFromUrl(); + const [step, setStep] = useState(initialToken ? 'reset' : 'request'); const [username, setUsername] = useState(''); - const [token, setToken] = useState(''); + const [token, setToken] = useState(initialToken); const [newPassword, setNewPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); const [loading, setLoading] = useState(false); @@ -62,7 +80,7 @@ function PasswordReset() { setToken(''); setNewPassword(''); setConfirmPassword(''); - window.location.href = '/'; // Redirect to login + window.location.href = APP_HOME; // Redirect to login }, 2000); } else { setMessage({ type: 'error', text: result.message }); @@ -100,7 +118,7 @@ function PasswordReset() {
- Zurück zum Login + Zurück zum Login
) : ( diff --git a/drohnenfuehrer/frontend/src/components/public/PublicUserList.js b/drohnenfuehrer/frontend/src/components/public/PublicUserList.js index 5a74055..446f533 100644 --- a/drohnenfuehrer/frontend/src/components/public/PublicUserList.js +++ b/drohnenfuehrer/frontend/src/components/public/PublicUserList.js @@ -1,4 +1,4 @@ -import React, { useState, useMemo, useEffect, useCallback } from 'react'; +import React, { useState, useMemo, useEffect, useCallback, useRef } from 'react'; import { useConfigContext } from '../../contexts/ConfigContext'; import MapView from '../map/MapView'; import FilterPanel from '../users/FilterPanel'; @@ -6,18 +6,32 @@ import { calculateDistance } from '../../utils/helpers'; import { getGeocodeByPostalCode } from '../../services/users'; import './PublicUserList.css'; +const LOCATION_KEY = 'userLocation'; + +// localStorage kann werfen (privater Modus, blockierte Site-Data) und der Inhalt +// kann beschaedigt sein. Beides darf die Seite nicht mitreissen. +const readStoredLocation = () => { + try { + const raw = localStorage.getItem(LOCATION_KEY); + if (!raw) return null; + const parsed = JSON.parse(raw); + return (typeof parsed?.lat === 'number' && typeof parsed?.lng === 'number') ? parsed : null; + } catch { + return null; + } +}; + const PublicUserList = ({ users, loading, onRefetch }) => { const { userTypeLabels } = useConfigContext(); - // Load saved location from localStorage on mount - const savedLocation = localStorage.getItem('userLocation'); - const initialCoords = savedLocation ? JSON.parse(savedLocation) : null; - const [showMap, setShowMap] = useState(true); const [showFilters, setShowFilters] = useState(false); - const [locationStatus, setLocationStatus] = useState(initialCoords ? 'granted' : 'idle'); + // Gespeicherten Standort lazy und abgesichert lesen. Vorher stand ein + // ungeschuetztes JSON.parse im Render-Pfad: ein beschaedigter Eintrag - oder ein + // Browser, der Site-Data blockiert - hat die gesamte Liste weiss werden lassen. + const [coords, setCoords] = useState(readStoredLocation); + const [locationStatus, setLocationStatus] = useState(coords ? 'granted' : 'idle'); const [locationError, setLocationError] = useState(''); - const [coords, setCoords] = useState(initialCoords); const [radiusKm, setRadiusKm] = useState(100); const [postalCode, setPostalCode] = useState(''); const [postalSearching, setPostalSearching] = useState(false); @@ -28,8 +42,11 @@ const PublicUserList = ({ users, loading, onRefetch }) => { // Save location to localStorage whenever it changes useEffect(() => { - if (coords) { - localStorage.setItem('userLocation', JSON.stringify(coords)); + if (!coords) return; + try { + localStorage.setItem(LOCATION_KEY, JSON.stringify(coords)); + } catch { + // Speicher nicht verfuegbar - der Standort gilt dann nur fuer diese Sitzung. } }, [coords]); @@ -37,8 +54,15 @@ const PublicUserList = ({ users, loading, onRefetch }) => { setFilters(prev => ({ ...prev, [key]: value })); }, []); - // Re-fetch from backend when type filter changes (server-side filtering) + // Re-fetch from backend when type filter changes (server-side filtering). + // Der erste Lauf wird uebersprungen: useUsers laedt beim Mounten bereits selbst, + // sonst setzt jeder Seitenaufruf zwei identische Anfragen ab. + const skipInitialRefetch = useRef(true); useEffect(() => { + if (skipInitialRefetch.current) { + skipInitialRefetch.current = false; + return; + } if (onRefetch) { onRefetch(filters.type ? { type: filters.type } : {}); } diff --git a/nachsuche/frontend/src/components/auth/PasswordReset.js b/nachsuche/frontend/src/components/auth/PasswordReset.js index e08c41c..71a9470 100644 --- a/nachsuche/frontend/src/components/auth/PasswordReset.js +++ b/nachsuche/frontend/src/components/auth/PasswordReset.js @@ -1,11 +1,29 @@ import React, { useState } from 'react'; import { requestPasswordReset, resetPassword } from '../../services/passwordReset'; +import { BASE_PATH } from '../../utils/constants'; import './PasswordReset.css'; +// Ziel nach dem Zuruecksetzen bzw. fuer "Zurueck zum Login": die App selbst. +// Ein absolutes "/" landet beim Unterpfad-Deployment auf dem Portal. +const APP_HOME = `${BASE_PATH}/`; + +// Token aus dem Link der Reset-Mail lesen (…/passwort-zuruecksetzen?token=…). +// Ohne das landete jeder, der auf den gemailten Link klickt, wieder auf Schritt 1 +// mit leerem Token-Feld – der Mailversand war damit wirkungslos. +const tokenFromUrl = () => { + if (typeof window === 'undefined') return ''; + try { + return new URLSearchParams(window.location.search).get('token') || ''; + } catch { + return ''; + } +}; + function PasswordReset() { - const [step, setStep] = useState('request'); // 'request' or 'reset' + const initialToken = tokenFromUrl(); + const [step, setStep] = useState(initialToken ? 'reset' : 'request'); const [username, setUsername] = useState(''); - const [token, setToken] = useState(''); + const [token, setToken] = useState(initialToken); const [newPassword, setNewPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); const [loading, setLoading] = useState(false); @@ -62,7 +80,7 @@ function PasswordReset() { setToken(''); setNewPassword(''); setConfirmPassword(''); - window.location.href = '/'; // Redirect to login + window.location.href = APP_HOME; // Redirect to login }, 2000); } else { setMessage({ type: 'error', text: result.message }); @@ -100,7 +118,7 @@ function PasswordReset() {
- Zurück zum Login + Zurück zum Login
) : ( diff --git a/nachsuche/frontend/src/components/public/PublicUserList.js b/nachsuche/frontend/src/components/public/PublicUserList.js index 9148e67..59b4bd8 100644 --- a/nachsuche/frontend/src/components/public/PublicUserList.js +++ b/nachsuche/frontend/src/components/public/PublicUserList.js @@ -1,4 +1,4 @@ -import React, { useState, useMemo, useEffect, useCallback } from 'react'; +import React, { useState, useMemo, useEffect, useCallback, useRef } from 'react'; import { useConfigContext } from '../../contexts/ConfigContext'; import MapView from '../map/MapView'; import FilterPanel from '../users/FilterPanel'; @@ -6,18 +6,32 @@ import { calculateDistance } from '../../utils/helpers'; import { getGeocodeByPostalCode } from '../../services/users'; import './PublicUserList.css'; +const LOCATION_KEY = 'userLocation'; + +// localStorage kann werfen (privater Modus, blockierte Site-Data) und der Inhalt +// kann beschädigt sein. Beides darf die Seite nicht mitreißen. +const readStoredLocation = () => { + try { + const raw = localStorage.getItem(LOCATION_KEY); + if (!raw) return null; + const parsed = JSON.parse(raw); + return (typeof parsed?.lat === 'number' && typeof parsed?.lng === 'number') ? parsed : null; + } catch { + return null; + } +}; + const PublicUserList = ({ users, loading, onRefetch }) => { const { userTypeLabels } = useConfigContext(); - // Load saved location from localStorage on mount - const savedLocation = localStorage.getItem('userLocation'); - const initialCoords = savedLocation ? JSON.parse(savedLocation) : null; - const [showMap, setShowMap] = useState(true); const [showFilters, setShowFilters] = useState(false); - const [locationStatus, setLocationStatus] = useState(initialCoords ? 'granted' : 'idle'); + // Gespeicherten Standort lazy und abgesichert lesen. Vorher stand ein + // ungeschütztes JSON.parse im Render-Pfad: ein beschädigter Eintrag – oder ein + // Browser, der Site-Data blockiert – hat die gesamte Liste weiß werden lassen. + const [coords, setCoords] = useState(readStoredLocation); + const [locationStatus, setLocationStatus] = useState(coords ? 'granted' : 'idle'); const [locationError, setLocationError] = useState(''); - const [coords, setCoords] = useState(initialCoords); const [radiusKm, setRadiusKm] = useState(100); const [postalCode, setPostalCode] = useState(''); const [postalSearching, setPostalSearching] = useState(false); @@ -28,8 +42,11 @@ const PublicUserList = ({ users, loading, onRefetch }) => { // Save location to localStorage whenever it changes useEffect(() => { - if (coords) { - localStorage.setItem('userLocation', JSON.stringify(coords)); + if (!coords) return; + try { + localStorage.setItem(LOCATION_KEY, JSON.stringify(coords)); + } catch { + // Speicher nicht verfügbar – der Standort gilt dann nur für diese Sitzung. } }, [coords]); @@ -37,8 +54,15 @@ const PublicUserList = ({ users, loading, onRefetch }) => { setFilters(prev => ({ ...prev, [key]: value })); }, []); - // Re-fetch from backend when type filter changes (server-side filtering) + // Re-fetch from backend when type filter changes (server-side filtering). + // Der erste Lauf wird übersprungen: useUsers lädt beim Mounten bereits selbst, + // sonst setzt jeder Seitenaufruf zwei identische Anfragen ab. + const skipInitialRefetch = useRef(true); useEffect(() => { + if (skipInitialRefetch.current) { + skipInitialRefetch.current = false; + return; + } if (onRefetch) { onRefetch(filters.type ? { type: filters.type } : {}); } diff --git a/stoeberhunde/frontend/src/components/auth/PasswordReset.js b/stoeberhunde/frontend/src/components/auth/PasswordReset.js index e08c41c..71a9470 100644 --- a/stoeberhunde/frontend/src/components/auth/PasswordReset.js +++ b/stoeberhunde/frontend/src/components/auth/PasswordReset.js @@ -1,11 +1,29 @@ import React, { useState } from 'react'; import { requestPasswordReset, resetPassword } from '../../services/passwordReset'; +import { BASE_PATH } from '../../utils/constants'; import './PasswordReset.css'; +// Ziel nach dem Zuruecksetzen bzw. fuer "Zurueck zum Login": die App selbst. +// Ein absolutes "/" landet beim Unterpfad-Deployment auf dem Portal. +const APP_HOME = `${BASE_PATH}/`; + +// Token aus dem Link der Reset-Mail lesen (…/passwort-zuruecksetzen?token=…). +// Ohne das landete jeder, der auf den gemailten Link klickt, wieder auf Schritt 1 +// mit leerem Token-Feld – der Mailversand war damit wirkungslos. +const tokenFromUrl = () => { + if (typeof window === 'undefined') return ''; + try { + return new URLSearchParams(window.location.search).get('token') || ''; + } catch { + return ''; + } +}; + function PasswordReset() { - const [step, setStep] = useState('request'); // 'request' or 'reset' + const initialToken = tokenFromUrl(); + const [step, setStep] = useState(initialToken ? 'reset' : 'request'); const [username, setUsername] = useState(''); - const [token, setToken] = useState(''); + const [token, setToken] = useState(initialToken); const [newPassword, setNewPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); const [loading, setLoading] = useState(false); @@ -62,7 +80,7 @@ function PasswordReset() { setToken(''); setNewPassword(''); setConfirmPassword(''); - window.location.href = '/'; // Redirect to login + window.location.href = APP_HOME; // Redirect to login }, 2000); } else { setMessage({ type: 'error', text: result.message }); @@ -100,7 +118,7 @@ function PasswordReset() {
- Zurück zum Login + Zurück zum Login
) : ( diff --git a/stoeberhunde/frontend/src/components/public/PublicUserList.js b/stoeberhunde/frontend/src/components/public/PublicUserList.js index 3773b70..b3df037 100644 --- a/stoeberhunde/frontend/src/components/public/PublicUserList.js +++ b/stoeberhunde/frontend/src/components/public/PublicUserList.js @@ -1,4 +1,4 @@ -import React, { useState, useMemo, useEffect, useCallback } from 'react'; +import React, { useState, useMemo, useEffect, useCallback, useRef } from 'react'; import { useConfigContext } from '../../contexts/ConfigContext'; import MapView from '../map/MapView'; import FilterPanel from '../users/FilterPanel'; @@ -6,18 +6,32 @@ import { calculateDistance } from '../../utils/helpers'; import { getGeocodeByPostalCode } from '../../services/users'; import './PublicUserList.css'; +const LOCATION_KEY = 'userLocation'; + +// localStorage kann werfen (privater Modus, blockierte Site-Data) und der Inhalt +// kann beschaedigt sein. Beides darf die Seite nicht mitreissen. +const readStoredLocation = () => { + try { + const raw = localStorage.getItem(LOCATION_KEY); + if (!raw) return null; + const parsed = JSON.parse(raw); + return (typeof parsed?.lat === 'number' && typeof parsed?.lng === 'number') ? parsed : null; + } catch { + return null; + } +}; + const PublicUserList = ({ users, loading, onRefetch }) => { const { userTypeLabels } = useConfigContext(); - // Load saved location from localStorage on mount - const savedLocation = localStorage.getItem('userLocation'); - const initialCoords = savedLocation ? JSON.parse(savedLocation) : null; - const [showMap, setShowMap] = useState(true); const [showFilters, setShowFilters] = useState(false); - const [locationStatus, setLocationStatus] = useState(initialCoords ? 'granted' : 'idle'); + // Gespeicherten Standort lazy und abgesichert lesen. Vorher stand ein + // ungeschuetztes JSON.parse im Render-Pfad: ein beschaedigter Eintrag - oder ein + // Browser, der Site-Data blockiert - hat die gesamte Liste weiss werden lassen. + const [coords, setCoords] = useState(readStoredLocation); + const [locationStatus, setLocationStatus] = useState(coords ? 'granted' : 'idle'); const [locationError, setLocationError] = useState(''); - const [coords, setCoords] = useState(initialCoords); const [radiusKm, setRadiusKm] = useState(100); const [postalCode, setPostalCode] = useState(''); const [postalSearching, setPostalSearching] = useState(false); @@ -28,8 +42,11 @@ const PublicUserList = ({ users, loading, onRefetch }) => { // Save location to localStorage whenever it changes useEffect(() => { - if (coords) { - localStorage.setItem('userLocation', JSON.stringify(coords)); + if (!coords) return; + try { + localStorage.setItem(LOCATION_KEY, JSON.stringify(coords)); + } catch { + // Speicher nicht verfuegbar - der Standort gilt dann nur fuer diese Sitzung. } }, [coords]); @@ -37,8 +54,15 @@ const PublicUserList = ({ users, loading, onRefetch }) => { setFilters(prev => ({ ...prev, [key]: value })); }, []); - // Re-fetch from backend when type filter changes (server-side filtering) + // Re-fetch from backend when type filter changes (server-side filtering). + // Der erste Lauf wird uebersprungen: useUsers laedt beim Mounten bereits selbst, + // sonst setzt jeder Seitenaufruf zwei identische Anfragen ab. + const skipInitialRefetch = useRef(true); useEffect(() => { + if (skipInitialRefetch.current) { + skipInitialRefetch.current = false; + return; + } if (onRefetch) { onRefetch(filters.type ? { type: filters.type } : {}); }