- File input in feedback modal (max 3 images, 5 MB each) - Frontend validation for file count and size - Backend: multipart/form-data with UploadFile, MIME attachments - Images attached to feedback email as base64-encoded attachments - Only JPEG and PNG allowed (validated server-side) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
225 Zeilen
6.5 KiB
JavaScript
225 Zeilen
6.5 KiB
JavaScript
/**
|
|
* API-Client für den OSINT Lagemonitor.
|
|
*/
|
|
const API = {
|
|
baseUrl: '/api',
|
|
|
|
_getHeaders() {
|
|
const token = localStorage.getItem('osint_token');
|
|
return {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': token ? `Bearer ${token}` : '',
|
|
};
|
|
},
|
|
|
|
async _request(method, path, body = null) {
|
|
const controller = new AbortController();
|
|
const timeout = setTimeout(() => controller.abort(), 30000);
|
|
|
|
const options = {
|
|
method,
|
|
headers: this._getHeaders(),
|
|
signal: controller.signal,
|
|
};
|
|
if (body) {
|
|
options.body = JSON.stringify(body);
|
|
}
|
|
|
|
let response;
|
|
try {
|
|
response = await fetch(`${this.baseUrl}${path}`, options);
|
|
} catch (err) {
|
|
clearTimeout(timeout);
|
|
if (err.name === 'AbortError') {
|
|
throw new Error('Zeitüberschreitung bei der Anfrage');
|
|
}
|
|
throw err;
|
|
}
|
|
clearTimeout(timeout);
|
|
|
|
if (response.status === 401) {
|
|
localStorage.removeItem('osint_token');
|
|
localStorage.removeItem('osint_username');
|
|
window.location.href = '/';
|
|
return;
|
|
}
|
|
|
|
if (!response.ok) {
|
|
const data = await response.json().catch(() => ({}));
|
|
let detail = data.detail;
|
|
if (Array.isArray(detail)) {
|
|
detail = detail.map(e => e.msg || JSON.stringify(e)).join('; ');
|
|
} else if (typeof detail === 'object' && detail !== null) {
|
|
detail = JSON.stringify(detail);
|
|
}
|
|
throw new Error(detail || `Fehler ${response.status}`);
|
|
}
|
|
|
|
if (response.status === 204) return null;
|
|
return response.json();
|
|
},
|
|
|
|
// Auth
|
|
getMe() {
|
|
return this._request('GET', '/auth/me');
|
|
},
|
|
|
|
// Incidents
|
|
listIncidents(statusFilter = null) {
|
|
const query = statusFilter ? `?status_filter=${statusFilter}` : '';
|
|
return this._request('GET', `/incidents${query}`);
|
|
},
|
|
|
|
createIncident(data) {
|
|
return this._request('POST', '/incidents', data);
|
|
},
|
|
|
|
getRefreshingIncidents() {
|
|
return this._request('GET', '/incidents/refreshing');
|
|
},
|
|
|
|
getIncident(id) {
|
|
return this._request('GET', `/incidents/${id}`);
|
|
},
|
|
|
|
updateIncident(id, data) {
|
|
return this._request('PUT', `/incidents/${id}`, data);
|
|
},
|
|
|
|
deleteIncident(id) {
|
|
return this._request('DELETE', `/incidents/${id}`);
|
|
},
|
|
|
|
getArticles(incidentId) {
|
|
return this._request('GET', `/incidents/${incidentId}/articles`);
|
|
},
|
|
|
|
getFactChecks(incidentId) {
|
|
return this._request('GET', `/incidents/${incidentId}/factchecks`);
|
|
},
|
|
|
|
getSnapshots(incidentId) {
|
|
return this._request('GET', `/incidents/${incidentId}/snapshots`);
|
|
},
|
|
|
|
getLocations(incidentId) {
|
|
return this._request('GET', `/incidents/${incidentId}/locations`);
|
|
},
|
|
|
|
triggerGeoparse(incidentId) {
|
|
return this._request('POST', `/incidents/${incidentId}/geoparse`);
|
|
},
|
|
|
|
getGeoparseStatus(incidentId) {
|
|
return this._request('GET', `/incidents/${incidentId}/geoparse-status`);
|
|
},
|
|
|
|
refreshIncident(id) {
|
|
return this._request('POST', `/incidents/${id}/refresh`);
|
|
},
|
|
|
|
getRefreshLog(incidentId, limit = 20) {
|
|
return this._request('GET', `/incidents/${incidentId}/refresh-log?limit=${limit}`);
|
|
},
|
|
|
|
// Sources (Quellenverwaltung)
|
|
listSources(params = {}) {
|
|
const query = new URLSearchParams();
|
|
if (params.source_type) query.set('source_type', params.source_type);
|
|
if (params.category) query.set('category', params.category);
|
|
if (params.source_status) query.set('source_status', params.source_status);
|
|
const qs = query.toString();
|
|
return this._request('GET', `/sources${qs ? '?' + qs : ''}`);
|
|
},
|
|
|
|
createSource(data) {
|
|
return this._request('POST', '/sources', data);
|
|
},
|
|
|
|
updateSource(id, data) {
|
|
return this._request('PUT', `/sources/${id}`, data);
|
|
},
|
|
|
|
deleteSource(id) {
|
|
return this._request('DELETE', `/sources/${id}`);
|
|
},
|
|
|
|
getSourceStats() {
|
|
return this._request('GET', '/sources/stats');
|
|
},
|
|
|
|
discoverMulti(url) {
|
|
return this._request('POST', '/sources/discover-multi', { url });
|
|
},
|
|
|
|
getMyExclusions() {
|
|
return this._request('GET', '/sources/my-exclusions');
|
|
},
|
|
|
|
blockDomain(domain, notes) {
|
|
return this._request('POST', '/sources/block-domain', { domain, notes });
|
|
},
|
|
|
|
unblockDomain(domain) {
|
|
return this._request('POST', '/sources/unblock-domain', { domain });
|
|
},
|
|
|
|
deleteDomain(domain) {
|
|
return this._request('DELETE', `/sources/domain/${encodeURIComponent(domain)}`);
|
|
},
|
|
|
|
cancelRefresh(id) {
|
|
return this._request('POST', `/incidents/${id}/cancel-refresh`);
|
|
},
|
|
|
|
// Notifications
|
|
listNotifications(limit = 50) {
|
|
return this._request('GET', `/notifications?limit=${limit}`);
|
|
},
|
|
|
|
markNotificationsRead(ids = null) {
|
|
return this._request('PUT', '/notifications/mark-read', { notification_ids: ids });
|
|
},
|
|
|
|
|
|
|
|
// Subscriptions (E-Mail-Benachrichtigungen)
|
|
getSubscription(incidentId) {
|
|
return this._request('GET', '/incidents/' + incidentId + '/subscription');
|
|
},
|
|
|
|
updateSubscription(incidentId, data) {
|
|
return this._request('PUT', '/incidents/' + incidentId + '/subscription', data);
|
|
},
|
|
|
|
// Feedback
|
|
sendFeedback(data) {
|
|
return this._request('POST', '/feedback', data);
|
|
},
|
|
|
|
async sendFeedbackForm(formData) {
|
|
const token = localStorage.getItem('osint_token');
|
|
const controller = new AbortController();
|
|
const timeout = setTimeout(() => controller.abort(), 60000);
|
|
const resp = await fetch(this.baseUrl + '/feedback', {
|
|
method: 'POST',
|
|
headers: { 'Authorization': token ? 'Bearer ' + token : '' },
|
|
body: formData,
|
|
signal: controller.signal,
|
|
});
|
|
clearTimeout(timeout);
|
|
if (!resp.ok) {
|
|
const err = await resp.json().catch(() => ({}));
|
|
throw new Error(err.detail || 'Fehler ' + resp.status);
|
|
}
|
|
},
|
|
|
|
// Export
|
|
exportIncident(id, format, scope) {
|
|
const token = localStorage.getItem('osint_token');
|
|
return fetch(`${this.baseUrl}/incidents/${id}/export?format=${format}&scope=${scope}`, {
|
|
headers: { 'Authorization': `Bearer ${token}` },
|
|
});
|
|
},
|
|
};
|