2 Commits

Author SHA1 Message Date
AliceDarkest 83929f24ba Merge pull request 'Profilbild-Upload (413+Skalierung) und CSRF' (#3) from fix/upload-csrf into main
mindflux-ci / build (push) Successful in 27s
2026-06-26 13:24:32 +00:00
AliceDarkest fc98e5aad1 Profilbild-Upload (413 + Auto-Skalierung) und CSRF aktivieren
- application.properties: multipart max-file/request-size auf 10MB
  (nginx client_max_body_size muss serverseitig passend gesetzt sein)
- ProfileController: hochgeladene Profilbilder werden serverseitig auf
  max. 400px herunterskaliert und als JPEG normalisiert gespeichert
  (statt der Originaldatei roh); nicht lesbare Bilder werden ignoriert
- SecurityConfig: CSRF aktiviert; Spiele-JSON-APIs (/api/games,
  /api/techtacular, /api/mindmatch) ausgenommen; GET-Logout erlaubt,
  da die Logout-Links in den Templates GET sind

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 15:22:25 +02:00
3 changed files with 55 additions and 19 deletions
@@ -10,6 +10,7 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.*;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@Configuration
public class SecurityConfig {
@@ -31,10 +32,16 @@ public class SecurityConfig {
.permitAll()
)
.logout(logout -> logout
// Logout-Links in den Templates sind GET -> GET-Logout erlauben, sonst 404/Fehler unter CSRF
.logoutRequestMatcher(new AntPathRequestMatcher("/logout", "GET"))
.logoutSuccessUrl("/?logout")
.permitAll()
)
.csrf(csrf -> csrf.disable()); // optional deaktivieren bei Custom-Logik/API-Nutzung
.csrf(csrf -> csrf
// Spiele-APIs werden per fetch (application/json) ohne Token aufgerufen -> ausnehmen.
// Reine JSON-Endpunkte sind nicht über klassische HTML-Formulare CSRF-ausnutzbar.
.ignoringRequestMatchers("/api/games/**", "/api/techtacular/**", "/api/mindmatch/**")
);
return http.build();
}
@@ -14,6 +14,11 @@ import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import javax.imageio.ImageIO;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -99,26 +104,42 @@ public class ProfileController {
if (file != null && !file.isEmpty()) {
try {
BufferedImage original = ImageIO.read(file.getInputStream());
if (original == null) {
// Keine lesbare Bilddatei -> Upload ignorieren, restliches Profil trotzdem speichern.
System.out.println("Profilbild-Upload ignoriert: keine lesbare Bilddatei.");
} else {
// Auf max. Kantenlänge herunterskalieren (kein Hochskalieren).
final int MAX_DIM = 400;
int w = original.getWidth();
int h = original.getHeight();
double scale = Math.min(1.0, (double) MAX_DIM / Math.max(w, h));
int nw = Math.max(1, (int) Math.round(w * scale));
int nh = Math.max(1, (int) Math.round(h * scale));
BufferedImage scaled = new BufferedImage(nw, nh, BufferedImage.TYPE_INT_RGB);
Graphics2D g = scaled.createGraphics();
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g.setColor(Color.WHITE); // JPEG kennt kein Alpha -> Transparenz auf weißem Grund
g.fillRect(0, 0, nw, nh);
g.drawImage(original, 0, 0, nw, nh, null);
g.dispose();
// Altes Profilbild löschen (falls vorhanden)
if (existingUser.getProfilePicture() != null && !existingUser.getProfilePicture().isEmpty()) {
Path oldFilePath = Paths.get(System.getProperty("user.dir"), "uploads", "profiles", existingUser.getProfilePicture());
if (Files.exists(oldFilePath)) {
Files.delete(oldFilePath);
}
Files.deleteIfExists(oldFilePath);
}
// Neues Profilbild speichern
String fileExtension = getFileExtension(file.getOriginalFilename());
String fileName = UUID.randomUUID() + fileExtension;
// Normalisiert als JPEG speichern
String fileName = UUID.randomUUID() + ".jpg";
Path uploadPath = Paths.get(UPLOAD_DIR);
Files.createDirectories(uploadPath);
Path filePath = uploadPath.resolve(fileName);
file.transferTo(filePath.toFile());
ImageIO.write(scaled, "jpg", uploadPath.resolve(fileName).toFile());
existingUser.setProfilePicture(fileName);
}
} catch (IOException e) {
e.printStackTrace();
}
@@ -41,3 +41,11 @@ spring.output.ansi.enabled=ALWAYS
# Öffentliche Basis-URL, z. B. für künftig erzeugte absolute Links.
mindflux.public-url=${MINDFLUX_PUBLIC_URL:http://localhost:8282}
# =======================
# Datei-Uploads (Profilbilder)
# =======================
# Hinweis: Der nginx-Reverse-Proxy muss client_max_body_size passend gesetzt haben,
# sonst greift schon dort ein 413, bevor die App den Upload sieht.
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=10MB