From 086e5a867d2ba975dad312ea9f16f0d5a170c377 Mon Sep 17 00:00:00 2001 From: Domonkos <162434141+domonkosszer@users.noreply.github.com> Date: Tue, 27 Jan 2026 18:32:29 +0100 Subject: [PATCH] basic login implementation at localhost:3000//login --- .DS_Store | Bin 0 -> 6148 bytes .idea/AndroidProjectSystem.xml | 6 + .idea/misc.xml | 3 + apps/.DS_Store | Bin 0 -> 6148 bytes apps/public-web/ARCHITECTURE.md | 143 ++++++++++++++---- apps/public-web/app/(site)/about/page.tsx | 1 + apps/public-web/app/(site)/admin/page.tsx | 38 +++++ apps/public-web/app/(site)/login/page.tsx | 7 + apps/public-web/cookies.txt | 4 + apps/workspace-api/data/voyage-db.lock.db | 6 + apps/workspace-api/data/voyage-db.mv.db | Bin 45056 -> 49152 bytes .../workspace/config/SecurityConfig.java | 23 ++- .../{ => controller}/HealthController.java | 2 +- .../controller/LoginRedirectController.java | 16 ++ .../workspace/controller/MeController.java | 19 +++ data/voyage-db.mv.db | Bin 0 -> 40960 bytes 16 files changed, 234 insertions(+), 34 deletions(-) create mode 100644 .DS_Store create mode 100644 .idea/AndroidProjectSystem.xml create mode 100644 apps/.DS_Store create mode 100644 apps/public-web/app/(site)/admin/page.tsx create mode 100644 apps/public-web/app/(site)/login/page.tsx create mode 100644 apps/public-web/cookies.txt create mode 100644 apps/workspace-api/data/voyage-db.lock.db rename apps/workspace-api/src/main/java/com/voyage/workspace/{ => controller}/HealthController.java (86%) create mode 100644 apps/workspace-api/src/main/java/com/voyage/workspace/controller/LoginRedirectController.java create mode 100644 apps/workspace-api/src/main/java/com/voyage/workspace/controller/MeController.java create mode 100644 data/voyage-db.mv.db diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..e57816e58f4e7c2bab80922777c82ab683e436f3 GIT binary patch literal 6148 zcmeHKPfrs;6n|4HZb4*$7BFhEp%)W~Qlp~r;Id#0@c>~7mH?LBcBm`MPTk#7kdXAM zU%(IG2QcyCRX>0py?XEqc=3#H{uC_LqejfWX6E;1-g`6ie!H{N0RUpvD+K@@05mKd z(>+-ILb#om4oRt=ZbZVznDbfL^_h!pK42mV0|^8FHUoU`ron+4{-P86*FxLXLm?uG zBR{th)zm(J=hl2LiYF&OlT=swO!rw$(|Waa`w45amS3kOy+6Wq=gKTt2;cEXDf%y_ZL{?#U2Y**2L^F z-JT0J8P3R8Fkj=QXqko)VnOa4R7EB ze1fm=9S%t!875<7oJ^3LWQv$%nk8met7Y)V863X!3RGL)z*22nQ$BE!*cD*vWL84g6ujN_=8 ziMpW>b@3pEa0g;aOzMPzgn@Pjx@1%5=l^}&!@s%zw+AJygn@*C|B3;Up0#H!Oi7)s z9m(;t*1)oXg$ws + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index ade1ecd..79e7899 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,6 +1,9 @@ + + + diff --git a/apps/.DS_Store b/apps/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..f788cefb0f044fa996c864fb58ff3f8592f40e39 GIT binary patch literal 6148 zcmeH~ze)o^5XQgJ6af)ZTJ8%d_y$Lag|#nW5-r5QNknX~^Id#N{pOG2aa!0|$Tu+i z?as~Y=6=OZ7Jx0EZq9)PfH~b2FFs7opSw@&pdv=4bB{+nV2v$qaXQKVKH#;-ct(#s z?nm`&?sa#$>$jU-zhRdD#8>*+TNr0D5fA|p5CIVof!`DG-b)ar_1S5R8XhjpnMML-0`1P*h#@cw^6|Ka{WF49T_MBrZ$u-WQrwd5;RZ=JlH_u59k srhgc7Bb`IEVrsNvF1!_=e93D*=ku;~NR4vFqnxTg0?tJy0)Ii^9j|8`Qvd(} literal 0 HcmV?d00001 diff --git a/apps/public-web/ARCHITECTURE.md b/apps/public-web/ARCHITECTURE.md index 64d6054..e17715d 100644 --- a/apps/public-web/ARCHITECTURE.md +++ b/apps/public-web/ARCHITECTURE.md @@ -7,7 +7,7 @@ This document describes the architecture of the VOYAGE public blog, including folder structure, routing, data flow, and rendering logic. A new developer should be able to understand how blog posts are loaded, -renderouten funktionieren, and where to extend the system after reading this file. +render routes work, and where to extend the system after reading this file. High-Level Overview @@ -49,11 +49,13 @@ apps/public-web/ │ │ ├── page.tsx → Homepage │ │ ├── about/ │ │ │ └── page.tsx → Static About page -│ │ └── blog/ -│ │ ├── layout.tsx → Blog-specific layout (optional) -│ │ ├── page.tsx → Blog index (list of posts) -│ │ └── [slug]/ -│ │ └── page.tsx → Dynamic blog post page +│ │ ├── blog/ +│ │ │ ├── layout.tsx → Blog-specific layout (optional) +│ │ │ ├── page.tsx → Blog index (list of posts) +│ │ │ └── [slug]/ +│ │ │ └── page.tsx → Dynamic blog post page +│ │ └── admin/ +│ │ └── page.tsx → Admin-only page (protected) │ │ │ ├── global.css → Global styles │ └── layout.tsx → Root layout @@ -93,6 +95,9 @@ Static routes: Dynamic routes: - /blog/[slug] → app/(site)/blog/[slug]/page.tsx +Admin route: +- /admin → app/(site)/admin/page.tsx (protected) + The `[slug]` directory defines a dynamic route parameter that is passed to the page component as `params.slug`. @@ -169,35 +174,110 @@ public/blog/visuals/ - Referenced in MDX or page components +Admin Authentication & Security +-------------------------------- + +The blog includes an **admin-only section** used for internal tools +(e.g. editor preview, drafts, future CMS features). + +Authentication is **not handled by Next.js**, but delegated to the +existing Spring Boot backend (`workspace-api`). + +Key principles: +- Public blog remains fully accessible without login +- Admin routes require a valid backend session +- Session-based authentication (JSESSIONID) +- No JWT, no duplicate auth system + + +Admin Route Protection (Frontend) +--------------------------------- + +Route: +- /admin → app/(site)/admin/page.tsx + +Protection strategy: +- Implemented as an **async Server Component guard** +- On each request: + - Calls backend endpoint `/api/me` + - Forwards cookies manually + - If response is 401 → redirect to /login + - If response is 200 → render admin UI + +Important detail: +- Server-side fetch must forward cookies explicitly +- credentials: "include" is NOT sufficient in Server Components + + +Login Flow (End-to-End) +---------------------- + +1. User navigates to: + http://localhost:3000/admin + +2. Admin page fetches: + GET http://localhost:8080/api/me + +3. If not authenticated: + - Backend returns 401 + - Next.js redirects to /login (frontend route) + +4. Frontend /login page redirects to backend: + GET http://localhost:8080/login-redirect?redirect=http://localhost:3000/admin + +5. Backend: + - Stores redirect target in session + - Redirects to /login (without query params) + +6. Spring Security default login page is shown + +7. User submits credentials + +8. On successful login: + - Custom successHandler reads redirect from session + - User is redirected to: + http://localhost:3000/admin + +9. Admin page loads successfully + + +Backend Endpoints Involved +------------------------- + +/api/me +- Returns current authenticated user +- 200 → logged in +- 401 → not authenticated +- Never redirects (API-safe) + +/login +- Spring Security default login page +- HTML form-based login + +/login-redirect +- Helper endpoint +- Stores redirect target in session +- Avoids unsupported query params on /login + + Why This Architecture --------------------- -- Clear separation of concerns -- File-based routing (no manual routing config) -- Content-driven architecture -- Easy to add new posts -- Scales well for future features: - - Tags - - Categories - - RSS feeds - - Pagination - - Search +- Single source of truth for authentication (Spring) +- No duplicate auth logic in frontend +- Clean separation: + - Public content → no auth + - Admin tools → backend session +- Works with SSR and Server Components +- Production-ready pattern for multi-app monorepos -How to Add a New Blog Post -------------------------- -1. Create a new MDX file in: - content/posts/ - -2. Use a unique filename: - YYYY-MM-DD-your-title.mdx - -3. Add frontmatter metadata (title, date, etc.) - -4. Optionally add images to: - public/blog/visuals/ - -5. The post is automatically available at: - /blog/your-title +How to Extend (Future) +---------------------- +- Admin editor UI +- Draft / preview mode +- Role-based admin features +- CMS integration +- Protected preview links Current Status @@ -206,4 +286,5 @@ Current Status - Dynamic blog slugs working - Shared public layout integrated - MDX content loading stable +- Admin auth flow stable and tested - Ready for styling, animations, and feature extensions \ No newline at end of file diff --git a/apps/public-web/app/(site)/about/page.tsx b/apps/public-web/app/(site)/about/page.tsx index 6487fc9..d9a8a24 100644 --- a/apps/public-web/app/(site)/about/page.tsx +++ b/apps/public-web/app/(site)/about/page.tsx @@ -1,6 +1,7 @@ import { TopBar, BackLink } from "../../../components/shell/TopBar"; export default function AboutPage() { + // @ts-ignore return (
} /> 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 3839747d5be4572867c07b26abf51237950b9605..50493b26652b3e1e7a8861d6d1052eff7b7c1196 100644 GIT binary patch literal 49152 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 literal 45056 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 JiK-T~O 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 0000000000000000000000000000000000000000..201fa75a2fc7f8efb10b37a5d1aebfe64fc6f24b GIT binary patch literal 40960 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 APyhe` literal 0 HcmV?d00001