diff --git a/drohnenfuehrer/backend/controllers/userController.js b/drohnenfuehrer/backend/controllers/userController.js index 42f07c7..9f3679e 100644 --- a/drohnenfuehrer/backend/controllers/userController.js +++ b/drohnenfuehrer/backend/controllers/userController.js @@ -1,5 +1,5 @@ const User = require('../models/User'); -const { geocodeAddress } = require('../utils/geocode'); +const { geocodeAddress, searchAddresses } = require('../utils/geocode'); const logger = require('../utils/logger'); const { escapeCell } = require('../utils/csv'); const config = require('../config/env'); @@ -729,6 +729,32 @@ const getGeocodeByPostalCode = async (req, res) => { } }; +/** + * GET /api/public/geocode/search?q=... + * Freitext-Adresssuche fuer die Autovervollstaendigung im Benutzerformular. + * Laeuft ueber den Server, damit Mindestwartezeit, Cache und der von Nominatim + * geforderte User-Agent greifen und keine Nutzer-IP beim Dienst landet. + */ +const searchAddressSuggestions = async (req, res) => { + const { q } = req.query; + const query = String(q || '').trim(); + + if (query.length < 3) { + return res.status(400).json({ success: false, message: 'Suchbegriff zu kurz (min. 3 Zeichen)' }); + } + if (query.length > 200) { + return res.status(400).json({ success: false, message: 'Suchbegriff zu lang' }); + } + + try { + const results = await searchAddresses(query, req.query.limit); + res.json({ success: true, data: results }); + } catch (error) { + logger.error('Fehler bei der Adresssuche:', error); + res.status(500).json({ success: false, message: 'Fehler bei der Adresssuche' }); + } +}; + module.exports = { getAllUsers, getUserById, @@ -746,5 +772,6 @@ module.exports = { bulkDeleteUsers, uploadUserPhoto, deleteUserPhoto, - getGeocodeByPostalCode + getGeocodeByPostalCode, + searchAddressSuggestions }; diff --git a/drohnenfuehrer/backend/routes/userRoutes.js b/drohnenfuehrer/backend/routes/userRoutes.js index 0697be1..fa2cfeb 100644 --- a/drohnenfuehrer/backend/routes/userRoutes.js +++ b/drohnenfuehrer/backend/routes/userRoutes.js @@ -21,13 +21,15 @@ const { bulkDeleteUsers, uploadUserPhoto, deleteUserPhoto, - getGeocodeByPostalCode + getGeocodeByPostalCode, + searchAddressSuggestions } = require('../controllers/userController'); // Public routes router.get('/public/users', getPublicUsers); // Eigenes, engeres Limit: der Endpunkt loest ausgehende Nominatim-Anfragen aus. router.get('/public/geocode', geocodeLimiter, getGeocodeByPostalCode); +router.get('/public/geocode/search', geocodeLimiter, searchAddressSuggestions); // Protected routes (require authentication) router.get('/users', authenticateToken, getAllUsers); diff --git a/drohnenfuehrer/frontend/public/sw.js b/drohnenfuehrer/frontend/public/sw.js index f267364..2a664d9 100644 --- a/drohnenfuehrer/frontend/public/sw.js +++ b/drohnenfuehrer/frontend/public/sw.js @@ -1,11 +1,13 @@ // Service Worker: nur Offline-Fallback, keine Asset-Caches // Vite erzeugt content-addressierte Hashes, kein manuelles Caching nötig -const CACHE_NAME = 'drohnenfuehrer-offline-v1'; +const CACHE_NAME = 'drohnenfuehrer-offline-v2'; self.addEventListener('install', event => { event.waitUntil( - caches.open(CACHE_NAME).then(cache => cache.add('offline.html')) + caches.open(CACHE_NAME) + .then(cache => cache.add('offline.html')) + .catch(err => console.warn('Offline-Seite konnte nicht gecacht werden:', err)) ); self.skipWaiting(); }); diff --git a/drohnenfuehrer/frontend/src/components/map/MapView.js b/drohnenfuehrer/frontend/src/components/map/MapView.js index 4a18216..3f4a3e0 100644 --- a/drohnenfuehrer/frontend/src/components/map/MapView.js +++ b/drohnenfuehrer/frontend/src/components/map/MapView.js @@ -2,14 +2,20 @@ import React, { useEffect } from 'react'; import { MapContainer, TileLayer, Marker, Popup, Circle, useMap } from 'react-leaflet'; import L from 'leaflet'; import 'leaflet/dist/leaflet.css'; +import markerIcon2x from 'leaflet/dist/images/marker-icon-2x.png'; +import markerIcon from 'leaflet/dist/images/marker-icon.png'; +import markerShadow from 'leaflet/dist/images/marker-shadow.png'; import './MapView.css'; -// Fix für Standard-Marker-Icons in Leaflet +// Standard-Marker-Icons aus dem installierten Leaflet-Paket buendeln. +// Vorher kamen sie von cdnjs — in einer Offline-PWA fuer den Wald waren die +// Marker damit ohne Empfang kaputt, und die IP jedes Besuchers ging an ein +// fremdes CDN. Die Dateien liegen ohnehin in leaflet/dist/images. delete L.Icon.Default.prototype._getIconUrl; L.Icon.Default.mergeOptions({ - iconRetinaUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png', - iconUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png', - shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png', + iconRetinaUrl: markerIcon2x, + iconUrl: markerIcon, + shadowUrl: markerShadow, }); // Komponente zum Aktualisieren der Kartenansicht diff --git a/drohnenfuehrer/frontend/src/components/users/UserForm.js b/drohnenfuehrer/frontend/src/components/users/UserForm.js index 9afb559..040660e 100644 --- a/drohnenfuehrer/frontend/src/components/users/UserForm.js +++ b/drohnenfuehrer/frontend/src/components/users/UserForm.js @@ -1,5 +1,6 @@ import React, { useState, useEffect, useRef } from 'react'; import { useConfigContext } from '../../contexts/ConfigContext'; +import { searchAddresses } from '../../services/users'; import './UserForm.css'; const formatSuggestionAddress = (addr) => { @@ -125,17 +126,13 @@ const UserForm = ({ user, onSave, onCancel }) => { searchTimer.current = setTimeout(async () => { try { - const res = await fetch( - `https://nominatim.openstreetmap.org/search?format=json&countrycodes=de&addressdetails=1&limit=5&q=${encodeURIComponent(value)}`, - { headers: { 'User-Agent': 'drohnenfuehrer-app/1.0 (admin@kasimirat.de)' } } - ); - const data = await res.json(); - setSuggestions(Array.isArray(data) ? data : []); + const result = await searchAddresses(value); + setSuggestions(result.data); setShowSuggestions(true); } catch { setSuggestions([]); } - }, 400); + }, 600); }; const selectSuggestion = (s) => { @@ -233,7 +230,8 @@ const UserForm = ({ user, onSave, onCancel }) => {
✓ GPS automatisch aus Adresse übernommen
) : ( diff --git a/drohnenfuehrer/frontend/src/services/users.js b/drohnenfuehrer/frontend/src/services/users.js index fc5d22a..bdecf68 100644 --- a/drohnenfuehrer/frontend/src/services/users.js +++ b/drohnenfuehrer/frontend/src/services/users.js @@ -214,6 +214,22 @@ export const uploadUserPhoto = async (id, photoDataUrl) => { } }; +// Freitext-Adresssuche ueber das eigene Backend statt direkt bei Nominatim: +// dort greifen Mindestwartezeit, Cache und der vorgeschriebene User-Agent, +// und es wandert keine Nutzer-IP zu einem fremden Dienst. +export const searchAddresses = async (query) => { + try { + const response = await api.get('/public/geocode/search', { params: { q: query } }); + return { success: true, data: response.data.data || [] }; + } catch (error) { + return { + success: false, + data: [], + message: error.response?.data?.message || 'Fehler bei der Adresssuche' + }; + } +}; + // Einmal-Token, mit dem ein Fuehrer sein erstes Passwort setzt. // Laeuft ueber die Admin-Session (Cookie), nicht ueber den Fuehrer-Token. export const generateInviteToken = async (id) => { diff --git a/nachsuche/backend/controllers/userController.js b/nachsuche/backend/controllers/userController.js index 8d203ae..a0b81b2 100644 --- a/nachsuche/backend/controllers/userController.js +++ b/nachsuche/backend/controllers/userController.js @@ -1,5 +1,5 @@ const User = require('../models/User'); -const { geocodeAddress } = require('../utils/geocode'); +const { geocodeAddress, searchAddresses } = require('../utils/geocode'); const logger = require('../utils/logger'); const { escapeCell } = require('../utils/csv'); const config = require('../config/env'); @@ -733,6 +733,32 @@ const getGeocodeByPostalCode = async (req, res) => { } }; +/** + * GET /api/public/geocode/search?q=... + * Freitext-Adresssuche fuer die Autovervollstaendigung im Benutzerformular. + * Laeuft ueber den Server, damit Mindestwartezeit, Cache und der von Nominatim + * geforderte User-Agent greifen und keine Nutzer-IP beim Dienst landet. + */ +const searchAddressSuggestions = async (req, res) => { + const { q } = req.query; + const query = String(q || '').trim(); + + if (query.length < 3) { + return res.status(400).json({ success: false, message: 'Suchbegriff zu kurz (min. 3 Zeichen)' }); + } + if (query.length > 200) { + return res.status(400).json({ success: false, message: 'Suchbegriff zu lang' }); + } + + try { + const results = await searchAddresses(query, req.query.limit); + res.json({ success: true, data: results }); + } catch (error) { + logger.error('Fehler bei der Adresssuche:', error); + res.status(500).json({ success: false, message: 'Fehler bei der Adresssuche' }); + } +}; + module.exports = { getAllUsers, getUserById, @@ -750,5 +776,6 @@ module.exports = { bulkDeleteUsers, uploadUserPhoto, deleteUserPhoto, - getGeocodeByPostalCode + getGeocodeByPostalCode, + searchAddressSuggestions }; diff --git a/nachsuche/backend/routes/userRoutes.js b/nachsuche/backend/routes/userRoutes.js index 0697be1..fa2cfeb 100644 --- a/nachsuche/backend/routes/userRoutes.js +++ b/nachsuche/backend/routes/userRoutes.js @@ -21,13 +21,15 @@ const { bulkDeleteUsers, uploadUserPhoto, deleteUserPhoto, - getGeocodeByPostalCode + getGeocodeByPostalCode, + searchAddressSuggestions } = require('../controllers/userController'); // Public routes router.get('/public/users', getPublicUsers); // Eigenes, engeres Limit: der Endpunkt loest ausgehende Nominatim-Anfragen aus. router.get('/public/geocode', geocodeLimiter, getGeocodeByPostalCode); +router.get('/public/geocode/search', geocodeLimiter, searchAddressSuggestions); // Protected routes (require authentication) router.get('/users', authenticateToken, getAllUsers); diff --git a/nachsuche/backend/utils/geocode.js b/nachsuche/backend/utils/geocode.js index 973cfdf..46d5348 100644 --- a/nachsuche/backend/utils/geocode.js +++ b/nachsuche/backend/utils/geocode.js @@ -161,6 +161,76 @@ const geocodeAddress = async (address) => { } }; -module.exports = { - geocodeAddress +// Nur die Felder, die das Adressformular im Frontend tatsächlich auswertet. +// Alles andere aus der Nominatim-Antwort wird verworfen. +const ADDRESS_PARTS = [ + 'road', 'house_number', 'postcode', + 'city', 'town', 'village', 'municipality', 'hamlet', + 'county', 'state' +]; + +const trimSuggestion = (hit) => { + const address = {}; + for (const key of ADDRESS_PARTS) { + if (hit.address && hit.address[key]) address[key] = hit.address[key]; + } + return { + lat: hit.lat, + lon: hit.lon, + display_name: hit.display_name, + address + }; +}; + +/** + * Freitext-Adresssuche mit mehreren Treffern — für die Autovervollständigung + * im Benutzerformular. + * + * Läuft bewusst über den Server: vorher hat der Browser Nominatim direkt + * angefragt, wodurch weder die Mindestwartezeit noch der vorgeschriebene + * User-Agent griffen und die IP jedes Admins beim Dienst landete. + * + * Teilt sich Wartezeit und Cache mit geocodeAddress; der Cache-Schlüssel ist + * mit einem Präfix versehen, damit die Einträge sich nicht überschneiden. + */ +const searchAddresses = async (query, limit = 5) => { + const normalized = (query || '').trim(); + if (normalized.length < 3) { + return []; + } + + const count = Math.min(Math.max(parseInt(limit, 10) || 5, 1), 10); + const cacheKey = `search:${count}:${normalized.toLowerCase()}`; + if (cache.has(cacheKey)) { + const hit = cache.get(cacheKey); + cache.delete(cacheKey); + cache.set(cacheKey, hit); + return hit || []; + } + + const elapsed = Date.now() - lastRequestTime; + if (elapsed < config.geocodeMinDelayMs) { + await sleep(config.geocodeMinDelayMs - elapsed); + } + + const url = `${config.geocodeUrl}?format=json&addressdetails=1&limit=${count}` + + `&countrycodes=de&q=${encodeURIComponent(normalized)}`; + + try { + const results = await fetchJson(url, { 'User-Agent': config.geocodeUserAgent }); + lastRequestTime = Date.now(); + + const list = Array.isArray(results) ? results.map(trimSuggestion) : []; + rememberInCache(cacheKey, list); + return list; + } catch (error) { + logger.warn('Adresssuche fehlgeschlagen', { query: normalized, error: error.message }); + lastRequestTime = Date.now(); + return []; + } +}; + +module.exports = { + geocodeAddress, + searchAddresses }; diff --git a/nachsuche/frontend/public/sw.js b/nachsuche/frontend/public/sw.js index f1324a4..ed28878 100644 --- a/nachsuche/frontend/public/sw.js +++ b/nachsuche/frontend/public/sw.js @@ -1,11 +1,13 @@ // Service Worker: nur Offline-Fallback, keine Asset-Caches // Vite erzeugt content-addressierte Hashes, kein manuelles Caching nötig -const CACHE_NAME = 'nachsuchenfuehrer-offline-v2'; +const CACHE_NAME = 'nachsuchenfuehrer-offline-v3'; self.addEventListener('install', event => { event.waitUntil( - caches.open(CACHE_NAME).then(cache => cache.add('offline.html')) + caches.open(CACHE_NAME) + .then(cache => cache.add('offline.html')) + .catch(err => console.warn('Offline-Seite konnte nicht gecacht werden:', err)) ); self.skipWaiting(); }); diff --git a/nachsuche/frontend/src/components/map/MapView.js b/nachsuche/frontend/src/components/map/MapView.js index 4a18216..3f4a3e0 100644 --- a/nachsuche/frontend/src/components/map/MapView.js +++ b/nachsuche/frontend/src/components/map/MapView.js @@ -2,14 +2,20 @@ import React, { useEffect } from 'react'; import { MapContainer, TileLayer, Marker, Popup, Circle, useMap } from 'react-leaflet'; import L from 'leaflet'; import 'leaflet/dist/leaflet.css'; +import markerIcon2x from 'leaflet/dist/images/marker-icon-2x.png'; +import markerIcon from 'leaflet/dist/images/marker-icon.png'; +import markerShadow from 'leaflet/dist/images/marker-shadow.png'; import './MapView.css'; -// Fix für Standard-Marker-Icons in Leaflet +// Standard-Marker-Icons aus dem installierten Leaflet-Paket buendeln. +// Vorher kamen sie von cdnjs — in einer Offline-PWA fuer den Wald waren die +// Marker damit ohne Empfang kaputt, und die IP jedes Besuchers ging an ein +// fremdes CDN. Die Dateien liegen ohnehin in leaflet/dist/images. delete L.Icon.Default.prototype._getIconUrl; L.Icon.Default.mergeOptions({ - iconRetinaUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png', - iconUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png', - shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png', + iconRetinaUrl: markerIcon2x, + iconUrl: markerIcon, + shadowUrl: markerShadow, }); // Komponente zum Aktualisieren der Kartenansicht diff --git a/nachsuche/frontend/src/components/users/UserForm.js b/nachsuche/frontend/src/components/users/UserForm.js index f78688a..664ee01 100644 --- a/nachsuche/frontend/src/components/users/UserForm.js +++ b/nachsuche/frontend/src/components/users/UserForm.js @@ -1,5 +1,6 @@ import React, { useState, useEffect, useRef } from 'react'; import { useConfigContext } from '../../contexts/ConfigContext'; +import { searchAddresses } from '../../services/users'; import './UserForm.css'; const formatSuggestionAddress = (addr) => { @@ -125,17 +126,13 @@ const UserForm = ({ user, onSave, onCancel }) => { searchTimer.current = setTimeout(async () => { try { - const res = await fetch( - `https://nominatim.openstreetmap.org/search?format=json&countrycodes=de&addressdetails=1&limit=5&q=${encodeURIComponent(value)}`, - { headers: { 'User-Agent': 'nachsuche-app/1.0 (admin@kasimirat.de)' } } - ); - const data = await res.json(); - setSuggestions(Array.isArray(data) ? data : []); + const result = await searchAddresses(value); + setSuggestions(result.data); setShowSuggestions(true); } catch { setSuggestions([]); } - }, 400); + }, 600); }; const selectSuggestion = (s) => { @@ -233,7 +230,8 @@ const UserForm = ({ user, onSave, onCancel }) => {✓ GPS automatisch aus Adresse übernommen
) : ( diff --git a/nachsuche/frontend/src/services/users.js b/nachsuche/frontend/src/services/users.js index 40ba044..87b0890 100644 --- a/nachsuche/frontend/src/services/users.js +++ b/nachsuche/frontend/src/services/users.js @@ -214,6 +214,22 @@ export const uploadUserPhoto = async (id, photoDataUrl) => { } }; +// Freitext-Adresssuche ueber das eigene Backend statt direkt bei Nominatim: +// dort greifen Mindestwartezeit, Cache und der vorgeschriebene User-Agent, +// und es wandert keine Nutzer-IP zu einem fremden Dienst. +export const searchAddresses = async (query) => { + try { + const response = await api.get('/public/geocode/search', { params: { q: query } }); + return { success: true, data: response.data.data || [] }; + } catch (error) { + return { + success: false, + data: [], + message: error.response?.data?.message || 'Fehler bei der Adresssuche' + }; + } +}; + // Einmal-Token, mit dem ein Fuehrer sein erstes Passwort setzt. // Laeuft ueber die Admin-Session (Cookie), nicht ueber den Fuehrer-Token. export const generateInviteToken = async (id) => { diff --git a/portal/sw.js b/portal/sw.js index 56cb5bb..cc85b81 100644 --- a/portal/sw.js +++ b/portal/sw.js @@ -1,9 +1,11 @@ // Service Worker: Offline-Fallback für Portal-Selektor -const CACHE_NAME = 'portal-offline-v2'; +const CACHE_NAME = 'portal-offline-v3'; self.addEventListener('install', event => { event.waitUntil( - caches.open(CACHE_NAME).then(cache => cache.add('offline.html')) + caches.open(CACHE_NAME) + .then(cache => cache.add('offline.html')) + .catch(err => console.warn('Offline-Seite konnte nicht gecacht werden:', err)) ); self.skipWaiting(); }); diff --git a/stoeberhunde/backend/controllers/userController.js b/stoeberhunde/backend/controllers/userController.js index e661ca6..73e91e3 100644 --- a/stoeberhunde/backend/controllers/userController.js +++ b/stoeberhunde/backend/controllers/userController.js @@ -1,5 +1,5 @@ const User = require('../models/User'); -const { geocodeAddress } = require('../utils/geocode'); +const { geocodeAddress, searchAddresses } = require('../utils/geocode'); const logger = require('../utils/logger'); const { escapeCell } = require('../utils/csv'); const config = require('../config/env'); @@ -729,6 +729,32 @@ const getGeocodeByPostalCode = async (req, res) => { } }; +/** + * GET /api/public/geocode/search?q=... + * Freitext-Adresssuche fuer die Autovervollstaendigung im Benutzerformular. + * Laeuft ueber den Server, damit Mindestwartezeit, Cache und der von Nominatim + * geforderte User-Agent greifen und keine Nutzer-IP beim Dienst landet. + */ +const searchAddressSuggestions = async (req, res) => { + const { q } = req.query; + const query = String(q || '').trim(); + + if (query.length < 3) { + return res.status(400).json({ success: false, message: 'Suchbegriff zu kurz (min. 3 Zeichen)' }); + } + if (query.length > 200) { + return res.status(400).json({ success: false, message: 'Suchbegriff zu lang' }); + } + + try { + const results = await searchAddresses(query, req.query.limit); + res.json({ success: true, data: results }); + } catch (error) { + logger.error('Fehler bei der Adresssuche:', error); + res.status(500).json({ success: false, message: 'Fehler bei der Adresssuche' }); + } +}; + module.exports = { getAllUsers, getUserById, @@ -746,5 +772,6 @@ module.exports = { bulkDeleteUsers, uploadUserPhoto, deleteUserPhoto, - getGeocodeByPostalCode + getGeocodeByPostalCode, + searchAddressSuggestions }; diff --git a/stoeberhunde/backend/routes/userRoutes.js b/stoeberhunde/backend/routes/userRoutes.js index 0697be1..fa2cfeb 100644 --- a/stoeberhunde/backend/routes/userRoutes.js +++ b/stoeberhunde/backend/routes/userRoutes.js @@ -21,13 +21,15 @@ const { bulkDeleteUsers, uploadUserPhoto, deleteUserPhoto, - getGeocodeByPostalCode + getGeocodeByPostalCode, + searchAddressSuggestions } = require('../controllers/userController'); // Public routes router.get('/public/users', getPublicUsers); // Eigenes, engeres Limit: der Endpunkt loest ausgehende Nominatim-Anfragen aus. router.get('/public/geocode', geocodeLimiter, getGeocodeByPostalCode); +router.get('/public/geocode/search', geocodeLimiter, searchAddressSuggestions); // Protected routes (require authentication) router.get('/users', authenticateToken, getAllUsers); diff --git a/stoeberhunde/frontend/public/sw.js b/stoeberhunde/frontend/public/sw.js index 71ccc59..3b71ec8 100644 --- a/stoeberhunde/frontend/public/sw.js +++ b/stoeberhunde/frontend/public/sw.js @@ -1,11 +1,13 @@ // Service Worker: nur Offline-Fallback, keine Asset-Caches // Vite erzeugt content-addressierte Hashes, kein manuelles Caching nötig -const CACHE_NAME = 'stoeberhunde-offline-v1'; +const CACHE_NAME = 'stoeberhunde-offline-v2'; self.addEventListener('install', event => { event.waitUntil( - caches.open(CACHE_NAME).then(cache => cache.add('offline.html')) + caches.open(CACHE_NAME) + .then(cache => cache.add('offline.html')) + .catch(err => console.warn('Offline-Seite konnte nicht gecacht werden:', err)) ); self.skipWaiting(); }); diff --git a/stoeberhunde/frontend/src/components/map/MapView.js b/stoeberhunde/frontend/src/components/map/MapView.js index 4a18216..3f4a3e0 100644 --- a/stoeberhunde/frontend/src/components/map/MapView.js +++ b/stoeberhunde/frontend/src/components/map/MapView.js @@ -2,14 +2,20 @@ import React, { useEffect } from 'react'; import { MapContainer, TileLayer, Marker, Popup, Circle, useMap } from 'react-leaflet'; import L from 'leaflet'; import 'leaflet/dist/leaflet.css'; +import markerIcon2x from 'leaflet/dist/images/marker-icon-2x.png'; +import markerIcon from 'leaflet/dist/images/marker-icon.png'; +import markerShadow from 'leaflet/dist/images/marker-shadow.png'; import './MapView.css'; -// Fix für Standard-Marker-Icons in Leaflet +// Standard-Marker-Icons aus dem installierten Leaflet-Paket buendeln. +// Vorher kamen sie von cdnjs — in einer Offline-PWA fuer den Wald waren die +// Marker damit ohne Empfang kaputt, und die IP jedes Besuchers ging an ein +// fremdes CDN. Die Dateien liegen ohnehin in leaflet/dist/images. delete L.Icon.Default.prototype._getIconUrl; L.Icon.Default.mergeOptions({ - iconRetinaUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png', - iconUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png', - shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png', + iconRetinaUrl: markerIcon2x, + iconUrl: markerIcon, + shadowUrl: markerShadow, }); // Komponente zum Aktualisieren der Kartenansicht diff --git a/stoeberhunde/frontend/src/components/users/UserForm.js b/stoeberhunde/frontend/src/components/users/UserForm.js index 351bea8..67f577c 100644 --- a/stoeberhunde/frontend/src/components/users/UserForm.js +++ b/stoeberhunde/frontend/src/components/users/UserForm.js @@ -1,5 +1,6 @@ import React, { useState, useEffect, useRef } from 'react'; import { useConfigContext } from '../../contexts/ConfigContext'; +import { searchAddresses } from '../../services/users'; import './UserForm.css'; const formatSuggestionAddress = (addr) => { @@ -125,17 +126,13 @@ const UserForm = ({ user, onSave, onCancel }) => { searchTimer.current = setTimeout(async () => { try { - const res = await fetch( - `https://nominatim.openstreetmap.org/search?format=json&countrycodes=de&addressdetails=1&limit=5&q=${encodeURIComponent(value)}`, - { headers: { 'User-Agent': 'stoeberhunde-app/1.0 (admin@kasimirat.de)' } } - ); - const data = await res.json(); - setSuggestions(Array.isArray(data) ? data : []); + const result = await searchAddresses(value); + setSuggestions(result.data); setShowSuggestions(true); } catch { setSuggestions([]); } - }, 400); + }, 600); }; const selectSuggestion = (s) => { @@ -233,7 +230,8 @@ const UserForm = ({ user, onSave, onCancel }) => {✓ GPS automatisch aus Adresse übernommen
) : ( diff --git a/stoeberhunde/frontend/src/services/users.js b/stoeberhunde/frontend/src/services/users.js index fcf20df..ec56bca 100644 --- a/stoeberhunde/frontend/src/services/users.js +++ b/stoeberhunde/frontend/src/services/users.js @@ -214,6 +214,22 @@ export const uploadUserPhoto = async (id, photoDataUrl) => { } }; +// Freitext-Adresssuche ueber das eigene Backend statt direkt bei Nominatim: +// dort greifen Mindestwartezeit, Cache und der vorgeschriebene User-Agent, +// und es wandert keine Nutzer-IP zu einem fremden Dienst. +export const searchAddresses = async (query) => { + try { + const response = await api.get('/public/geocode/search', { params: { q: query } }); + return { success: true, data: response.data.data || [] }; + } catch (error) { + return { + success: false, + data: [], + message: error.response?.data?.message || 'Fehler bei der Adresssuche' + }; + } +}; + // Einmal-Token, mit dem ein Fuehrer sein erstes Passwort setzt. // Laeuft ueber die Admin-Session (Cookie), nicht ueber den Fuehrer-Token. export const generateInviteToken = async (id) => {