fix(frontend): Reset-Link auswerten, Absturz bei defektem localStorage verhindern
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
868699a1bc
commit
f4939ef6c5
|
|
@ -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() {
|
|||
</button>
|
||||
|
||||
<div className="form-footer">
|
||||
<a href="/">Zurück zum Login</a>
|
||||
<a href={APP_HOME}>Zurück zum Login</a>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -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 } : {});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
</button>
|
||||
|
||||
<div className="form-footer">
|
||||
<a href="/">Zurück zum Login</a>
|
||||
<a href={APP_HOME}>Zurück zum Login</a>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -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 } : {});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
</button>
|
||||
|
||||
<div className="form-footer">
|
||||
<a href="/">Zurück zum Login</a>
|
||||
<a href={APP_HOME}>Zurück zum Login</a>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -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 } : {});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue