/* ============================================================ BAN YARO — API Client Zentraler Eingang für ALLE Backend-Kommunikation. Kein fetch() wird außerhalb dieser Datei aufgerufen. ============================================================ */ const API = (() => { // ---------------------------------------------------------- // Interner HTTP-Kern // ---------------------------------------------------------- async function _request(method, path, body = null, options = {}) { const config = { method, headers: { 'Content-Type': 'application/json' }, credentials: 'include', // HttpOnly Cookie wird automatisch mitgesendet }; if (body && !(body instanceof FormData)) { config.body = JSON.stringify(body); } else if (body instanceof FormData) { delete config.headers['Content-Type']; // Browser setzt multipart/form-data config.body = body; } // JWT aus localStorage als Bearer (für API-Calls die das brauchen) const token = localStorage.getItem('by_token'); if (token) { config.headers['Authorization'] = `Bearer ${token}`; } let response; try { response = await fetch(`/api${path}`, config); } catch (err) { throw new APIError('Keine Verbindung zum Server.', 0, 'network'); } // 204 No Content if (response.status === 204) return null; let data; try { data = await response.json(); } catch { data = null; } if (!response.ok) { const message = data?.detail || data?.message || `Fehler ${response.status}`; throw new APIError(message, response.status, data?.code); } return data; } // ---------------------------------------------------------- // Öffentliche HTTP-Methoden // ---------------------------------------------------------- const get = (path) => _request('GET', path); const post = (path, body) => _request('POST', path, body); const put = (path, body) => _request('PUT', path, body); const patch = (path, body) => _request('PATCH', path, body); const del = (path) => _request('DELETE', path); const upload = (path, form) => _request('POST', path, form); // FormData // ---------------------------------------------------------- // AUTH // ---------------------------------------------------------- const auth = { login(email, password) { return post('/auth/login', { email, password }); }, register(email, password, name) { return post('/auth/register', { email, password, name }); }, logout() { localStorage.removeItem('by_token'); return post('/auth/logout'); }, me() { return get('/auth/me'); }, }; // ---------------------------------------------------------- // HUNDE-PROFIL // ---------------------------------------------------------- const dogs = { list() { return get('/dogs'); }, get(id) { return get(`/dogs/${id}`); }, create(data) { return post('/dogs', data); }, update(id, data) { return patch(`/dogs/${id}`, data); }, delete(id) { return del(`/dogs/${id}`); }, uploadPhoto(id, formData) { return upload(`/dogs/${id}/photo`, formData); }, }; // ---------------------------------------------------------- // TAGEBUCH // ---------------------------------------------------------- const diary = { list(dogId, params = {}) { const q = new URLSearchParams(params).toString(); return get(`/dogs/${dogId}/diary${q ? '?' + q : ''}`); }, get(dogId, entryId) { return get(`/dogs/${dogId}/diary/${entryId}`); }, create(dogId, data) { return post(`/dogs/${dogId}/diary`, data); }, update(dogId, id, data){ return patch(`/dogs/${dogId}/diary/${id}`, data); }, delete(dogId, id) { return del(`/dogs/${dogId}/diary/${id}`); }, uploadMedia(dogId, id, formData) { return upload(`/dogs/${dogId}/diary/${id}/media`, formData); }, }; // ---------------------------------------------------------- // GESUNDHEIT // ---------------------------------------------------------- const health = { list(dogId, typ = null) { const q = typ ? `?typ=${encodeURIComponent(typ)}` : ''; return get(`/dogs/${dogId}/health${q}`); }, create(dogId, data) { return post(`/dogs/${dogId}/health`, data); }, update(dogId, id, d) { return patch(`/dogs/${dogId}/health/${id}`, d); }, delete(dogId, id) { return del(`/dogs/${dogId}/health/${id}`); }, uploadDokument(dogId, id, formData) { return upload(`/dogs/${dogId}/health/${id}/dokument`, formData); }, kiZusammenfassung(dogId) { return post(`/dogs/${dogId}/health/ki-zusammenfassung`); }, symptomCheck(dogId, symptoms) { return post(`/dogs/${dogId}/health/symptom-check`, { symptoms }); }, }; // ---------------------------------------------------------- // GIFTKÖDER-ALARM // ---------------------------------------------------------- const poison = { listNearby(lat, lon, radius = 5000) { return get(`/poison?lat=${lat}&lon=${lon}&radius=${radius}`); }, report(data) { return post('/poison', data); }, confirm(id) { return post(`/poison/${id}/confirm`); }, resolve(id, data={}) { return post(`/poison/${id}/resolve`, data); }, uploadPhoto(id, form){ return upload(`/poison/${id}/photo`, form); }, }; // ---------------------------------------------------------- // KARTE & ORTE // ---------------------------------------------------------- const places = { listNearby(lat, lon, type = null, radius = 3000) { const params = new URLSearchParams({ lat, lon, radius }); if (type) params.set('type', type); return get(`/places?${params}`); }, get(id) { return get(`/places/${id}`); }, create(data) { return post('/places', data); }, rate(id, rating, comment) { return post(`/places/${id}/ratings`, { rating, comment }); }, }; // ---------------------------------------------------------- // GASSI-ROUTEN // ---------------------------------------------------------- const routes = { listNearby(lat, lon, radius = 10000) { return get(`/routes?lat=${lat}&lon=${lon}&radius=${radius}`); }, get(id) { return get(`/routes/${id}`); }, create(data) { return post('/routes', data); }, rate(id, d) { return post(`/routes/${id}/ratings`, d); }, }; // ---------------------------------------------------------- // WETTER // ---------------------------------------------------------- const weather = { alerts(lat, lon) { return get(`/weather/alerts?lat=${lat}&lon=${lon}`); }, }; // ---------------------------------------------------------- // PUSH NOTIFICATIONS // ---------------------------------------------------------- const push = { getVapidKey() { return get('/push/vapid-key'); }, subscribe(subscription) { return post('/push/subscribe', subscription); }, unsubscribe() { return del('/push/subscribe'); }, }; // ---------------------------------------------------------- // PUSH-SUBSCRIPTION HELPER // ---------------------------------------------------------- async function subscribeToPush() { if (!('serviceWorker' in navigator) || !('PushManager' in window)) return null; const permission = await Notification.requestPermission(); if (permission !== 'granted') return null; const { vapid_public_key } = await push.getVapidKey(); const reg = await navigator.serviceWorker.ready; const subscription = await reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: _urlBase64ToUint8Array(vapid_public_key), }); await push.subscribe(subscription.toJSON()); return subscription; } function _urlBase64ToUint8Array(base64String) { const padding = '='.repeat((4 - base64String.length % 4) % 4); const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/'); const raw = atob(base64); return new Uint8Array([...raw].map(c => c.charCodeAt(0))); } // ---------------------------------------------------------- // GEOLOCATION HELPER // ---------------------------------------------------------- function getLocation(options = {}) { return new Promise((resolve, reject) => { if (!navigator.geolocation) { reject(new Error('Geolocation wird nicht unterstützt.')); return; } navigator.geolocation.getCurrentPosition( pos => resolve({ lat: pos.coords.latitude, lon: pos.coords.longitude }), err => reject(err), { timeout: 8000, maximumAge: 60000, ...options } ); }); } // ---------------------------------------------------------- // ERROR-KLASSE // ---------------------------------------------------------- class APIError extends Error { constructor(message, status, code) { super(message); this.name = 'APIError'; this.status = status; this.code = code; } } // Öffentliche API return { get, post, put, patch, del, upload, auth, dogs, diary, health, poison, places, routes, weather, push, subscribeToPush, getLocation, APIError, }; })();