From db9b02449d5546ff90b37ab439783d0c6eb353d0 Mon Sep 17 00:00:00 2001 From: Lars Gebhardt-Kusche Date: Fri, 31 Jul 2026 20:40:25 +0200 Subject: [PATCH] csad --- .projektstructure.txt | 1 + PROJECT_CONTEXT.md | 1 + README.md | 1 + partials/landing/account/dashboard.php | 113 +++++++++++++++++++++++-- public/page/datenschutz.php | 7 +- schema.sql | 1 + src/App/AccountPages.php | 14 ++- src/App/ProfileSettings.php | 6 ++ 8 files changed, 136 insertions(+), 8 deletions(-) diff --git a/.projektstructure.txt b/.projektstructure.txt index 9dadc1c..0da9504 100755 --- a/.projektstructure.txt +++ b/.projektstructure.txt @@ -16,6 +16,7 @@ Aktueller Projektstand - Kinder koennen im Mitgliederbereich zusaetzlich nachtraeglich bearbeitet und geloescht werden. - Wenn bei einem Kind ein Geburtsdatum hinterlegt ist, wird das Alter automatisch daraus berechnet und spaeter bei Bedarf jaehrlich aktualisiert; ohne Geburtsdatum bleibt das manuell gepflegte Alter massgeblich. - Profilbezogene sensible Angaben wie Vorname, Nachname, Telefonnummer, Beruf, Sprachen und Kurzvorstellung werden app-seitig verschluesselt gespeichert; vorhandene Klartextwerte werden bei der Nutzung schrittweise nachmigriert. +- Das Profil besitzt jetzt zusaetzlich eine vollstaendige Adresse mit verschluesselter Strasse/Hausnummer sowie eine optionale Adress-Lokalisierung ueber die vorhandenen externen Karten-/Geocoding-Dienste. - Alte statische Avatar-Presets und frühere Layer-Assets sind entfernt. - Änderungen an Funktionen mit Cookies, LocalStorage, SessionStorage, Geolocation, Tracking oder Drittanbietern müssen immer auch in Datenschutz-/Cookie-Hinweisen und im Consent-Flow berücksichtigt werden. diff --git a/PROJECT_CONTEXT.md b/PROJECT_CONTEXT.md index 15874ef..53115f1 100644 --- a/PROJECT_CONTEXT.md +++ b/PROJECT_CONTEXT.md @@ -36,6 +36,7 @@ Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter. Kernbereiche sind l - Kinder koennen im Mitgliederbereich angelegt, nachtraeglich bearbeitet und wieder geloescht werden. - Wenn ein Geburtsdatum gesetzt ist, wird das Alter im Mitgliederbereich automatisch berechnet und spaeter bei Bedarf anhand des Datums aktualisiert; ohne Geburtsdatum gilt der manuelle Alterswert. - Sensible Profildaten wie Vorname, Nachname, Telefonnummer, Beruf, Sprachen und Kurzvorstellung werden app-seitig verschluesselt gespeichert; bestehende Klartextwerte werden bei Nutzung schrittweise in den verschluesselten Zustand ueberfuehrt. +- Im Profil koennen jetzt auch Strasse/Hausnummer, PLZ, Ort und Region gepflegt werden; fuer die Adress-Lokalisierung werden die bereits vorhandenen externen Karten-/Geocoding-Dienste wiederverwendet. - Im Profil kann der Nutzer aktuell Komponenten wie Augen, Augenbrauen, Mund, Brille, Haare, Bart und Ohrringe direkt per Vorschaubild auswählen. - Der Button `Random Profilbild` setzt Seed und Varianten des aktiven Styles zufaellig neu und aktualisiert die Vorschau sofort. - Beim Oeffnen des Avatar-Generators wird nur der aktive Style-/Komponenten-Tab geladen; weitere Varianten werden erst beim Wechsel nachgeladen. diff --git a/README.md b/README.md index fb552ce..33b844c 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter mit Fokus auf lokale - im Bereich `Kinder` koennen vorhandene Eintraege jetzt auch nachtraeglich bearbeitet und geloescht werden - bei Kindern mit Geburtsdatum wird das Alter automatisch berechnet und bei Aufruf des Mitgliederbereichs bei Bedarf jaehrlich nachgezogen; ohne Geburtsdatum gilt das manuell gepflegte Alter - sensible Profildaten werden im Mitgliederbereich jetzt konsequent app-seitig verschluesselt gespeichert; bestehende Klartextwerte werden beim Lesen schrittweise in den verschluesselten Zustand ueberfuehrt +- das Profil unterstuetzt jetzt eine vollstaendige Adresse mit verschluesselter Strasse/Hausnummer sowie optionaler Adress-Lokalisierung ueber die bereits vorhandenen Karten-/Geocoding-Dienste - Debug-Floating-Button nur für `site_admin` im Debug-Modus ## Technik diff --git a/partials/landing/account/dashboard.php b/partials/landing/account/dashboard.php index f001c9f..84f04df 100755 --- a/partials/landing/account/dashboard.php +++ b/partials/landing/account/dashboard.php @@ -81,7 +81,7 @@ $sectionLinks = [
Name
Anzeigename
-
Ort
+
Adresse
E-Mail
Telefon
Beruf
@@ -486,6 +486,9 @@ $sectionLinks = [
+ + +
@@ -498,12 +501,12 @@ $sectionLinks = [
- - + +
- - + +
@@ -511,11 +514,25 @@ $sectionLinks = [
+
+ + +
+
+
+
+ + +
+
+ +

Verwendet dieselben Karten-/Adressdienste wie bei Events und nur nach Einwilligung in externe Dienste.

+
@@ -728,6 +745,14 @@ $sectionLinks = [ const zipInput = document.getElementById('evZip'); const cityInput = document.getElementById('evCity'); const regionInput = document.getElementById('evRegion'); + const profileLocateButton = document.getElementById('btnProfileLocate'); + const profileStreetInput = document.getElementById('pStreet'); + const profileZipInput = document.getElementById('pZip'); + const profileCityInput = document.getElementById('pCity'); + const profileRegionInput = document.getElementById('pRegion'); + const profileRegionViewInput = document.getElementById('pRegionView'); + const profileLatInput = document.getElementById('pLat'); + const profileLngInput = document.getElementById('pLng'); let map, marker; function ensureLeaflet(callback) { @@ -818,6 +843,28 @@ $sectionLinks = [ .catch(() => {}); } + function geocodeAddressData(query, onSuccess) { + if (!window.PKTConsent || !window.PKTConsent.has('external_services')) { + alert('Für Karten und Adresssuche bitte zuerst die externen Dienste in den Cookie-Einstellungen erlauben.'); + window.PKTConsent?.openPreferences?.(); + return; + } + fetch('https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&addressdetails=1&q=' + encodeURIComponent(query), { + headers: { 'Accept-Language': 'de', 'User-Agent': 'papa-kind-treff/1.0' }, + }) + .then(r => r.json()) + .then(data => { + if (Array.isArray(data) && data[0]) { + onSuccess?.(data[0]); + return; + } + alert('Adresse konnte nicht eindeutig lokalisiert werden.'); + }) + .catch(() => { + alert('Adresssuche aktuell nicht erreichbar.'); + }); + } + function initMap() { if (map) { map.invalidateSize(); return; } map = L.map(mapContainer).setView([51.1657, 10.4515], 6); @@ -863,6 +910,62 @@ $sectionLinks = [ geocodeAndPlace(parts.join(', ')); }); + (function(){ + const syncProfileRegionView = () => { + if (profileRegionViewInput && profileRegionInput) { + profileRegionInput.value = profileRegionViewInput.value.trim(); + } + }; + + const clearProfileCoordinates = () => { + if (profileLatInput) profileLatInput.value = ''; + if (profileLngInput) profileLngInput.value = ''; + }; + + [profileStreetInput, profileZipInput, profileCityInput].forEach((input) => { + input?.addEventListener('input', clearProfileCoordinates); + }); + + profileRegionViewInput?.addEventListener('input', () => { + syncProfileRegionView(); + clearProfileCoordinates(); + }); + + profileLocateButton?.addEventListener('click', () => { + const parts = [ + profileStreetInput?.value || '', + profileZipInput?.value || '', + profileCityInput?.value || '', + profileRegionViewInput?.value || '', + ].map(v => v.trim()).filter(Boolean); + + if (!parts.length) { + alert('Bitte zuerst mindestens einen Teil der Adresse eingeben.'); + return; + } + + geocodeAddressData(parts.join(', '), (result) => { + const address = result.address || {}; + const street = [address.road || '', address.house_number || ''].filter(Boolean).join(' ').trim(); + const region = address.suburb || address.state || address.county || ''; + const city = address.city || address.town || address.village || ''; + + if (profileStreetInput && street) profileStreetInput.value = street; + if (profileZipInput && address.postcode) profileZipInput.value = address.postcode; + if (profileCityInput && city) profileCityInput.value = city; + if (profileRegionViewInput) profileRegionViewInput.value = region; + if (profileRegionInput) profileRegionInput.value = region; + + const lat = parseFloat(result.lat); + const lng = parseFloat(result.lon); + if (profileLatInput) profileLatInput.value = Number.isNaN(lat) ? '' : lat.toFixed(7); + if (profileLngInput) profileLngInput.value = Number.isNaN(lng) ? '' : lng.toFixed(7); + }); + }); + + syncProfileRegionView(); + })(); + (function(){ const modal = document.getElementById('modalEvent'); diff --git a/public/page/datenschutz.php b/public/page/datenschutz.php index becb739..d902912 100644 --- a/public/page/datenschutz.php +++ b/public/page/datenschutz.php @@ -48,7 +48,7 @@ $clientCookie = $config->cookiePrefix() . 'client';

Wenn du ein Konto anlegst oder den Mitgliederbereich nutzt, verarbeiten wir die dafür notwendigen Daten, insbesondere E-Mail-Adresse, Passwort-Hash, Verifikationsstatus sowie die von dir gepflegten Profilangaben. - Dazu können Anzeigename, Name, Ort, Sprachen, Kurzbeschreibung, optionale Kinderangaben, + Dazu können Anzeigename, Name, Adresse, Ort, Sprachen, Kurzbeschreibung, optionale Kinderangaben, eigene Events, Event-Teilnahmen und Community-Inhalte gehören.

@@ -137,6 +137,11 @@ $clientCookie = $config->cookiePrefix() . 'client'; Je nach Auswahl wird dieser nur für den aktuellen Besuch oder darüber hinaus lokal gespeichert, damit dir passende Treffen in deiner Nähe angezeigt werden können.

+

+ Zusätzlich kannst du im Mitgliederbereich deine Profiladresse über die gleichen externen Adressdienste + lokalisieren lassen, die auch für Event-Adressen genutzt werden. Dabei werden die von dir eingegebenen + Adressbestandteile an den jeweiligen Geocoding-Dienst übermittelt. +

Für Karten- und Adressfunktionen werden derzeit externe Dienste genutzt:

diff --git a/schema.sql b/schema.sql index feb4767..c6576b7 100755 --- a/schema.sql +++ b/schema.sql @@ -22,6 +22,7 @@ CREATE TABLE user_profiles ( display_name VARCHAR(120) NOT NULL, first_name VARBINARY(512) NULL, last_name VARBINARY(512) NULL, + street VARBINARY(512) NULL, share_level ENUM('basic','papa','papa_contact') NOT NULL DEFAULT 'basic', children_visibility ENUM('hidden','age_only','details') NOT NULL DEFAULT 'hidden', zip CHAR(5) NULL, diff --git a/src/App/AccountPages.php b/src/App/AccountPages.php index 56c32e6..d2d2c75 100755 --- a/src/App/AccountPages.php +++ b/src/App/AccountPages.php @@ -11,6 +11,7 @@ final class AccountPages private const ENCRYPTED_PROFILE_FIELDS = [ 'first_name', 'last_name', + 'street', 'contact_phone', 'profession', 'languages', @@ -185,6 +186,7 @@ final class AccountPages } $firstNameEnc = self::encryptOptionalProfileField($crypto, trim((string)$_POST['first_name'])); $lastNameEnc = self::encryptOptionalProfileField($crypto, trim((string)$_POST['last_name'])); + $streetEnc = self::encryptOptionalProfileField($crypto, trim((string)($_POST['street'] ?? ''))); $phoneEnc = self::encryptOptionalProfileField($crypto, trim((string)$_POST['contact_phone'])); $professionEnc = self::encryptOptionalProfileField($crypto, trim((string)$_POST['profession'])); $languagesEnc = self::encryptOptionalProfileField($crypto, trim((string)$languages)); @@ -193,13 +195,17 @@ final class AccountPages if ($profileSettings) { $profileSettings->ensureSchema(); } - $stmt = $pdo?->prepare('UPDATE user_profiles SET display_name=:name, first_name=:fname, last_name=:lname, zip=:zip, city=:city, profession=:prof, languages=:langs, about=:about, contact_phone=:phone, location_tracking_preference=:locationPref, updated_at=NOW() WHERE user_id=:id'); + $stmt = $pdo?->prepare('UPDATE user_profiles SET display_name=:name, first_name=:fname, last_name=:lname, street=:street, zip=:zip, city=:city, region=:region, lat=:lat, lng=:lng, profession=:prof, languages=:langs, about=:about, contact_phone=:phone, location_tracking_preference=:locationPref, updated_at=NOW() WHERE user_id=:id'); $stmt?->execute([ 'name' => trim((string)$_POST['display_name']), 'fname' => $firstNameEnc, 'lname' => $lastNameEnc, + 'street' => $streetEnc, 'zip' => trim((string)$_POST['zip']), 'city' => trim((string)$_POST['city']), + 'region' => trim((string)($_POST['region'] ?? '')), + 'lat' => isset($_POST['lat']) && $_POST['lat'] !== '' ? (float)$_POST['lat'] : null, + 'lng' => isset($_POST['lng']) && $_POST['lng'] !== '' ? (float)$_POST['lng'] : null, 'prof' => $professionEnc, 'langs' => $languagesEnc, 'about' => $aboutEnc, @@ -402,6 +408,10 @@ final class AccountPages 'last_name' => '', 'zip' => '', 'city' => '', + 'region' => '', + 'lat' => null, + 'lng' => null, + 'street' => '', 'profession' => '', 'languages' => '', 'about' => '', @@ -427,7 +437,7 @@ final class AccountPages static fn(string $column): string => 'p.' . $column, AvatarManager::allProfileColumns() )); - $stmt = $pdo?->prepare("SELECT u.email, u.status, p.display_name, p.first_name, p.last_name, p.zip, p.city, p.profession, p.languages, p.about, p.contact_phone, p.location_tracking_preference, $avatarColumns FROM users u LEFT JOIN user_profiles p ON p.user_id = u.id WHERE u.id = :id LIMIT 1"); + $stmt = $pdo?->prepare("SELECT u.email, u.status, p.display_name, p.first_name, p.last_name, p.street, p.zip, p.city, p.region, p.lat, p.lng, p.profession, p.languages, p.about, p.contact_phone, p.location_tracking_preference, $avatarColumns FROM users u LEFT JOIN user_profiles p ON p.user_id = u.id WHERE u.id = :id LIMIT 1"); $stmt?->execute(['id' => $userId]); $row = $stmt?->fetch(\PDO::FETCH_ASSOC); if ($row) { diff --git a/src/App/ProfileSettings.php b/src/App/ProfileSettings.php index cfb78e5..3645416 100644 --- a/src/App/ProfileSettings.php +++ b/src/App/ProfileSettings.php @@ -61,6 +61,7 @@ final class ProfileSettings $columnDefinitions = [ 'first_name' => 'VARBINARY(512) NULL', 'last_name' => 'VARBINARY(512) NULL', + 'street' => 'VARBINARY(512) NULL', 'contact_phone' => 'VARBINARY(512) NULL', 'contact_email' => 'VARBINARY(512) NULL', 'profession' => 'VARBINARY(512) NULL', @@ -70,6 +71,11 @@ final class ProfileSettings foreach ($columnDefinitions as $column => $definition) { if (!$this->hasColumn('user_profiles', $column)) { + if ($column === 'street') { + $this->pdo->exec('ALTER TABLE user_profiles ADD COLUMN street VARBINARY(512) NULL AFTER last_name'); + $this->columnCache['user_profiles.street'] = true; + $this->columnTypeCache['user_profiles.street'] = 'varbinary'; + } continue; }