From 6dd0ecaceb0c22a710233ae8fd261a35016aff6f Mon Sep 17 00:00:00 2001 From: Francesco Lorenzo D'Amico Date: Fri, 11 Sep 2026 13:38:55 +0200 Subject: [PATCH] Add the page-content API endpoint for saving and file uploads --- .../Api/V1/PageContentController.cs | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 BewerbungWebseite/Controllers/Api/V1/PageContentController.cs diff --git a/BewerbungWebseite/Controllers/Api/V1/PageContentController.cs b/BewerbungWebseite/Controllers/Api/V1/PageContentController.cs new file mode 100644 index 0000000..8f70ff1 --- /dev/null +++ b/BewerbungWebseite/Controllers/Api/V1/PageContentController.cs @@ -0,0 +1,111 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using BewerbungsSeite.Models.SiteContent; +using BewerbungsSeite.Services; + +namespace BewerbungsSeite.Controllers.Api.V1; + +/// +/// Schreibzugriff auf den Seiteninhalt. Nur fuer angemeldete Administratoren: +/// die Konvention AuthorizeFolder("/Admin") gilt nur fuer Razor Pages, deshalb +/// steht das [Authorize] hier ausdruecklich am Controller. +/// +[Authorize] +[AutoValidateAntiforgeryToken] +[ApiController] +[Route("api/v1/[controller]")] +public class PageContentController( + IPageContentService pageContentService, + IWebHostEnvironment environment, + ILogger logger) : ControllerBase +{ + private static readonly string[] AllowedImageExtensions = [".jpg", ".jpeg", ".png", ".webp", ".gif"]; + private static readonly string[] AllowedDocumentExtensions = [".pdf"]; + + private const long MaxImageBytes = 5 * 1024 * 1024; + private const long MaxDocumentBytes = 20 * 1024 * 1024; + + /// Schreibt den kompletten Seiteninhalt zurueck. + [HttpPut] + public async Task Put([FromBody] PageContent content) + { + if (content is null) + { + return BadRequest(new { error = "Kein Inhalt übermittelt." }); + } + + await pageContentService.SaveAsync(content, HttpContext.RequestAborted); + return Ok(new { success = true, savedAt = DateTime.UtcNow }); + } + + /// + /// Nimmt ein Bild entgegen, legt es unter wwwroot/uploads/images ab und + /// liefert den Pfad zurueck, der anschliessend im JSON gespeichert wird. + /// + [HttpPost("images")] + [RequestSizeLimit(MaxImageBytes)] + public Task UploadImage(IFormFile? file) => + StoreAsync(file, "images", AllowedImageExtensions, MaxImageBytes, + requiredContentTypePrefix: "image/", + wrongTypeMessage: "Erlaubt sind JPG, PNG, WEBP und GIF.", + tooLargeMessage: "Die Datei ist grösser als 5 MB."); + + /// + /// Nimmt ein Dokument entgegen (PDF), legt es unter wwwroot/uploads/documents + /// ab und liefert den Pfad zurueck. + /// + [HttpPost("documents")] + [RequestSizeLimit(MaxDocumentBytes)] + public Task UploadDocument(IFormFile? file) => + StoreAsync(file, "documents", AllowedDocumentExtensions, MaxDocumentBytes, + requiredContentTypePrefix: null, + wrongTypeMessage: "Erlaubt sind nur PDF-Dateien.", + tooLargeMessage: "Die Datei ist grösser als 20 MB."); + + private async Task StoreAsync( + IFormFile? file, + string folder, + string[] allowedExtensions, + long maxBytes, + string? requiredContentTypePrefix, + string wrongTypeMessage, + string tooLargeMessage) + { + if (file is null || file.Length == 0) + { + return BadRequest(new { error = "Keine Datei übermittelt." }); + } + + if (file.Length > maxBytes) + { + return BadRequest(new { error = tooLargeMessage }); + } + + var extension = Path.GetExtension(file.FileName).ToLowerInvariant(); + if (!allowedExtensions.Contains(extension)) + { + return BadRequest(new { error = wrongTypeMessage }); + } + + if (requiredContentTypePrefix is not null && + !file.ContentType.StartsWith(requiredContentTypePrefix, StringComparison.OrdinalIgnoreCase)) + { + return BadRequest(new { error = wrongTypeMessage }); + } + + // Der Dateiname wird selbst vergeben; der Name aus dem Browser wird nie uebernommen. + var fileName = $"{Guid.NewGuid():N}{extension}"; + var uploadDirectory = Path.Combine(environment.WebRootPath, "uploads", folder); + Directory.CreateDirectory(uploadDirectory); + + var targetPath = Path.Combine(uploadDirectory, fileName); + await using (var stream = System.IO.File.Create(targetPath)) + { + await file.CopyToAsync(stream, HttpContext.RequestAborted); + } + + logger.LogInformation("Datei hochgeladen: {Folder}/{FileName}", folder, fileName); + + return Ok(new { url = $"/uploads/{folder}/{fileName}" }); + } +}