From 8a489409fd149742094ee46844b02ac44918c104 Mon Sep 17 00:00:00 2001 From: Austin Bennett Date: Thu, 23 Jul 2026 12:34:31 -0500 Subject: [PATCH] Return to where you signed in from, and take motion 12.42.2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Post-login landed on the home page. Spring only remembers the pre-login location when it BOUNCES you off a protected page, but every route here is public — the SPA sends you to the identity provider itself — so nothing is saved and login defaults to '/'. AuthProvider now stashes the current path in sessionStorage before the redirect and navigates back once /api/me confirms the session. sessionStorage, not a query parameter: it survives the redirect chain, stays in this tab, and cannot be pointed at another site. Sign-in is only ever triggered from an /admin route, so that is exactly where the reader is returned. Also merges the motion 12.42.2 bump, which had stayed open on its own PR. --- frontend/package-lock.json | 20 ++++++++++---------- frontend/src/lib/auth.tsx | 29 ++++++++++++++++++++++++++--- 2 files changed, 36 insertions(+), 13 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 04a4a1c..5e0a68d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,7 +8,7 @@ "name": "itsthevine-frontend", "version": "0.1.0", "dependencies": { - "motion": "12.34.4", + "motion": "12.42.2", "react": "^19.2.7", "react-dom": "^19.2.7", "react-router-dom": "7.18.1" @@ -1847,13 +1847,13 @@ } }, "node_modules/framer-motion": { - "version": "12.34.4", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.34.4.tgz", - "integrity": "sha512-q1PwNhc1XJ3qYG7nc9+pEU5P3tnjB6Eh9vv5gGzy61nedDLB4+xk5peMCWhKM0Zn6sfhgunf/q9n0UgCoyKOBA==", + "version": "12.42.2", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.42.2.tgz", + "integrity": "sha512-5XY9luDiu0oHfHBjpDthFMh0ES+122w6p/papSJBweMkO8Sn+PW2QaEgRblQBpWFnuvZS5qvarpt/hO2pjGmnw==", "license": "MIT", "dependencies": { - "motion-dom": "^12.34.3", - "motion-utils": "^12.29.2", + "motion-dom": "^12.42.2", + "motion-utils": "^12.39.0", "tslib": "^2.4.0" }, "peerDependencies": { @@ -2301,12 +2301,12 @@ } }, "node_modules/motion": { - "version": "12.34.4", - "resolved": "https://registry.npmjs.org/motion/-/motion-12.34.4.tgz", - "integrity": "sha512-J0cuDNRymNzE0M2WY8CFcbQuprHBZwY+iqADKGLLe6kQUVP4kBQ2l7Z6gWK7Zfrt5Wgxs+kCojj4qu7I4wxBIw==", + "version": "12.42.2", + "resolved": "https://registry.npmjs.org/motion/-/motion-12.42.2.tgz", + "integrity": "sha512-Atvv11yUKIid41cVrRBDVX5m8tF8kNpExRSlbpt6APClhDjtwQssgFHhQzejxw7/7YYbjHSPKBVbHo05BuJT5Q==", "license": "MIT", "dependencies": { - "framer-motion": "^12.34.4", + "framer-motion": "^12.42.2", "tslib": "^2.4.0" }, "peerDependencies": { diff --git a/frontend/src/lib/auth.tsx b/frontend/src/lib/auth.tsx index 0bfd4c1..7e20477 100644 --- a/frontend/src/lib/auth.tsx +++ b/frontend/src/lib/auth.tsx @@ -1,4 +1,5 @@ import { createContext, useContext, useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; import type { ReactNode } from 'react'; import { fetchMe, type Me } from './api'; @@ -12,14 +13,31 @@ const AuthContext = createContext<{ me: Me; loading: boolean }>({ me: ANON, load * It has to be public: this runs on every page load, and if it required a login every anonymous * visitor would be bounced to Authentik just to read the menu. */ +/** Where the reader was when they clicked Sign in, so they can be put back afterwards. */ +const RETURN_TO = 'vine:post-login-path'; + export function AuthProvider({ children }: { children: ReactNode }) { const [state, setState] = useState<{ me: Me; loading: boolean }>({ me: ANON, loading: true }); + const navigate = useNavigate(); useEffect(() => { fetchMe() - .then((me) => setState({ me, loading: false })) + .then((me) => { + setState({ me, loading: false }); + if (!me.authenticated) return; + // Spring only remembers where you were if you were BOUNCED off a protected page. Every route + // here is public — the SPA sends you to the identity provider itself — so there is nothing + // saved and login lands on "/". Put the reader back where they started. + const back = sessionStorage.getItem(RETURN_TO); + if (back) { + sessionStorage.removeItem(RETURN_TO); + if (back !== window.location.pathname + window.location.search) { + navigate(back, { replace: true }); + } + } + }) .catch(() => setState({ me: ANON, loading: false })); - }, []); + }, [navigate]); return {children}; } @@ -27,4 +45,9 @@ export function AuthProvider({ children }: { children: ReactNode }) { export const useAuth = () => useContext(AuthContext); /** Full-page navigation, not fetch: the OIDC handshake is a redirect chain the browser must follow. */ -export const signIn = () => { window.location.href = '/oauth2/authorization/authentik'; }; +export const signIn = () => { + // sessionStorage rather than a query parameter: it survives the whole redirect chain, stays in this + // tab, and never becomes something an attacker can point at another site. + sessionStorage.setItem(RETURN_TO, window.location.pathname + window.location.search); + window.location.href = '/oauth2/authorization/authentik'; +};