} />
diff --git a/apps/public-web/app/(site)/admin/page.tsx b/apps/public-web/app/(site)/admin/page.tsx
new file mode 100644
index 0000000..cc9485e
--- /dev/null
+++ b/apps/public-web/app/(site)/admin/page.tsx
@@ -0,0 +1,38 @@
+import { redirect } from "next/navigation";
+import { cookies } from "next/headers";
+
+export default async function AdminPage() {
+ const cookieHeader = (await cookies())
+ .getAll()
+ .map(({ name, value }) => `${name}=${value}`)
+ .join("; ");
+
+ const res = await fetch(`${process.env.BACKEND_URL}/api/me`, {
+ headers: { cookie: cookieHeader },
+ cache: "no-store",
+ redirect: "manual", // IMPORTANT: don’t follow to /login
+ });
+
+ // Spring might answer 302 to /login when unauthenticated
+ if (res.status === 401 || res.status === 302) redirect("/login");
+
+ if (!res.ok) {
+ const text = await res.text();
+ throw new Error(`Failed to load session: ${res.status} ${text.slice(0, 200)}`);
+ }
+
+ const contentType = res.headers.get("content-type") ?? "";
+ if (!contentType.includes("application/json")) {
+ // we got HTML (login page) or something else
+ redirect("/login");
+ }
+
+ const me = await res.json();
+
+ return (
+
+ Admin
+ {JSON.stringify(me, null, 2)}
+
+ );
+}
\ No newline at end of file
diff --git a/apps/public-web/app/(site)/login/page.tsx b/apps/public-web/app/(site)/login/page.tsx
new file mode 100644
index 0000000..d27eef4
--- /dev/null
+++ b/apps/public-web/app/(site)/login/page.tsx
@@ -0,0 +1,7 @@
+import { redirect } from "next/navigation";
+
+export default function LoginPage() {
+ const backend = process.env.BACKEND_URL!;
+ const returnTo = encodeURIComponent("http://localhost:3000/admin");
+ redirect(`${backend}/login-redirect?redirect=${returnTo}`);
+}
\ No newline at end of file
diff --git a/apps/public-web/cookies.txt b/apps/public-web/cookies.txt
new file mode 100644
index 0000000..c31d989
--- /dev/null
+++ b/apps/public-web/cookies.txt
@@ -0,0 +1,4 @@
+# Netscape HTTP Cookie File
+# https://curl.se/docs/http-cookies.html
+# This file was generated by libcurl! Edit at your own risk.
+
diff --git a/apps/workspace-api/data/voyage-db.lock.db b/apps/workspace-api/data/voyage-db.lock.db
new file mode 100644
index 0000000..25537e8
--- /dev/null
+++ b/apps/workspace-api/data/voyage-db.lock.db
@@ -0,0 +1,6 @@
+#FileLock
+#Tue Jan 27 18:25:29 CET 2026
+hostName=macbook-air-von-melika.fritz.box
+id=19c007d24ac0d06a483d34679bbb29160d6d33ed895
+method=file
+server=192.168.178.68\:58736
diff --git a/apps/workspace-api/data/voyage-db.mv.db b/apps/workspace-api/data/voyage-db.mv.db
index 3839747..50493b2 100644
Binary files a/apps/workspace-api/data/voyage-db.mv.db and b/apps/workspace-api/data/voyage-db.mv.db differ
diff --git a/apps/workspace-api/src/main/java/com/voyage/workspace/config/SecurityConfig.java b/apps/workspace-api/src/main/java/com/voyage/workspace/config/SecurityConfig.java
index dc4cbda..4f531a6 100644
--- a/apps/workspace-api/src/main/java/com/voyage/workspace/config/SecurityConfig.java
+++ b/apps/workspace-api/src/main/java/com/voyage/workspace/config/SecurityConfig.java
@@ -2,10 +2,12 @@ package com.voyage.workspace.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpStatus;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.HttpStatusEntryPoint;
@Configuration
public class SecurityConfig {
@@ -14,7 +16,7 @@ public class SecurityConfig {
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(auth -> auth
- .requestMatchers("/health", "/error").permitAll()
+ .requestMatchers("/health", "/error", "/login", "/login-redirect").permitAll()
.requestMatchers("/h2-console/**").permitAll()
// Admin-only user management
@@ -27,8 +29,25 @@ public class SecurityConfig {
.anyRequest().authenticated()
);
+ // IMPORTANT: For API calls, return 401 instead of redirecting to /login (HTML)
+ http.exceptionHandling(ex -> ex
+ .defaultAuthenticationEntryPointFor(
+ new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED),
+ request -> request.getRequestURI() != null && request.getRequestURI().startsWith("/api/")
+ )
+ );
+
http.formLogin(form -> form
- .defaultSuccessUrl("http://localhost:3000/", true)
+ .successHandler((request, response, authentication) -> {
+ String target = request.getParameter("redirect");
+ if (target == null) {
+ Object saved = request.getSession().getAttribute("LOGIN_REDIRECT");
+ if (saved != null) target = saved.toString();
+ }
+
+ boolean allowed = target != null && target.startsWith("http://localhost:3000/");
+ response.sendRedirect(allowed ? target : "http://localhost:3000/admin");
+ })
);
http.logout(logout -> logout
diff --git a/apps/workspace-api/src/main/java/com/voyage/workspace/HealthController.java b/apps/workspace-api/src/main/java/com/voyage/workspace/controller/HealthController.java
similarity index 86%
rename from apps/workspace-api/src/main/java/com/voyage/workspace/HealthController.java
rename to apps/workspace-api/src/main/java/com/voyage/workspace/controller/HealthController.java
index b2d486e..487a1cf 100644
--- a/apps/workspace-api/src/main/java/com/voyage/workspace/HealthController.java
+++ b/apps/workspace-api/src/main/java/com/voyage/workspace/controller/HealthController.java
@@ -1,4 +1,4 @@
-package com.voyage.workspace;
+package com.voyage.workspace.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
diff --git a/apps/workspace-api/src/main/java/com/voyage/workspace/controller/LoginRedirectController.java b/apps/workspace-api/src/main/java/com/voyage/workspace/controller/LoginRedirectController.java
new file mode 100644
index 0000000..a104862
--- /dev/null
+++ b/apps/workspace-api/src/main/java/com/voyage/workspace/controller/LoginRedirectController.java
@@ -0,0 +1,16 @@
+package com.voyage.workspace.controller;
+
+import jakarta.servlet.http.HttpSession;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+
+@Controller
+public class LoginRedirectController {
+
+ @GetMapping("/login-redirect")
+ public String loginRedirect(@RequestParam("redirect") String redirect, HttpSession session) {
+ session.setAttribute("LOGIN_REDIRECT", redirect);
+ return "redirect:/login";
+ }
+}
\ No newline at end of file
diff --git a/apps/workspace-api/src/main/java/com/voyage/workspace/controller/MeController.java b/apps/workspace-api/src/main/java/com/voyage/workspace/controller/MeController.java
new file mode 100644
index 0000000..5d33aa0
--- /dev/null
+++ b/apps/workspace-api/src/main/java/com/voyage/workspace/controller/MeController.java
@@ -0,0 +1,19 @@
+package com.voyage.workspace.controller;
+
+import org.springframework.security.core.Authentication;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.Map;
+
+@RestController
+public class MeController {
+
+ @GetMapping("/api/me")
+ public Map
me(Authentication auth) {
+ return Map.of(
+ "name", auth.getName(),
+ "authorities", auth.getAuthorities()
+ );
+ }
+}
\ No newline at end of file
diff --git a/data/voyage-db.mv.db b/data/voyage-db.mv.db
new file mode 100644
index 0000000..201fa75
Binary files /dev/null and b/data/voyage-db.mv.db differ