diff --git a/job-tracker-ui/src/App.tsx b/job-tracker-ui/src/App.tsx index 15e49cc..4afeee7 100644 --- a/job-tracker-ui/src/App.tsx +++ b/job-tracker-ui/src/App.tsx @@ -203,7 +203,7 @@ function Shell({ jobPageSize, setJobPageSize, jobColumns, setJobColumns, themeMo }, []); if (requireAuth === null || !authResolved) return Loading...; - if (requireAuth && !me) return ; + if (requireAuth && !me) return ; const pageTitle = titleFor(path, t); const breadcrumbs = breadcrumbsFor(path, t); diff --git a/job-tracker-ui/src/LandingPage.authRedirect.test.tsx b/job-tracker-ui/src/LandingPage.authRedirect.test.tsx new file mode 100644 index 0000000..323ff77 --- /dev/null +++ b/job-tracker-ui/src/LandingPage.authRedirect.test.tsx @@ -0,0 +1,49 @@ +import React from "react"; +import "@testing-library/jest-dom"; +import { render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { MemoryRouter, Route, Routes, useLocation } from "react-router-dom"; +import { api } from "./api"; +import LandingPage from "./pages/LandingPage"; + +jest.mock("./api", () => ({ + api: { + get: jest.fn(), + post: jest.fn(), + interceptors: { request: { use: jest.fn() }, response: { use: jest.fn() } }, + }, +})); + +const mockedApi = api as jest.Mocked; + +function LoginStub() { + const location = useLocation() as { state?: { from?: string } }; + return
login page, from={location.state?.from ?? "none"}
; +} + +// Regression check for the auth-guard fix: a protected route bounces an +// unauthenticated visitor to "/" with `state.from` set to the page they +// wanted. The home page must forward that state to /login so sign-in +// returns them to the originally-requested page instead of dropping them +// on /jobs. +describe("LandingPage forwards deep-link intent to /login", () => { + it("preserves location.state.from through the Sign in CTA", async () => { + mockedApi.get.mockRejectedValueOnce(new Error("401")); // /auth/me: not signed in + + render( + + + } /> + } /> + + , + ); + + await waitFor(() => expect(mockedApi.get).toHaveBeenCalledWith("/auth/me")); + + const signInButtons = await screen.findAllByText("Sign in", { selector: "button" }); + await userEvent.click(signInButtons[0]); + + expect(await screen.findByText("login page, from=/jobs/42")).toBeInTheDocument(); + }); +}); diff --git a/job-tracker-ui/src/pages/LandingPage.tsx b/job-tracker-ui/src/pages/LandingPage.tsx index 70f674d..7029a26 100644 --- a/job-tracker-ui/src/pages/LandingPage.tsx +++ b/job-tracker-ui/src/pages/LandingPage.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react"; import { Box, Button, Container, Stack, Typography } from "@mui/material"; import { alpha } from "@mui/material/styles"; -import { useNavigate } from "react-router-dom"; +import { useLocation, useNavigate } from "react-router-dom"; import DashboardIcon from "@mui/icons-material/SpaceDashboardOutlined"; import AlarmIcon from "@mui/icons-material/NotificationsActiveOutlined"; @@ -42,6 +42,7 @@ const PRICING: { name: string; price: string; cadence: string; highlight: boolea export default function LandingPage() { const navigate = useNavigate(); + const location = useLocation() as { state?: { from?: string } }; const [checking, setChecking] = useState(true); // If the visitor already has a session, send them straight into the app. @@ -54,6 +55,11 @@ export default function LandingPage() { return () => { active = false; }; }, [navigate]); + // A protected route redirects unauthenticated visitors here with the page they + // wanted in location state; forward it to /login so sign-in returns them there + // instead of dropping them on /jobs. + const goToLogin = () => navigate("/login", { state: location.state }); + if (checking) { return ( @@ -78,7 +84,7 @@ export default function LandingPage() { JobTrack - @@ -100,7 +106,7 @@ export default function LandingPage() { right job — all in one focused workspace. Assistive, never autonomous: you approve every draft. - @@ -248,7 +254,7 @@ export default function LandingPage() { © {new Date().getFullYear()} JobTrack — a focused workspace for the modern job search. - +