git add .
git commit -m "feat(workspace): inventory overview, auth, persistent h2 db, users & roles - add inventory overview endpoint (aggregated stock per SKU) - implement inventory movements (production, sale, return, adjustment) - add session-based login with Spring Security (form login) - protect /api/** endpoints - add logout with session invalidation - introduce user roles (ADMIN, WORKER) and active flag - seed admin user in dev profile - switch from H2 in-memory to persistent H2 file database - enable H2 console for development - add Next.js workspace UI (inventory table, filters, movements) - support active/inactive products and users (soft disable)"
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
VOYAGE – Nächste Projektschritte (Workspace + Login + Webblog) – UPDATE 2026-01-20
|
VOYAGE – Nächste Projektschritte (Workspace + Login + Webblog) – UPDATE 2026-01-21
|
||||||
===============================================================================
|
===============================================================================
|
||||||
|
|
||||||
Ziel
|
Ziel
|
||||||
@@ -13,36 +13,48 @@ Bereits implementiert:
|
|||||||
- Products
|
- Products
|
||||||
- SKUs (Varianten)
|
- SKUs (Varianten)
|
||||||
- InventoryMovements (Bestandsbuchungen)
|
- InventoryMovements (Bestandsbuchungen)
|
||||||
- Current Stock pro SKU
|
- Current Stock pro SKU (Aggregation)
|
||||||
- Inventory Overview Endpoint (aggregierte Bestandsübersicht)
|
- Inventory Overview Endpoint (GET /api/inventory/overview)
|
||||||
|
- Workspace UI (Next.js, minimal)
|
||||||
- Login-System (Spring Security, Form Login, Session)
|
- Login-System (Spring Security, Form Login, Session)
|
||||||
- /api/** ist geschützt (nur eingeloggt)
|
- /api/** ist geschützt (nur eingeloggt)
|
||||||
- Login per Browser (/login) und per curl (Cookie/JSESSIONID) funktioniert
|
- Login per Browser (/login) und per curl (Cookie/JSESSIONID) funktioniert
|
||||||
|
- Logout invalidiert Session + löscht JSESSIONID
|
||||||
|
|
||||||
Damit ist das Domain-Fundament + Zugriffsschutz + UI-taugliche API abgeschlossen.
|
Damit ist das Domain-Fundament + Zugriffsschutz + UI-taugliche API abgeschlossen.
|
||||||
|
|
||||||
|
|
||||||
Wichtiger Hinweis zur Datenbank (AKTUELLER ZUSTAND)
|
Wichtiger Hinweis zur Datenbank (AKTUELLER ZUSTAND)
|
||||||
---------------------------------------------------
|
---------------------------------------------------
|
||||||
Die Anwendung verwendet derzeit eine H2 In-Memory Datenbank:
|
Die Anwendung verwendet NICHT MEHR eine In-Memory Datenbank.
|
||||||
|
|
||||||
- JDBC URL: jdbc:h2:mem:...
|
Aktueller Stand:
|
||||||
- Die Datenbank liegt ausschließlich im RAM
|
- H2 File-basierte Datenbank (persistent)
|
||||||
- BEI JEDEM APP-RESTART WERDEN ALLE DATEN GELÖSCHT
|
- JDBC URL: jdbc:h2:file:./data/voyage-db;AUTO_SERVER=TRUE
|
||||||
|
- Daten liegen auf der Festplatte (data/voyage-db.mv.db)
|
||||||
Dieser Zustand ist bewusst gewählt und sinnvoll für:
|
- KEIN Datenverlust bei App-Restart
|
||||||
- frühe Entwicklung
|
|
||||||
- schnelles Testen
|
|
||||||
- Debugging
|
|
||||||
- Fokus auf Domain-Logik statt Persistenz
|
|
||||||
|
|
||||||
Wichtig:
|
Wichtig:
|
||||||
- Products, SKUs und InventoryMovements müssen nach jedem Neustart neu angelegt werden
|
- Die DB überlebt Neustarts
|
||||||
- IDs (productId, skuId) ändern sich nach jedem Restart
|
- IDs (productId, skuId, userId) bleiben stabil
|
||||||
|
- AUTO_SERVER erlaubt parallelen Zugriff (App + H2 Console)
|
||||||
|
|
||||||
Geplanter späterer Schritt:
|
SEHR WICHTIG:
|
||||||
- Umstellung auf persistente DB (z. B. H2 file, Postgres)
|
- Das H2 Passwort wird beim ERSTEN Start festgelegt
|
||||||
- Einführung von Migrations (Flyway)
|
- Danach MUSS es gleich bleiben
|
||||||
|
- Falsches Passwort → App startet NICHT (Error 28000)
|
||||||
|
|
||||||
|
Empfohlene Config:
|
||||||
|
- Username: sa
|
||||||
|
- Password: leer (oder exakt das beim ersten Start gesetzte)
|
||||||
|
|
||||||
|
H2 Console:
|
||||||
|
- URL: http://localhost:8080/h2-console
|
||||||
|
- JDBC URL: jdbc:h2:file:./data/voyage-db;AUTO_SERVER=TRUE
|
||||||
|
- User: sa
|
||||||
|
- Password: (leer oder wie initial gesetzt)
|
||||||
|
|
||||||
|
H2 Login ≠ Workspace Login (komplett getrennt)
|
||||||
|
|
||||||
|
|
||||||
Grundprinzip
|
Grundprinzip
|
||||||
@@ -58,13 +70,14 @@ Empfohlene Architektur
|
|||||||
----------------------
|
----------------------
|
||||||
|
|
||||||
1) workspace-api (Spring Boot)
|
1) workspace-api (Spring Boot)
|
||||||
- Products / SKUs / Inventory (besteht)
|
- Products / SKUs / Inventory
|
||||||
- Aggregierte Inventory-Übersicht (besteht)
|
- Inventory Overview (aggregiert)
|
||||||
- Auth (besteht)
|
- Auth & Rollen
|
||||||
|
- persistente DB
|
||||||
- später Orders / Drops
|
- später Orders / Drops
|
||||||
|
|
||||||
2) workspace-ui (Webinterface)
|
2) workspace-ui (Webinterface)
|
||||||
- internes Dashboard
|
- internes Dashboard (Next.js)
|
||||||
- spricht mit workspace-api
|
- spricht mit workspace-api
|
||||||
- nutzt Session-Cookies (Browser)
|
- nutzt Session-Cookies (Browser)
|
||||||
|
|
||||||
@@ -100,10 +113,17 @@ Implementiert:
|
|||||||
- Spring Security
|
- Spring Security
|
||||||
- Form Login
|
- Form Login
|
||||||
- Session-basiert (kein JWT)
|
- Session-basiert (kein JWT)
|
||||||
|
- Logout mit Session-Invalidierung
|
||||||
|
- Redirect zurück ins Workspace UI
|
||||||
|
|
||||||
Offene Endpoints:
|
User-Modell:
|
||||||
- /health (öffentlich)
|
- UserAccount in DB
|
||||||
- /h2-console (nur Development)
|
- Rollen: ADMIN / WORKER
|
||||||
|
- active Flag (true / false)
|
||||||
|
|
||||||
|
Inactive User:
|
||||||
|
- kann sich NICHT einloggen
|
||||||
|
- bleibt für Historie erhalten
|
||||||
|
|
||||||
|
|
||||||
SCHRITT 2: Workspace-taugliche Endpoints
|
SCHRITT 2: Workspace-taugliche Endpoints
|
||||||
@@ -121,63 +141,97 @@ Liefert pro SKU:
|
|||||||
- Aktueller Bestand (SUM der InventoryMovements)
|
- Aktueller Bestand (SUM der InventoryMovements)
|
||||||
|
|
||||||
Bedeutung:
|
Bedeutung:
|
||||||
- Ein einzelner Request ist ausreichend für das gesamte Inventory-Dashboard
|
- Ein Request = komplette Inventory Tabelle
|
||||||
- Backend liefert UI-fertige Daten (keine Aggregation im Frontend nötig)
|
- Keine Aggregation im Frontend nötig
|
||||||
|
- Grundlage für UI, Low-Stock, Drops
|
||||||
Grundlage für:
|
|
||||||
- Inventory Tabelle
|
|
||||||
- Low-Stock Checks
|
|
||||||
- Drop-Planung
|
|
||||||
- Workspace UI
|
|
||||||
|
|
||||||
|
|
||||||
SCHRITT 3: Workspace UI (minimal starten)
|
SCHRITT 3: Workspace UI (minimal starten)
|
||||||
-----------------------------------------
|
-----------------------------------------
|
||||||
Status: ALS NÄCHSTES ✅
|
Status: ERLEDIGT (MINIMAL) ✅
|
||||||
|
|
||||||
Nicht zu groß denken. Start mit EINER Seite:
|
|
||||||
|
|
||||||
|
Implementiert:
|
||||||
- Inventory Übersicht
|
- Inventory Übersicht
|
||||||
- Tabelle mit allen SKUs + currentStock
|
- Tabelle mit allen SKUs + currentStock
|
||||||
- Suche & Filter (Name, SKU, Kategorie)
|
- Suche & Filter (Name, SKU, Kategorie)
|
||||||
- Button für Inventory Movements (+ / -)
|
- Inventory Movements (+ / -)
|
||||||
|
- SALE erzeugt negatives Delta
|
||||||
|
- PRODUCTION positives Delta
|
||||||
|
|
||||||
Ergebnis:
|
Ergebnis:
|
||||||
Ein echtes internes Tool, das täglich nutzbar ist.
|
Ein echtes internes Tool, sofort nutzbar.
|
||||||
|
|
||||||
|
|
||||||
SCHRITT 4: Public Webblog (parallel, aber getrennt)
|
SCHRITT 4: Active / Inactive statt Löschen
|
||||||
|
------------------------------------------
|
||||||
|
Status: IMPLEMENTIERT / EMPFOHLEN ✅
|
||||||
|
|
||||||
|
Grundregel:
|
||||||
|
- NIEMALS löschen, wenn Historie existiert
|
||||||
|
|
||||||
|
Umsetzung:
|
||||||
|
- Products und SKUs haben active Flag
|
||||||
|
- active = false:
|
||||||
|
- wird im Workspace ausgeblendet
|
||||||
|
- bleibt in DB für Historie
|
||||||
|
|
||||||
|
Beispiel:
|
||||||
|
- Caps werden nicht mehr verkauft
|
||||||
|
- Product/SKUs auf inactive setzen
|
||||||
|
- Inventory bleibt korrekt nachvollziehbar
|
||||||
|
|
||||||
|
|
||||||
|
SCHRITT 5: Worker-User
|
||||||
|
---------------------
|
||||||
|
Status: IMPLEMENTIERT (Basis) ✅
|
||||||
|
|
||||||
|
User-Typen:
|
||||||
|
- ADMIN: volle Rechte, Userverwaltung
|
||||||
|
- WORKER: Inventory sehen & buchen
|
||||||
|
|
||||||
|
Worker anlegen:
|
||||||
|
- per Admin Endpoint oder DB
|
||||||
|
- Passwort wird BCrypt-gehasht
|
||||||
|
- active = true notwendig
|
||||||
|
|
||||||
|
Worker deaktivieren:
|
||||||
|
- active = false
|
||||||
|
- Login gesperrt
|
||||||
|
- Daten bleiben erhalten
|
||||||
|
|
||||||
|
|
||||||
|
SCHRITT 6: Public Webblog (parallel, aber getrennt)
|
||||||
---------------------------------------------------
|
---------------------------------------------------
|
||||||
Status: PARALLEL MÖGLICH ✅
|
Status: PARALLEL MÖGLICH ✅
|
||||||
|
|
||||||
Empfohlener Start:
|
Empfohlener Start:
|
||||||
- Eigenes Frontend (public-web)
|
- Eigenes Frontend (public-web)
|
||||||
- Content als Markdown-Dateien im Repo
|
- Content als Markdown im Repo
|
||||||
- Git als CMS
|
- Git als CMS
|
||||||
|
|
||||||
Vorteile:
|
Vorteile:
|
||||||
- Kein Admin-UI nötig
|
- Kein Admin-UI nötig
|
||||||
- Keine Auth
|
- Keine Auth
|
||||||
- Sehr schneller Content-Workflow
|
- Sehr schneller Workflow
|
||||||
|
|
||||||
|
|
||||||
Warum diese Reihenfolge sinnvoll ist
|
Warum diese Reihenfolge sinnvoll ist
|
||||||
------------------------------------
|
------------------------------------
|
||||||
- Workspace erzeugt operativen Wert
|
- Workspace erzeugt operativen Wert
|
||||||
- Login schützt eure Arbeit
|
- Login schützt eure Arbeit
|
||||||
- Inventory Overview macht das System direkt benutzbar
|
- Inventory Overview macht das System sofort nutzbar
|
||||||
- UI kann ohne weitere Backend-Arbeit gebaut werden
|
- UI basiert auf stabiler API
|
||||||
- Blog kann unabhängig wachsen
|
- Blog kann unabhängig wachsen
|
||||||
- Keine unnötige Komplexität zu früh
|
|
||||||
|
|
||||||
|
|
||||||
Langfristige Erweiterungen
|
Langfristige Erweiterungen
|
||||||
--------------------------
|
--------------------------
|
||||||
- Orders → automatische InventoryMovements
|
- Orders → automatische SALE Movements
|
||||||
- Drops → Production → initialer Bestand
|
- Drops → Production → initialer Bestand
|
||||||
- Automations (Low-Stock Alerts)
|
- Low-Stock Alerts
|
||||||
- Rollen (Admin / Viewer)
|
- Rollenfeingranularität
|
||||||
- Persistente Datenbank + Migrations
|
- Flyway Migrations
|
||||||
|
- Postgres als DB
|
||||||
- Public Shop (optional)
|
- Public Shop (optional)
|
||||||
|
|
||||||
|
|
||||||
@@ -187,14 +241,34 @@ Merksätze
|
|||||||
- Login früh einbauen (erledigt)
|
- Login früh einbauen (erledigt)
|
||||||
- Inventory niemals direkt ändern
|
- Inventory niemals direkt ändern
|
||||||
- SKUs sind die operative Wahrheit
|
- SKUs sind die operative Wahrheit
|
||||||
- In-Memory DB = flüchtig, aber perfekt für den Start
|
- Löschen ist fast immer falsch → inactive ist richtig
|
||||||
|
- H2 Passwort niemals ändern ohne DB Reset
|
||||||
|
|
||||||
|
|
||||||
Nächster konkreter Schritt
|
Nächster konkreter Schritt
|
||||||
--------------------------
|
--------------------------
|
||||||
Start von:
|
- Admin UI für User + Active/Inactive
|
||||||
- workspace-ui (Inventory Tabelle auf Basis von /api/inventory/overview)
|
- Deactivate-Flow für Products & SKUs
|
||||||
|
- Orders mit automatischen InventoryMovements
|
||||||
|
|
||||||
Danach:
|
Curls
|
||||||
- Orders + automatische Inventory-Abbuchung
|
-----------------
|
||||||
- Umstellung auf persistente DB, sobald Stabilität erreicht ist
|
curl -X POST http://localhost:8080/api/admin/users \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-b cookies.txt \
|
||||||
|
-d '{
|
||||||
|
"username": "admin2",
|
||||||
|
"password": "admin123!",
|
||||||
|
"role": "ADMIN",
|
||||||
|
"active": true
|
||||||
|
}'
|
||||||
|
|
||||||
|
curl -X POST http://localhost:8080/api/admin/users \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-b cookies.txt \
|
||||||
|
-d '{
|
||||||
|
"username": "worker1",
|
||||||
|
"password": "worker123!",
|
||||||
|
"role": "WORKER",
|
||||||
|
"active": true
|
||||||
|
}'
|
||||||
@@ -2,4 +2,4 @@
|
|||||||
# https://curl.se/docs/http-cookies.html
|
# https://curl.se/docs/http-cookies.html
|
||||||
# This file was generated by libcurl! Edit at your own risk.
|
# This file was generated by libcurl! Edit at your own risk.
|
||||||
|
|
||||||
#HttpOnly_localhost FALSE / FALSE 0 JSESSIONID 1D763BECEF84ECD1D335BF07E96D3691
|
#HttpOnly_localhost FALSE / FALSE 0 JSESSIONID 1464F869FB095717F548FC25F22453F1
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package com.voyage.workspace.auth;
|
||||||
|
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/admin/users")
|
||||||
|
public class AdminUsersController {
|
||||||
|
|
||||||
|
private final UserAccountRepository repo;
|
||||||
|
private final PasswordEncoder encoder;
|
||||||
|
|
||||||
|
public AdminUsersController(UserAccountRepository repo, PasswordEncoder encoder) {
|
||||||
|
this.repo = repo;
|
||||||
|
this.encoder = encoder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public record CreateUserRequest(String username, String password, UserAccount.Role role) {}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ResponseEntity<?> create(@RequestBody CreateUserRequest req) {
|
||||||
|
if (req.username() == null || req.username().isBlank()) return ResponseEntity.badRequest().body("username required");
|
||||||
|
if (req.password() == null || req.password().length() < 6) return ResponseEntity.badRequest().body("password min 6 chars");
|
||||||
|
if (repo.findByUsername(req.username()).isPresent()) return ResponseEntity.badRequest().body("username exists");
|
||||||
|
|
||||||
|
UserAccount u = new UserAccount();
|
||||||
|
u.setUsername(req.username());
|
||||||
|
u.setPasswordHash(encoder.encode(req.password()));
|
||||||
|
u.setRole(req.role() == null ? UserAccount.Role.WORKER : req.role());
|
||||||
|
u.setActive(true);
|
||||||
|
|
||||||
|
repo.save(u);
|
||||||
|
return ResponseEntity.ok("created");
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/disable")
|
||||||
|
public ResponseEntity<?> disable(@PathVariable Long id) {
|
||||||
|
var u = repo.findById(id).orElse(null);
|
||||||
|
if (u == null) return ResponseEntity.badRequest().body("unknown id");
|
||||||
|
u.setActive(false);
|
||||||
|
repo.save(u);
|
||||||
|
return ResponseEntity.ok("disabled");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package com.voyage.workspace.auth;
|
||||||
|
|
||||||
|
import org.springframework.boot.CommandLineRunner;
|
||||||
|
import org.springframework.context.annotation.Profile;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@Profile("dev")
|
||||||
|
public class AuthSeed implements CommandLineRunner {
|
||||||
|
|
||||||
|
private final UserAccountRepository repo;
|
||||||
|
private final PasswordEncoder encoder;
|
||||||
|
|
||||||
|
public AuthSeed(UserAccountRepository repo, PasswordEncoder encoder) {
|
||||||
|
this.repo = repo;
|
||||||
|
this.encoder = encoder;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run(String... args) {
|
||||||
|
repo.findByUsername("admin").orElseGet(() -> {
|
||||||
|
UserAccount u = new UserAccount();
|
||||||
|
u.setUsername("admin");
|
||||||
|
u.setPasswordHash(encoder.encode("admin123!"));
|
||||||
|
u.setRole(UserAccount.Role.ADMIN);
|
||||||
|
u.setActive(true);
|
||||||
|
return repo.save(u);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package com.voyage.workspace.auth;
|
||||||
|
|
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
|
import org.springframework.security.core.userdetails.*;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class DbUserDetailsService implements UserDetailsService {
|
||||||
|
|
||||||
|
private final UserAccountRepository repo;
|
||||||
|
|
||||||
|
public DbUserDetailsService(UserAccountRepository repo) {
|
||||||
|
this.repo = repo;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||||
|
UserAccount u = repo.findByUsername(username)
|
||||||
|
.orElseThrow(() -> new UsernameNotFoundException("Unknown user: " + username));
|
||||||
|
|
||||||
|
if (!u.isActive()) {
|
||||||
|
throw new UsernameNotFoundException("User disabled: " + username);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spring wants ROLE_ prefix for roles
|
||||||
|
var auth = List.of(new SimpleGrantedAuthority("ROLE_" + u.getRole().name()));
|
||||||
|
|
||||||
|
return new org.springframework.security.core.userdetails.User(
|
||||||
|
u.getUsername(),
|
||||||
|
u.getPasswordHash(),
|
||||||
|
auth
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package com.voyage.workspace.auth;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "user_account")
|
||||||
|
public class UserAccount {
|
||||||
|
|
||||||
|
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(nullable = false, unique = true, length = 64)
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
@Column(nullable = false, length = 120)
|
||||||
|
private String passwordHash;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(nullable = false, length = 20)
|
||||||
|
private Role role;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private boolean active = true;
|
||||||
|
|
||||||
|
public enum Role { ADMIN, WORKER }
|
||||||
|
|
||||||
|
// getters/setters
|
||||||
|
public Long getId() { return id; }
|
||||||
|
|
||||||
|
public String getUsername() { return username; }
|
||||||
|
public void setUsername(String username) { this.username = username; }
|
||||||
|
|
||||||
|
public String getPasswordHash() { return passwordHash; }
|
||||||
|
public void setPasswordHash(String passwordHash) { this.passwordHash = passwordHash; }
|
||||||
|
|
||||||
|
public Role getRole() { return role; }
|
||||||
|
public void setRole(Role role) { this.role = role; }
|
||||||
|
|
||||||
|
public boolean isActive() { return active; }
|
||||||
|
public void setActive(boolean active) { this.active = active; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.voyage.workspace.auth;
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public interface UserAccountRepository extends JpaRepository<UserAccount, Long> {
|
||||||
|
Optional<UserAccount> findByUsername(String username);
|
||||||
|
}
|
||||||
@@ -2,15 +2,9 @@ package com.voyage.workspace.config;
|
|||||||
|
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.context.annotation.Profile;
|
|
||||||
import org.springframework.security.config.Customizer;
|
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
import org.springframework.security.core.userdetails.User;
|
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
|
||||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
|
||||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@@ -19,35 +13,42 @@ public class SecurityConfig {
|
|||||||
@Bean
|
@Bean
|
||||||
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||||
|
|
||||||
http
|
http.authorizeHttpRequests(auth -> auth
|
||||||
// Autorisierung
|
.requestMatchers("/health", "/error").permitAll()
|
||||||
.authorizeHttpRequests(auth -> auth
|
.requestMatchers("/h2-console/**").permitAll()
|
||||||
// offen
|
|
||||||
.requestMatchers("/health", "/error").permitAll()
|
|
||||||
|
|
||||||
// H2-Console nur DEV (siehe extra Bean unten), hier erstmal erlaubt,
|
// Admin-only user management
|
||||||
// wird durch Profile gesteuert
|
.requestMatchers("/api/admin/**").hasRole("ADMIN")
|
||||||
.requestMatchers("/h2-console/**").permitAll()
|
|
||||||
|
|
||||||
// alles unter /api nur eingeloggt
|
// Everything else under /api requires login
|
||||||
.requestMatchers("/api/**").authenticated()
|
.requestMatchers("/api/**").authenticated()
|
||||||
|
|
||||||
// sonst auch nur eingeloggt (Workspace nicht public)
|
// Workspace should not be public
|
||||||
.anyRequest().authenticated()
|
.anyRequest().authenticated()
|
||||||
)
|
);
|
||||||
|
|
||||||
// Form Login (Session)
|
http.formLogin(form -> form
|
||||||
.formLogin(Customizer.withDefaults())
|
.defaultSuccessUrl("http://localhost:3000/", true)
|
||||||
|
);
|
||||||
|
|
||||||
// Logout ok
|
http.logout(logout -> logout
|
||||||
.logout(Customizer.withDefaults());
|
.logoutUrl("/logout")
|
||||||
|
.invalidateHttpSession(true)
|
||||||
|
.deleteCookies("JSESSIONID")
|
||||||
|
.logoutSuccessUrl("http://localhost:3000/login")
|
||||||
|
.permitAll()
|
||||||
|
);
|
||||||
|
|
||||||
// Damit H2 Console im Browser funktioniert:
|
|
||||||
http.headers(headers -> headers.frameOptions(frame -> frame.sameOrigin()));
|
http.headers(headers -> headers.frameOptions(frame -> frame.sameOrigin()));
|
||||||
|
|
||||||
// CSRF: Für H2 Console + einfache curl Tests disable (für internes Tool OK zum Start).
|
// Dev/testing convenience: allow curl + UI without CSRF tokens
|
||||||
// Später kann man das feiner machen (nur für /api/** tokenbasiert etc.)
|
http.csrf(csrf -> csrf.ignoringRequestMatchers(
|
||||||
http.csrf(csrf -> csrf.ignoringRequestMatchers("/h2-console/**", "/login", "/api/**"));
|
"/h2-console/**",
|
||||||
|
"/login",
|
||||||
|
"/logout",
|
||||||
|
"/api/**"
|
||||||
|
));
|
||||||
|
|
||||||
return http.build();
|
return http.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,18 +56,4 @@ public class SecurityConfig {
|
|||||||
PasswordEncoder passwordEncoder() {
|
PasswordEncoder passwordEncoder() {
|
||||||
return new BCryptPasswordEncoder();
|
return new BCryptPasswordEncoder();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* DEV-User: später ersetzt ihr das durch DB-User / Admin Tabelle.
|
|
||||||
*/
|
|
||||||
@Bean
|
|
||||||
@Profile("dev")
|
|
||||||
UserDetailsService devUsers(PasswordEncoder encoder) {
|
|
||||||
UserDetails admin = User.withUsername("admin")
|
|
||||||
.password(encoder.encode("admin123!"))
|
|
||||||
.roles("ADMIN")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
return new InMemoryUserDetailsManager(admin);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,12 @@
|
|||||||
spring.application.name=workspace-api
|
spring.application.name=workspace-api
|
||||||
|
|
||||||
|
spring.profiles.active=dev
|
||||||
spring.h2.console.enabled=true
|
spring.h2.console.enabled=true
|
||||||
spring.datasource.url=jdbc:h2:mem:voyage;DB_CLOSE_DELAY=-1
|
|
||||||
spring.jpa.hibernate.ddl-auto=update
|
# PERSISTENT (file) DB instead of mem
|
||||||
spring.profiles.active=dev
|
spring.datasource.url=jdbc:h2:file:./data/voyage-db;AUTO_SERVER=TRUE
|
||||||
|
spring.datasource.driverClassName=org.h2.Driver
|
||||||
|
spring.datasource.username=sa
|
||||||
|
spring.datasource.password=admin
|
||||||
|
|
||||||
|
spring.jpa.hibernate.ddl-auto=update
|
||||||
Reference in New Issue
Block a user