From cb640576f4b7acc1fbd02821450a0ce51f8dc993 Mon Sep 17 00:00:00 2001 From: "matthias.lotz" Date: Sun, 16 Nov 2025 18:39:40 +0100 Subject: [PATCH] feat(frontend): Migrate all API routes to new structure with admin auth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1: Route Structure & Admin Authentication ✅ Route Prefix Fixes: - All routes now use consistent /api prefix - Public: /groups/* → /api/groups/* - Admin: /groups/*, /moderation/* → /api/admin/* - Social Media: /api/social-media/* → /api/admin/social-media/* ✅ Admin API Authentication: - Created adminApi.js service with Bearer Token helpers * adminFetch() - Base fetch with Authorization header * adminGet() - GET with auto error handling * adminRequest() - POST/PUT/PATCH/DELETE with JSON * adminDownload() - For Blob downloads (CSV exports) - Added frontend/.env.example with REACT_APP_ADMIN_API_KEY - All /api/admin/* calls now use admin helpers ✅ Updated Components: - ModerationGroupsPage.js: All admin endpoints migrated - ModerationGroupImagesPage.js: Group loading + image deletion - PublicGroupImagesPage.js: Fixed public group route - DeletionLogSection.js: Deletion log endpoints - ConsentCheckboxes.js: Platform loading ⚠️ Next Steps: - Add user-friendly 403 error handling - Test all affected pages - Configure REACT_APP_ADMIN_API_KEY in deployment --- frontend/.env.example | 9 ++ .../ComponentUtils/DeletionLogSection.js | 19 +--- .../MultiUpload/ConsentCheckboxes.js | 9 +- .../Pages/ModerationGroupImagesPage.js | 7 +- .../Components/Pages/ModerationGroupsPage.js | 68 ++++------- .../Components/Pages/PublicGroupImagesPage.js | 2 +- frontend/src/services/adminApi.js | 106 ++++++++++++++++++ 7 files changed, 148 insertions(+), 72 deletions(-) create mode 100644 frontend/.env.example create mode 100644 frontend/src/services/adminApi.js diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..9862726 --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1,9 @@ +# Frontend Environment Variables + +# Admin API Authentication Token +# Generate with: openssl rand -hex 32 +# Must match ADMIN_API_KEY in backend/.env +REACT_APP_ADMIN_API_KEY=your-secure-admin-token-here + +# API Base URL (optional, defaults to same domain) +# REACT_APP_API_URL=http://localhost:3001 diff --git a/frontend/src/Components/ComponentUtils/DeletionLogSection.js b/frontend/src/Components/ComponentUtils/DeletionLogSection.js index 4af3458..4649919 100644 --- a/frontend/src/Components/ComponentUtils/DeletionLogSection.js +++ b/frontend/src/Components/ComponentUtils/DeletionLogSection.js @@ -19,6 +19,9 @@ import DeleteIcon from '@mui/icons-material/Delete'; import WarningIcon from '@mui/icons-material/Warning'; import InfoIcon from '@mui/icons-material/Info'; +// Services +import { adminGet } from '../../services/adminApi'; + const DeletionLogSection = () => { const [deletions, setDeletions] = useState([]); const [statistics, setStatistics] = useState(null); @@ -39,13 +42,7 @@ const DeletionLogSection = () => { ? '/api/admin/deletion-log/all' : '/api/admin/deletion-log?limit=10'; - const response = await fetch(endpoint); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const data = await response.json(); + const data = await adminGet(endpoint); setDeletions(data.deletions || []); setError(null); } catch (error) { @@ -58,13 +55,7 @@ const DeletionLogSection = () => { const loadStatistics = async () => { try { - const response = await fetch('/api/admin/deletion-log/stats'); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const data = await response.json(); + const data = await adminGet('/api/admin/deletion-log/stats'); setStatistics(data.statistics || null); } catch (error) { console.error('Fehler beim Laden der Statistiken:', error); diff --git a/frontend/src/Components/ComponentUtils/MultiUpload/ConsentCheckboxes.js b/frontend/src/Components/ComponentUtils/MultiUpload/ConsentCheckboxes.js index 255525f..b653009 100644 --- a/frontend/src/Components/ComponentUtils/MultiUpload/ConsentCheckboxes.js +++ b/frontend/src/Components/ComponentUtils/MultiUpload/ConsentCheckboxes.js @@ -8,6 +8,9 @@ import { Divider, Alert } from '@mui/material'; + +// Services +import { adminGet } from '../../../services/adminApi'; import InfoIcon from '@mui/icons-material/Info'; import FacebookIcon from '@mui/icons-material/Facebook'; import InstagramIcon from '@mui/icons-material/Instagram'; @@ -52,11 +55,7 @@ function ConsentCheckboxes({ const fetchPlatforms = async () => { try { - const response = await fetch('/api/social-media/platforms'); - if (!response.ok) { - throw new Error('Failed to load platforms'); - } - const data = await response.json(); + const data = await adminGet('/api/admin/social-media/platforms'); setPlatforms(data); setError(null); } catch (error) { diff --git a/frontend/src/Components/Pages/ModerationGroupImagesPage.js b/frontend/src/Components/Pages/ModerationGroupImagesPage.js index a5e6416..31beed1 100644 --- a/frontend/src/Components/Pages/ModerationGroupImagesPage.js +++ b/frontend/src/Components/Pages/ModerationGroupImagesPage.js @@ -2,6 +2,9 @@ import React, { useState, useEffect, useCallback } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { Container, Box } from '@mui/material'; +// Services +import { adminGet, adminRequest } from '../../services/adminApi'; + // Components import Navbar from '../ComponentUtils/Headers/Navbar'; import Footer from '../ComponentUtils/Footer'; @@ -26,9 +29,7 @@ const ModerationGroupImagesPage = () => { const loadGroup = useCallback(async () => { try { setLoading(true); - const res = await fetch(`/moderation/groups/${groupId}`); - if (!res.ok) throw new Error('Nicht gefunden'); - const data = await res.json(); + const data = await adminGet(`/api/admin/groups/${groupId}`); // Transform data similar to ManagementPortalPage const transformedData = { diff --git a/frontend/src/Components/Pages/ModerationGroupsPage.js b/frontend/src/Components/Pages/ModerationGroupsPage.js index df89462..d255d7d 100644 --- a/frontend/src/Components/Pages/ModerationGroupsPage.js +++ b/frontend/src/Components/Pages/ModerationGroupsPage.js @@ -4,6 +4,11 @@ import { useNavigate } from 'react-router-dom'; import { Container, Box, FormControl, InputLabel, Select, MenuItem } from '@mui/material'; import FilterListIcon from '@mui/icons-material/FilterList'; import Swal from 'sweetalert2/dist/sweetalert2.js'; + +// Services +import { adminGet, adminRequest, adminDownload } from '../../services/adminApi'; + +// Components import Navbar from '../ComponentUtils/Headers/Navbar'; import Footer from '../ComponentUtils/Footer'; import ImageGallery from '../ComponentUtils/ImageGallery'; @@ -32,11 +37,8 @@ const ModerationGroupsPage = () => { const loadPlatforms = async () => { try { - const response = await fetch('/api/social-media/platforms'); - if (response.ok) { - const data = await response.json(); - setPlatforms(data); - } + const data = await adminGet('/api/admin/social-media/platforms'); + setPlatforms(data); } catch (error) { console.error('Fehler beim Laden der Plattformen:', error); } @@ -47,7 +49,7 @@ const ModerationGroupsPage = () => { setLoading(true); // Build URL with filter params - let url = '/moderation/groups'; + let url = '/api/admin/groups'; const params = new URLSearchParams(); if (consentFilter !== 'all') { @@ -63,13 +65,7 @@ const ModerationGroupsPage = () => { url += '?' + params.toString(); } - const response = await fetch(url); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const data = await response.json(); + const data = await adminGet(url); setGroups(data.groups); } catch (error) { console.error('Fehler beim Laden der Moderations-Gruppen:', error); @@ -81,17 +77,11 @@ const ModerationGroupsPage = () => { const approveGroup = async (groupId, approved) => { try { - const response = await fetch(`/groups/${groupId}/approve`, { - method: 'PATCH', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ approved: approved }) - }); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } + await adminRequest( + `/api/admin/groups/${groupId}/approve`, + 'PATCH', + { approved: approved } + ); // Update local state setGroups(groups.map(group => @@ -125,20 +115,11 @@ const ModerationGroupsPage = () => { console.log('API_URL:', window._env_.API_URL); try { - // Use relative URL to go through Nginx proxy - const url = `/groups/${groupId}/images/${imageId}`; + // Use admin API endpoint + const url = `/api/admin/groups/${groupId}/images/${imageId}`; console.log('DELETE request to:', url); - const response = await fetch(url, { - method: 'DELETE' - }); - - console.log('Response status:', response.status); - console.log('Response ok:', response.ok); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } + await adminRequest(url, 'DELETE'); // Remove image from selectedGroup if (selectedGroup && selectedGroup.groupId === groupId) { @@ -170,13 +151,7 @@ const ModerationGroupsPage = () => { } try { - const response = await fetch(`/groups/${groupId}`, { - method: 'DELETE' - }); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } + await adminRequest(`/api/admin/groups/${groupId}`, 'DELETE'); setGroups(groups.filter(group => group.groupId !== groupId)); if (selectedGroup && selectedGroup.groupId === groupId) { @@ -196,13 +171,8 @@ const ModerationGroupsPage = () => { const exportConsentData = async () => { try { - const response = await fetch('/api/admin/consents/export?format=csv'); + const blob = await adminDownload('/api/admin/consents/export?format=csv'); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const blob = await response.blob(); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; diff --git a/frontend/src/Components/Pages/PublicGroupImagesPage.js b/frontend/src/Components/Pages/PublicGroupImagesPage.js index e0a93fc..5c9bec9 100644 --- a/frontend/src/Components/Pages/PublicGroupImagesPage.js +++ b/frontend/src/Components/Pages/PublicGroupImagesPage.js @@ -22,7 +22,7 @@ const PublicGroupImagesPage = () => { try { setLoading(true); // Public endpoint (no moderation controls) - const res = await fetch(`/groups/${groupId}`); + const res = await fetch(`/api/groups/${groupId}`); if (!res.ok) throw new Error('Nicht gefunden'); const data = await res.json(); setGroup(data); diff --git a/frontend/src/services/adminApi.js b/frontend/src/services/adminApi.js new file mode 100644 index 0000000..ef62edd --- /dev/null +++ b/frontend/src/services/adminApi.js @@ -0,0 +1,106 @@ +/** + * Admin API Helper mit Bearer Token Authentication + * + * Verwendet für alle /api/admin/* und /api/system/* Endpoints + */ + +/** + * Führt einen fetch-Request mit Admin-Bearer-Token aus + * @param {string} url - Die URL (mit /api/admin/* oder /api/system/* Prefix) + * @param {object} options - Fetch options (method, body, headers, etc.) + * @returns {Promise} + */ +export const adminFetch = async (url, options = {}) => { + const token = process.env.REACT_APP_ADMIN_API_KEY; + + if (!token) { + console.error('REACT_APP_ADMIN_API_KEY not configured!'); + throw new Error('Admin API Token not configured'); + } + + const headers = { + ...options.headers, + 'Authorization': `Bearer ${token}` + }; + + return fetch(url, { + ...options, + headers + }); +}; + +/** + * Hilfsfunktion für GET-Requests mit automatischer JSON-Parsing und Error-Handling + * @param {string} url + * @returns {Promise} + */ +export const adminGet = async (url) => { + const response = await adminFetch(url); + + if (!response.ok) { + if (response.status === 403) { + throw new Error('Unauthorized: Invalid or missing admin token'); + } + if (response.status === 429) { + throw new Error('Too many requests: Rate limit exceeded'); + } + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + return response.json(); +}; + +/** + * Hilfsfunktion für POST/PUT/PATCH/DELETE mit JSON body + * @param {string} url + * @param {string} method + * @param {object} body + * @returns {Promise} + */ +export const adminRequest = async (url, method, body = null) => { + const options = { + method, + headers: { + 'Content-Type': 'application/json' + } + }; + + if (body) { + options.body = JSON.stringify(body); + } + + const response = await adminFetch(url, options); + + if (!response.ok) { + if (response.status === 403) { + throw new Error('Unauthorized: Invalid or missing admin token'); + } + if (response.status === 429) { + throw new Error('Too many requests: Rate limit exceeded'); + } + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + return response; +}; + +/** + * Hilfsfunktion für Blob/File Downloads (CSV, PDF, etc.) + * @param {string} url + * @returns {Promise} + */ +export const adminDownload = async (url) => { + const response = await adminFetch(url); + + if (!response.ok) { + if (response.status === 403) { + throw new Error('Unauthorized: Invalid or missing admin token'); + } + if (response.status === 429) { + throw new Error('Too many requests: Rate limit exceeded'); + } + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + return response.blob(); +};