OSM-Verknüpfung (Modell A): OAuth2-Fundament für Nutzer-Beiträge

- Tabelle user_osm (access_token verschlüsselt at rest via Fernet,
  Schlüssel aus JWT_SECRET abgeleitet oder OSM_TOKEN_KEY).
- Router /api/osm-auth: authorize (signierter state mit user_id+CSRF),
  callback (Code-Tausch + OSM-Name holen + speichern), status, unlink.
- Profil-UI (Settings): "OSM-Konto verknüpfen" / verknüpft-als / trennen,
  hundehalter-spezifische Motivation.
- cryptography in requirements.
- Basis für dog=yes-Beiträge + Gamification/Pro (folgt). Staging-Branch.

ENV nötig: OSM_CLIENT_ID, OSM_CLIENT_SECRET (Redirect-URI default staging).
This commit is contained in:
rene 2026-06-03 21:14:36 +02:00
parent 4bc7454258
commit 46caa05020
5 changed files with 237 additions and 0 deletions

View file

@ -672,6 +672,13 @@ window.Page_settings = (() => {
</div>
</div>
<div class="card mb-4">
<div class="by-card-section-header">OpenStreetMap die Karte mitverbessern</div>
<div id="settings-osm-body" class="p-4">
<div class="text-sm-muted">Lädt</div>
</div>
</div>
<div class="card mb-4">
<div class="card-body" style="padding:0">
<div class="sidebar-item" data-page="dog-profile"
@ -925,6 +932,54 @@ window.Page_settings = (() => {
});
}).catch(() => {});
// OSM-Account-Verknüpfung (Modell A) — Status laden + Buttons verdrahten
(function _osmLink() {
const el = document.getElementById('settings-osm-body');
if (!el) return;
API.get('/osm-auth/status').then(st => {
if (st.linked) {
el.innerHTML = `
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">
<svg class="ph-icon" style="color:var(--c-success)" aria-hidden="true"><use href="/icons/phosphor.svg#check-circle"></use></svg>
<span style="font-size:var(--text-sm)">Verknüpft als <strong>${UI.escape(st.osm_name)}</strong></span>
</div>
<button id="settings-osm-unlink"
style="margin-top:var(--space-3);background:none;border:none;
color:var(--c-text-muted);font-size:var(--text-xs);cursor:pointer">
Verknüpfung trennen
</button>`;
el.querySelector('#settings-osm-unlink').addEventListener('click', async () => {
try { await API.post('/osm-auth/unlink', {}); } catch (e) {}
_osmLink();
});
} else {
el.innerHTML = `
<p class="text-sm-muted" style="margin:0 0 var(--space-3);line-height:1.45">
Du kennst die hundefreundlichen Orte besser als jede Karte. Verknüpfe deinen
kostenlosen OpenStreetMap-Account und trag mit einem Tap ein, wo dein Hund
willkommen war das hilft jedem Hundehalter nach dir. Kostenlos, gemeinnützig,
keine Werbung.
</p>
<button id="settings-osm-link"
style="display:flex;align-items:center;justify-content:center;gap:var(--space-2);
padding:var(--space-3) var(--space-4);border-radius:var(--radius-md);
border:none;background:var(--c-primary);color:#fff;
font-size:var(--text-sm);font-weight:600;cursor:pointer">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#map-trifold"></use></svg>
OSM-Konto verknüpfen
</button>`;
el.querySelector('#settings-osm-link').addEventListener('click', async () => {
try {
const r = await API.get('/osm-auth/authorize');
if (r.authorize_url) window.location.href = r.authorize_url;
} catch (e) {
UI.toast?.('OSM-Anbindung noch nicht konfiguriert.');
}
});
}
}).catch(() => { el.innerHTML = '<div class="text-sm-muted">OSM-Status nicht verfügbar.</div>'; });
})();
// Achievements laden (Streak + Stats + Badges)
API.get('/achievements/me').then(a => {
const statsEl = document.getElementById('settings-stats-body');