diff --git a/README.dev.md b/README.dev.md index f6f3949..5ac4a55 100644 --- a/README.dev.md +++ b/README.dev.md @@ -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 - **`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 - **Frontend**: http://localhost:3000 (Hot Module Reloading aktiv) - **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 - **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)**: + 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 # Automatisch beim Upload generiert 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 -- **Logout:** Bis ein eigener Button im UI existiert, kann die Session jederzeit über den vorhandenen Endpoint beendet werden, z. B. in der Browser-Konsole: - ```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. +- **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. +- **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`). ### OpenAPI-Spezifikation diff --git a/backend/docs/openapi.json b/backend/docs/openapi.json index 8bf480b..c0cdce5 100644 --- a/backend/docs/openapi.json +++ b/backend/docs/openapi.json @@ -12,15 +12,24 @@ } ], "tags": [ + { + "name": "Admin Authentication" + }, { "name": "Upload" }, { - "name": "Management Portal" + "name": "Download" + }, + { + "name": "Public Groups" }, { "name": "Consent Management" }, + { + "name": "Management Portal" + }, { "name": "Admin - Users" }, @@ -43,7 +52,11 @@ "paths": { "/auth/setup/status": { "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": { "200": { "description": "OK" @@ -56,7 +69,11 @@ }, "/auth/setup/initial-admin": { "post": { - "description": "", + "tags": [ + "Admin Authentication" + ], + "summary": "Complete initial admin setup", + "description": "Creates the very first admin account and immediately starts a session.", "parameters": [ { "name": "body", @@ -92,7 +109,11 @@ }, "/auth/login": { "post": { - "description": "", + "tags": [ + "Admin Authentication" + ], + "summary": "Admin login", + "description": "Starts a server-side admin session and returns a CSRF token.", "parameters": [ { "name": "body", @@ -131,7 +152,11 @@ }, "/auth/logout": { "post": { - "description": "", + "tags": [ + "Admin Authentication" + ], + "summary": "Terminate admin session", + "description": "Destroys the current session and clears the sid cookie.", "responses": { "204": { "description": "No Content" @@ -144,7 +169,11 @@ }, "/auth/csrf-token": { "get": { - "description": "", + "tags": [ + "Admin Authentication" + ], + "summary": "Fetch CSRF token", + "description": "Returns a CSRF token for the active admin session (session required).", "responses": { "200": { "description": "OK" @@ -157,7 +186,11 @@ }, "/auth/change-password": { "post": { - "description": "", + "tags": [ + "Admin Authentication" + ], + "summary": "Change admin password", + "description": "Allows a logged-in admin to rotate their password (CSRF protected).", "parameters": [ { "name": "x-csrf-token", @@ -297,25 +330,40 @@ }, "/api/download/{id}": { "get": { + "tags": [ + "Download" + ], + "summary": "Download original image", "description": "", "parameters": [ { "name": "id", "in": "path", "required": true, - "type": "string" + "type": "string", + "description": "Filename of the uploaded image" } ], "responses": { - "default": { - "description": "" + "200": { + "description": "Binary image response" + }, + "404": { + "description": "File not found" } } } }, "/api/upload/batch": { "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": [ { "name": "body", @@ -341,57 +389,98 @@ ], "responses": { "200": { - "description": "OK" + "description": "Batch upload successful (returns management token)" }, "400": { - "description": "Bad Request" + "description": "Missing files or workshop consent" }, "500": { - "description": "Internal Server Error" + "description": "Unexpected server error" } } } }, "/api/groups": { "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": { "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": { - "description": "Internal Server Error" + "description": "Server error" } } } }, "/api/groups/{groupId}": { "get": { + "tags": [ + "Public Groups" + ], + "summary": "Get approved group by ID", "description": "", "parameters": [ { "name": "groupId", "in": "path", "required": true, - "type": "string" + "type": "string", + "description": "Public groupId (e.g. cTV24Yn-a)" } ], "responses": { "200": { - "description": "OK" + "description": "Group payload (images + metadata)" }, "404": { - "description": "Not Found" + "description": "Group not found or not approved" }, "500": { - "description": "Internal Server Error" + "description": "Server error" } } } }, "/api/social-media/platforms": { "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": { "200": { "description": "OK" @@ -2483,13 +2572,18 @@ }, "/api/admin/{groupId}/reorder": { "put": { + "tags": [ + "Admin - Groups Moderation" + ], + "summary": "Reorder images within a group", "description": "", "parameters": [ { "name": "groupId", "in": "path", "required": true, - "type": "string" + "type": "string", + "description": "Admin groupId" }, { "name": "body", @@ -2506,19 +2600,19 @@ ], "responses": { "200": { - "description": "OK" + "description": "Order updated successfully" }, "400": { - "description": "Bad Request" + "description": "Validation error" }, "403": { "description": "Forbidden" }, "404": { - "description": "Not Found" + "description": "Group not found" }, "500": { - "description": "Internal Server Error" + "description": "Internal server error" } } } diff --git a/backend/src/routes/auth.js b/backend/src/routes/auth.js index 432415a..e919b93 100644 --- a/backend/src/routes/auth.js +++ b/backend/src/routes/auth.js @@ -5,6 +5,11 @@ const { requireAdminAuth } = require('../middlewares/auth'); const { requireCsrf } = require('../middlewares/csrf'); 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 { const needsSetup = await AdminAuthService.needsInitialSetup(); 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) => { + /* + #swagger.tags = ['Admin Authentication'] + #swagger.summary = 'Complete initial admin setup' + #swagger.description = 'Creates the very first admin account and immediately starts a session.' + */ try { const { username, password } = req.body || {}; if (!username || !password) { @@ -67,6 +77,11 @@ router.post('/setup/initial-admin', 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 { const { username, password } = req.body || {}; if (!username || !password) { @@ -100,6 +115,11 @@ router.post('/login', 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 { await AdminAuthService.destroySession(req); res.clearCookie('sid'); @@ -111,6 +131,11 @@ router.post('/logout', async (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) { 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) => { + /* + #swagger.tags = ['Admin Authentication'] + #swagger.summary = 'Change admin password' + #swagger.description = 'Allows a logged-in admin to rotate their password (CSRF protected).' + */ try { const { currentPassword, newPassword } = req.body || {}; if (!currentPassword || !newPassword) { diff --git a/backend/src/routes/batchUpload.js b/backend/src/routes/batchUpload.js index 55baa3d..35ec22a 100644 --- a/backend/src/routes/batchUpload.js +++ b/backend/src/routes/batchUpload.js @@ -84,6 +84,15 @@ const router = Router(); */ // Batch-Upload für mehrere Bilder 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 { // Überprüfe ob Dateien hochgeladen wurden if (!req.files || !req.files.images) { diff --git a/backend/src/routes/download.js b/backend/src/routes/download.js index de6f6bd..aab15fb 100644 --- a/backend/src/routes/download.js +++ b/backend/src/routes/download.js @@ -31,6 +31,18 @@ const router = Router(); * description: File not found */ 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); res.download(filePath); }); diff --git a/backend/src/routes/groups.js b/backend/src/routes/groups.js index 2153715..ec7f903 100644 --- a/backend/src/routes/groups.js +++ b/backend/src/routes/groups.js @@ -4,54 +4,21 @@ const MigrationService = require('../services/MigrationService'); 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) 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 { // Auto-Migration beim ersten Zugriff 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) 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 { const { groupId } = req.params; const group = await GroupRepository.getGroupById(groupId); diff --git a/backend/src/routes/reorder.js b/backend/src/routes/reorder.js index 0e13a61..b2bef2e 100644 --- a/backend/src/routes/reorder.js +++ b/backend/src/routes/reorder.js @@ -70,6 +70,20 @@ router.use(requireCsrf); * description: Server error during reordering */ 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 { const { groupId } = req.params; const { imageIds } = req.body; diff --git a/backend/src/routes/socialMedia.js b/backend/src/routes/socialMedia.js index 3e8bb75..c85c8b3 100644 --- a/backend/src/routes/socialMedia.js +++ b/backend/src/routes/socialMedia.js @@ -8,6 +8,11 @@ const router = express.Router(); * Public endpoint: list active social media platforms for consent selection */ 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 { const socialMediaRepo = new SocialMediaRepository(dbManager); const platforms = await socialMediaRepo.getActivePlatforms(); diff --git a/docker/prod/backend/Dockerfile b/docker/prod/backend/Dockerfile index bda5a98..454ff6b 100644 --- a/docker/prod/backend/Dockerfile +++ b/docker/prod/backend/Dockerfile @@ -15,7 +15,7 @@ RUN npm install --production COPY backend/src ./src # Copy production environment configuration -COPY docker/prod/backend/config/.env ./.env +#COPY docker/prod/backend/config/.env ./.env # Create data directories for file storage RUN mkdir -p src/data/images src/data/previews src/data/groups diff --git a/docker/prod/docker-compose.yml b/docker/prod/docker-compose.yml index f6693cf..147f911 100644 --- a/docker/prod/docker-compose.yml +++ b/docker/prod/docker-compose.yml @@ -34,8 +34,9 @@ services: networks: - prod-internal environment: + - REMOVE_IMAGES=false - NODE_ENV=production - - ADMIN_SESSION_SECRET=${ADMIN_SESSION_SECRET} + - ADMIN_SESSION_SECRET=MvFhivVIPIXvSGvWGfGOiQCkUJrmUsjWQTNGUgnSmtpsGHQlKruTBEBZgbVvOHHr - ADMIN_SESSION_DIR=/usr/src/app/src/data/sessions networks: