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:
Matthias Lotz 2025-11-16 18:39:40 +01:00
parent 25324cb91f
commit cb640576f4
7 changed files with 148 additions and 72 deletions

9
frontend/.env.example Normal file
View 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

View File

@ -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);

View File

@ -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) {

View File

@ -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 = {

View File

@ -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;

View File

@ -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);

View 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();
};