feat(frontend): Migrate all API routes to new structure with admin auth
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
This commit is contained in:
parent
25324cb91f
commit
cb640576f4
9
frontend/.env.example
Normal file
9
frontend/.env.example
Normal file
|
|
@ -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
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
106
frontend/src/services/adminApi.js
Normal file
106
frontend/src/services/adminApi.js
Normal file
|
|
@ -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<Response>}
|
||||
*/
|
||||
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<any>}
|
||||
*/
|
||||
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<Response>}
|
||||
*/
|
||||
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<Blob>}
|
||||
*/
|
||||
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();
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user