Return to where you signed in from, and take motion 12.42.2
build-and-publish / build (push) Successful in 1m57s

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.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01XXKjx7FNyRVAjU8dgB5KhN
This commit is contained in:
2026-07-23 12:34:31 -05:00
co-authored by Claude Opus 4.8
parent e18b730f6b
commit 021dd0db8d
2 changed files with 36 additions and 13 deletions
+10 -10
View File
@@ -8,7 +8,7 @@
"name": "itsthevine-frontend", "name": "itsthevine-frontend",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"motion": "12.34.4", "motion": "12.42.2",
"react": "^19.2.7", "react": "^19.2.7",
"react-dom": "^19.2.7", "react-dom": "^19.2.7",
"react-router-dom": "7.18.1" "react-router-dom": "7.18.1"
@@ -1847,13 +1847,13 @@
} }
}, },
"node_modules/framer-motion": { "node_modules/framer-motion": {
"version": "12.34.4", "version": "12.42.2",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.34.4.tgz", "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.42.2.tgz",
"integrity": "sha512-q1PwNhc1XJ3qYG7nc9+pEU5P3tnjB6Eh9vv5gGzy61nedDLB4+xk5peMCWhKM0Zn6sfhgunf/q9n0UgCoyKOBA==", "integrity": "sha512-5XY9luDiu0oHfHBjpDthFMh0ES+122w6p/papSJBweMkO8Sn+PW2QaEgRblQBpWFnuvZS5qvarpt/hO2pjGmnw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"motion-dom": "^12.34.3", "motion-dom": "^12.42.2",
"motion-utils": "^12.29.2", "motion-utils": "^12.39.0",
"tslib": "^2.4.0" "tslib": "^2.4.0"
}, },
"peerDependencies": { "peerDependencies": {
@@ -2301,12 +2301,12 @@
} }
}, },
"node_modules/motion": { "node_modules/motion": {
"version": "12.34.4", "version": "12.42.2",
"resolved": "https://registry.npmjs.org/motion/-/motion-12.34.4.tgz", "resolved": "https://registry.npmjs.org/motion/-/motion-12.42.2.tgz",
"integrity": "sha512-J0cuDNRymNzE0M2WY8CFcbQuprHBZwY+iqADKGLLe6kQUVP4kBQ2l7Z6gWK7Zfrt5Wgxs+kCojj4qu7I4wxBIw==", "integrity": "sha512-Atvv11yUKIid41cVrRBDVX5m8tF8kNpExRSlbpt6APClhDjtwQssgFHhQzejxw7/7YYbjHSPKBVbHo05BuJT5Q==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"framer-motion": "^12.34.4", "framer-motion": "^12.42.2",
"tslib": "^2.4.0" "tslib": "^2.4.0"
}, },
"peerDependencies": { "peerDependencies": {
+26 -3
View File
@@ -1,4 +1,5 @@
import { createContext, useContext, useEffect, useState } from 'react'; import { createContext, useContext, useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { fetchMe, type Me } from './api'; 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 * 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. * 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 }) { export function AuthProvider({ children }: { children: ReactNode }) {
const [state, setState] = useState<{ me: Me; loading: boolean }>({ me: ANON, loading: true }); const [state, setState] = useState<{ me: Me; loading: boolean }>({ me: ANON, loading: true });
const navigate = useNavigate();
useEffect(() => { useEffect(() => {
fetchMe() 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 })); .catch(() => setState({ me: ANON, loading: false }));
}, []); }, [navigate]);
return <AuthContext.Provider value={state}>{children}</AuthContext.Provider>; return <AuthContext.Provider value={state}>{children}</AuthContext.Provider>;
} }
@@ -27,4 +45,9 @@ export function AuthProvider({ children }: { children: ReactNode }) {
export const useAuth = () => useContext(AuthContext); export const useAuth = () => useContext(AuthContext);
/** Full-page navigation, not fetch: the OIDC handshake is a redirect chain the browser must follow. */ /** 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';
};