fix: Update Swagger Grouping

This commit is contained in:
Matthias Lotz 2025-11-23 21:48:40 +01:00
parent 6332b82c6a
commit 7a14c239d4
10 changed files with 225 additions and 127 deletions

View File

@ -15,7 +15,6 @@ Im Rahmen der OpenAPI-Auto-Generation wurden **massive Änderungen** an der API-
- **`backend/src/routes/README.md`** - Vollständige API-Route-Dokumentation - **`backend/src/routes/README.md`** - Vollständige API-Route-Dokumentation
- **`AUTHENTICATION.md`** - Auth-System-Setup und Verwendung - **`AUTHENTICATION.md`** - Auth-System-Setup und Verwendung
**Geschätzter Migrations-Aufwand**: 2-3 Stunden
--- ---
@ -34,7 +33,7 @@ docker compose -f docker/dev/docker-compose.yml up -d
### Zugriff ### Zugriff
- **Frontend**: http://localhost:3000 (Hot Module Reloading aktiv) - **Frontend**: http://localhost:3000 (Hot Module Reloading aktiv)
- **Backend**: http://localhost:5001 (API) - **Backend**: http://localhost:5001 (API)
- **API Documentation**: http://localhost:5001/api/docs (Swagger UI) - **API Documentation**: http://localhost:5001/api/docs/ (Swagger UI)
- **Slideshow**: http://localhost:3000/slideshow - **Slideshow**: http://localhost:3000/slideshow
- **Moderation**: http://localhost:3000/moderation (Login über Admin Session) - **Moderation**: http://localhost:3000/moderation (Login über Admin Session)
@ -116,6 +115,8 @@ Router mit spezifischen Routes **vor** generischen Routes mounten!
``` ```
2. **Management Portal (UUID Token)**: 2. **Management Portal (UUID Token)**:
User, die Bilder hochladen, erhalten automatisch einen UUID-Token für das Self-Service Management Portal.
Über diesen Token / Link können sie ihre hochgeladenen Gruppen verwalten:
```bash ```bash
# Automatisch beim Upload generiert # Automatisch beim Upload generiert
GET /api/manage/550e8400-e29b-41d4-a716-446655440000 GET /api/manage/550e8400-e29b-41d4-a716-446655440000
@ -125,12 +126,8 @@ Router mit spezifischen Routes **vor** generischen Routes mounten!
#### Admin-Hinweise: Logout & neue Nutzer #### Admin-Hinweise: Logout & neue Nutzer
- **Logout:** Bis ein eigener Button im UI existiert, kann die Session jederzeit über den vorhandenen Endpoint beendet werden, z. B. in der Browser-Konsole: - **Logout:** Der Moderationsbereich enthält jetzt einen Logout-Button (Icon in der Kopfzeile). Klick → `POST /auth/logout` → Session beendet, Login erscheint erneut. Für Skripte kannst du weiterhin `curl -b cookies.txt -X POST http://localhost:5001/auth/logout` verwenden.
```js - **Weiterer Admin:** Verwende das neue API-basierte Skript `./scripts/create_admin_user.sh --server http://localhost:5001 --username zweiteradmin --password 'SuperPasswort123!' [--admin-user bestehend --admin-password ... --role ... --require-password-change]`. Das Skript erledigt Login, CSRF, Duplikats-Check und legt zusätzliche Admins über `/api/admin/users` an (Fallback: `backend/src/scripts/createAdminUser.js`).
await fetch('/auth/logout', { method: 'POST', credentials: 'include' });
```
Alternativ per CLI: `curl -b cookies.txt -X POST http://localhost:5001/auth/logout`. Danach ist das `sid`-Cookie entfernt und die Moderationsseite zeigt wieder den Login.
- **Weiterer Admin:** `npm run create-admin -- --username zweiteradmin --password 'SuperPasswort123!' [--role admin --require-password-change]` oder alternativ `./scripts/create_admin_user.sh --username zweiteradmin --password 'SuperPasswort123!' [...]` ruft das Skript (`backend/src/scripts/createAdminUser.js`) auf und legt einen weiteren User an. Das Skript prüft Duplikate, nutzt dieselben Bcrypt-Runden wie das Backend und kann bei Bedarf weiterhin über die DB nachvollzogen werden. Falls du lieber manuell arbeitest, kannst du wie bisher einen Hash erzeugen und direkt in `admin_users` einfügen.
### OpenAPI-Spezifikation ### OpenAPI-Spezifikation

View File

@ -12,15 +12,24 @@
} }
], ],
"tags": [ "tags": [
{
"name": "Admin Authentication"
},
{ {
"name": "Upload" "name": "Upload"
}, },
{ {
"name": "Management Portal" "name": "Download"
},
{
"name": "Public Groups"
}, },
{ {
"name": "Consent Management" "name": "Consent Management"
}, },
{
"name": "Management Portal"
},
{ {
"name": "Admin - Users" "name": "Admin - Users"
}, },
@ -43,7 +52,11 @@
"paths": { "paths": {
"/auth/setup/status": { "/auth/setup/status": {
"get": { "get": {
"description": "", "tags": [
"Admin Authentication"
],
"summary": "Check onboarding status",
"description": "Returns whether the initial admin setup is still pending and if a session already exists.",
"responses": { "responses": {
"200": { "200": {
"description": "OK" "description": "OK"
@ -56,7 +69,11 @@
}, },
"/auth/setup/initial-admin": { "/auth/setup/initial-admin": {
"post": { "post": {
"description": "", "tags": [
"Admin Authentication"
],
"summary": "Complete initial admin setup",
"description": "Creates the very first admin account and immediately starts a session.",
"parameters": [ "parameters": [
{ {
"name": "body", "name": "body",
@ -92,7 +109,11 @@
}, },
"/auth/login": { "/auth/login": {
"post": { "post": {
"description": "", "tags": [
"Admin Authentication"
],
"summary": "Admin login",
"description": "Starts a server-side admin session and returns a CSRF token.",
"parameters": [ "parameters": [
{ {
"name": "body", "name": "body",
@ -131,7 +152,11 @@
}, },
"/auth/logout": { "/auth/logout": {
"post": { "post": {
"description": "", "tags": [
"Admin Authentication"
],
"summary": "Terminate admin session",
"description": "Destroys the current session and clears the sid cookie.",
"responses": { "responses": {
"204": { "204": {
"description": "No Content" "description": "No Content"
@ -144,7 +169,11 @@
}, },
"/auth/csrf-token": { "/auth/csrf-token": {
"get": { "get": {
"description": "", "tags": [
"Admin Authentication"
],
"summary": "Fetch CSRF token",
"description": "Returns a CSRF token for the active admin session (session required).",
"responses": { "responses": {
"200": { "200": {
"description": "OK" "description": "OK"
@ -157,7 +186,11 @@
}, },
"/auth/change-password": { "/auth/change-password": {
"post": { "post": {
"description": "", "tags": [
"Admin Authentication"
],
"summary": "Change admin password",
"description": "Allows a logged-in admin to rotate their password (CSRF protected).",
"parameters": [ "parameters": [
{ {
"name": "x-csrf-token", "name": "x-csrf-token",
@ -297,25 +330,40 @@
}, },
"/api/download/{id}": { "/api/download/{id}": {
"get": { "get": {
"tags": [
"Download"
],
"summary": "Download original image",
"description": "", "description": "",
"parameters": [ "parameters": [
{ {
"name": "id", "name": "id",
"in": "path", "in": "path",
"required": true, "required": true,
"type": "string" "type": "string",
"description": "Filename of the uploaded image"
} }
], ],
"responses": { "responses": {
"default": { "200": {
"description": "" "description": "Binary image response"
},
"404": {
"description": "File not found"
} }
} }
} }
}, },
"/api/upload/batch": { "/api/upload/batch": {
"post": { "post": {
"description": "", "tags": [
"Upload"
],
"summary": "Batch upload multiple images",
"description": "Accepts multiple images + metadata/consents and creates a managed group with management token.",
"consumes": [
"multipart/form-data"
],
"parameters": [ "parameters": [
{ {
"name": "body", "name": "body",
@ -341,57 +389,98 @@
], ],
"responses": { "responses": {
"200": { "200": {
"description": "OK" "description": "Batch upload successful (returns management token)"
}, },
"400": { "400": {
"description": "Bad Request" "description": "Missing files or workshop consent"
}, },
"500": { "500": {
"description": "Internal Server Error" "description": "Unexpected server error"
} }
} }
} }
}, },
"/api/groups": { "/api/groups": {
"get": { "get": {
"description": "", "tags": [
"Public Groups"
],
"summary": "Get approved groups with images",
"description": "Returns all approved groups (slideshow feed). Automatically triggers JSON→SQLite migration if required.",
"responses": { "responses": {
"200": { "200": {
"description": "OK" "description": "List of approved groups",
"schema": {
"type": "object",
"properties": {
"groups": {
"type": "array",
"items": {
"type": "object",
"properties": {
"groupId": {
"type": "string",
"example": "cTV24Yn-a"
},
"title": {
"type": "string",
"example": "Familie Mueller"
}
}
}
},
"totalCount": {
"type": "number",
"example": 73
}
},
"xml": {
"name": "main"
}
}
}, },
"500": { "500": {
"description": "Internal Server Error" "description": "Server error"
} }
} }
} }
}, },
"/api/groups/{groupId}": { "/api/groups/{groupId}": {
"get": { "get": {
"tags": [
"Public Groups"
],
"summary": "Get approved group by ID",
"description": "", "description": "",
"parameters": [ "parameters": [
{ {
"name": "groupId", "name": "groupId",
"in": "path", "in": "path",
"required": true, "required": true,
"type": "string" "type": "string",
"description": "Public groupId (e.g. cTV24Yn-a)"
} }
], ],
"responses": { "responses": {
"200": { "200": {
"description": "OK" "description": "Group payload (images + metadata)"
}, },
"404": { "404": {
"description": "Not Found" "description": "Group not found or not approved"
}, },
"500": { "500": {
"description": "Internal Server Error" "description": "Server error"
} }
} }
} }
}, },
"/api/social-media/platforms": { "/api/social-media/platforms": {
"get": { "get": {
"description": "", "tags": [
"Consent Management"
],
"summary": "List active social media platforms",
"description": "Public endpoint that exposes the available platforms for consent selection on the upload form.",
"responses": { "responses": {
"200": { "200": {
"description": "OK" "description": "OK"
@ -2483,13 +2572,18 @@
}, },
"/api/admin/{groupId}/reorder": { "/api/admin/{groupId}/reorder": {
"put": { "put": {
"tags": [
"Admin - Groups Moderation"
],
"summary": "Reorder images within a group",
"description": "", "description": "",
"parameters": [ "parameters": [
{ {
"name": "groupId", "name": "groupId",
"in": "path", "in": "path",
"required": true, "required": true,
"type": "string" "type": "string",
"description": "Admin groupId"
}, },
{ {
"name": "body", "name": "body",
@ -2506,19 +2600,19 @@
], ],
"responses": { "responses": {
"200": { "200": {
"description": "OK" "description": "Order updated successfully"
}, },
"400": { "400": {
"description": "Bad Request" "description": "Validation error"
}, },
"403": { "403": {
"description": "Forbidden" "description": "Forbidden"
}, },
"404": { "404": {
"description": "Not Found" "description": "Group not found"
}, },
"500": { "500": {
"description": "Internal Server Error" "description": "Internal server error"
} }
} }
} }

View File

@ -5,6 +5,11 @@ const { requireAdminAuth } = require('../middlewares/auth');
const { requireCsrf } = require('../middlewares/csrf'); const { requireCsrf } = require('../middlewares/csrf');
router.get('/setup/status', async (req, res) => { router.get('/setup/status', async (req, res) => {
/*
#swagger.tags = ['Admin Authentication']
#swagger.summary = 'Check onboarding status'
#swagger.description = 'Returns whether the initial admin setup is still pending and if a session already exists.'
*/
try { try {
const needsSetup = await AdminAuthService.needsInitialSetup(); const needsSetup = await AdminAuthService.needsInitialSetup();
const sessionUser = req.session && req.session.user const sessionUser = req.session && req.session.user
@ -27,6 +32,11 @@ router.get('/setup/status', async (req, res) => {
}); });
router.post('/setup/initial-admin', async (req, res) => { router.post('/setup/initial-admin', async (req, res) => {
/*
#swagger.tags = ['Admin Authentication']
#swagger.summary = 'Complete initial admin setup'
#swagger.description = 'Creates the very first admin account and immediately starts a session.'
*/
try { try {
const { username, password } = req.body || {}; const { username, password } = req.body || {};
if (!username || !password) { if (!username || !password) {
@ -67,6 +77,11 @@ router.post('/setup/initial-admin', async (req, res) => {
}); });
router.post('/login', async (req, res) => { router.post('/login', async (req, res) => {
/*
#swagger.tags = ['Admin Authentication']
#swagger.summary = 'Admin login'
#swagger.description = 'Starts a server-side admin session and returns a CSRF token.'
*/
try { try {
const { username, password } = req.body || {}; const { username, password } = req.body || {};
if (!username || !password) { if (!username || !password) {
@ -100,6 +115,11 @@ router.post('/login', async (req, res) => {
}); });
router.post('/logout', async (req, res) => { router.post('/logout', async (req, res) => {
/*
#swagger.tags = ['Admin Authentication']
#swagger.summary = 'Terminate admin session'
#swagger.description = 'Destroys the current session and clears the sid cookie.'
*/
try { try {
await AdminAuthService.destroySession(req); await AdminAuthService.destroySession(req);
res.clearCookie('sid'); res.clearCookie('sid');
@ -111,6 +131,11 @@ router.post('/logout', async (req, res) => {
}); });
router.get('/csrf-token', requireAdminAuth, (req, res) => { router.get('/csrf-token', requireAdminAuth, (req, res) => {
/*
#swagger.tags = ['Admin Authentication']
#swagger.summary = 'Fetch CSRF token'
#swagger.description = 'Returns a CSRF token for the active admin session (session required).'
*/
if (!req.session.csrfToken) { if (!req.session.csrfToken) {
req.session.csrfToken = AdminAuthService.generateCsrfToken(); req.session.csrfToken = AdminAuthService.generateCsrfToken();
} }
@ -119,6 +144,11 @@ router.get('/csrf-token', requireAdminAuth, (req, res) => {
}); });
router.post('/change-password', requireAdminAuth, requireCsrf, async (req, res) => { router.post('/change-password', requireAdminAuth, requireCsrf, async (req, res) => {
/*
#swagger.tags = ['Admin Authentication']
#swagger.summary = 'Change admin password'
#swagger.description = 'Allows a logged-in admin to rotate their password (CSRF protected).'
*/
try { try {
const { currentPassword, newPassword } = req.body || {}; const { currentPassword, newPassword } = req.body || {};
if (!currentPassword || !newPassword) { if (!currentPassword || !newPassword) {

View File

@ -84,6 +84,15 @@ const router = Router();
*/ */
// Batch-Upload für mehrere Bilder // Batch-Upload für mehrere Bilder
router.post('/upload/batch', async (req, res) => { router.post('/upload/batch', async (req, res) => {
/*
#swagger.tags = ['Upload']
#swagger.summary = 'Batch upload multiple images'
#swagger.description = 'Accepts multiple images + metadata/consents and creates a managed group with management token.'
#swagger.consumes = ['multipart/form-data']
#swagger.responses[200] = { description: 'Batch upload successful (returns management token)' }
#swagger.responses[400] = { description: 'Missing files or workshop consent' }
#swagger.responses[500] = { description: 'Unexpected server error' }
*/
try { try {
// Überprüfe ob Dateien hochgeladen wurden // Überprüfe ob Dateien hochgeladen wurden
if (!req.files || !req.files.images) { if (!req.files || !req.files.images) {

View File

@ -31,6 +31,18 @@ const router = Router();
* description: File not found * description: File not found
*/ */
router.get('/download/:id', (req, res) => { router.get('/download/:id', (req, res) => {
/*
#swagger.tags = ['Download']
#swagger.summary = 'Download original image'
#swagger.parameters['id'] = {
in: 'path',
required: true,
type: 'string',
description: 'Filename of the uploaded image'
}
#swagger.responses[200] = { description: 'Binary image response' }
#swagger.responses[404] = { description: 'File not found' }
*/
const filePath = path.join(__dirname, '..', UPLOAD_FS_DIR, req.params.id); const filePath = path.join(__dirname, '..', UPLOAD_FS_DIR, req.params.id);
res.download(filePath); res.download(filePath);
}); });

View File

@ -4,54 +4,21 @@ const MigrationService = require('../services/MigrationService');
const router = Router(); const router = Router();
/**
* @swagger
* /groups:
* get:
* tags: [Groups]
* summary: Get all approved groups with images
* description: Returns all approved groups with their images for public slideshow display. Automatically triggers migration if needed.
* responses:
* 200:
* description: List of approved groups
* content:
* application/json:
* schema:
* type: object
* properties:
* groups:
* type: array
* items:
* type: object
* properties:
* groupId:
* type: string
* example: "cTV24Yn-a"
* year:
* type: integer
* example: 2024
* title:
* type: string
* example: "Familie Mueller"
* description:
* type: string
* name:
* type: string
* approved:
* type: boolean
* example: true
* images:
* type: array
* items:
* type: object
* totalCount:
* type: integer
* example: 73
* 500:
* description: Server error
*/
// Alle Gruppen abrufen (für Slideshow mit vollständigen Bilddaten) // Alle Gruppen abrufen (für Slideshow mit vollständigen Bilddaten)
router.get('/groups', async (req, res) => { router.get('/groups', async (req, res) => {
/*
#swagger.tags = ['Public Groups']
#swagger.summary = 'Get approved groups with images'
#swagger.description = 'Returns all approved groups (slideshow feed). Automatically triggers JSON→SQLite migration if required.'
#swagger.responses[200] = {
description: 'List of approved groups',
schema: {
groups: [{ groupId: 'cTV24Yn-a', title: 'Familie Mueller' }],
totalCount: 73
}
}
#swagger.responses[500] = { description: 'Server error' }
*/
try { try {
// Auto-Migration beim ersten Zugriff // Auto-Migration beim ersten Zugriff
const migrationStatus = await MigrationService.getMigrationStatus(); const migrationStatus = await MigrationService.getMigrationStatus();
@ -75,52 +42,21 @@ router.get('/groups', async (req, res) => {
} }
}); });
/**
* @swagger
* /groups/{groupId}:
* get:
* tags: [Groups]
* summary: Get a specific approved group by ID
* description: Returns details of a single approved group with all its images
* parameters:
* - in: path
* name: groupId
* required: true
* schema:
* type: string
* example: "cTV24Yn-a"
* description: Unique identifier of the group
* responses:
* 200:
* description: Group details
* content:
* application/json:
* schema:
* type: object
* properties:
* groupId:
* type: string
* year:
* type: integer
* title:
* type: string
* description:
* type: string
* name:
* type: string
* approved:
* type: boolean
* images:
* type: array
* items:
* type: object
* 404:
* description: Group not found
* 500:
* description: Server error
*/
// Einzelne Gruppe abrufen (nur freigegebene) // Einzelne Gruppe abrufen (nur freigegebene)
router.get('/groups/:groupId', async (req, res) => { router.get('/groups/:groupId', async (req, res) => {
/*
#swagger.tags = ['Public Groups']
#swagger.summary = 'Get approved group by ID'
#swagger.parameters['groupId'] = {
in: 'path',
required: true,
type: 'string',
description: 'Public groupId (e.g. cTV24Yn-a)'
}
#swagger.responses[200] = { description: 'Group payload (images + metadata)' }
#swagger.responses[404] = { description: 'Group not found or not approved' }
#swagger.responses[500] = { description: 'Server error' }
*/
try { try {
const { groupId } = req.params; const { groupId } = req.params;
const group = await GroupRepository.getGroupById(groupId); const group = await GroupRepository.getGroupById(groupId);

View File

@ -70,6 +70,20 @@ router.use(requireCsrf);
* description: Server error during reordering * description: Server error during reordering
*/ */
router.put('/:groupId/reorder', async (req, res) => { router.put('/:groupId/reorder', async (req, res) => {
/*
#swagger.tags = ['Admin - Groups Moderation']
#swagger.summary = 'Reorder images within a group'
#swagger.parameters['groupId'] = {
in: 'path',
required: true,
type: 'string',
description: 'Admin groupId'
}
#swagger.responses[200] = { description: 'Order updated successfully' }
#swagger.responses[400] = { description: 'Validation error' }
#swagger.responses[404] = { description: 'Group not found' }
#swagger.responses[500] = { description: 'Internal server error' }
*/
try { try {
const { groupId } = req.params; const { groupId } = req.params;
const { imageIds } = req.body; const { imageIds } = req.body;

View File

@ -8,6 +8,11 @@ const router = express.Router();
* Public endpoint: list active social media platforms for consent selection * Public endpoint: list active social media platforms for consent selection
*/ */
router.get('/social-media/platforms', async (req, res) => { router.get('/social-media/platforms', async (req, res) => {
/*
#swagger.tags = ['Consent Management']
#swagger.summary = 'List active social media platforms'
#swagger.description = 'Public endpoint that exposes the available platforms for consent selection on the upload form.'
*/
try { try {
const socialMediaRepo = new SocialMediaRepository(dbManager); const socialMediaRepo = new SocialMediaRepository(dbManager);
const platforms = await socialMediaRepo.getActivePlatforms(); const platforms = await socialMediaRepo.getActivePlatforms();

View File

@ -15,7 +15,7 @@ RUN npm install --production
COPY backend/src ./src COPY backend/src ./src
# Copy production environment configuration # Copy production environment configuration
COPY docker/prod/backend/config/.env ./.env #COPY docker/prod/backend/config/.env ./.env
# Create data directories for file storage # Create data directories for file storage
RUN mkdir -p src/data/images src/data/previews src/data/groups RUN mkdir -p src/data/images src/data/previews src/data/groups

View File

@ -34,8 +34,9 @@ services:
networks: networks:
- prod-internal - prod-internal
environment: environment:
- REMOVE_IMAGES=false
- NODE_ENV=production - NODE_ENV=production
- ADMIN_SESSION_SECRET=${ADMIN_SESSION_SECRET} - ADMIN_SESSION_SECRET=MvFhivVIPIXvSGvWGfGOiQCkUJrmUsjWQTNGUgnSmtpsGHQlKruTBEBZgbVvOHHr
- ADMIN_SESSION_DIR=/usr/src/app/src/data/sessions - ADMIN_SESSION_DIR=/usr/src/app/src/data/sessions
networks: networks: