Skip to content

Commit af29a20

Browse files
committed
Handle SSO redirect
1 parent fe988f9 commit af29a20

2 files changed

Lines changed: 83 additions & 1 deletion

File tree

src/components/Login.tsx

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import ThemeSwitch from "./ThemeSwitch";
1212
import { RegisterUserDtoFromJSON } from "../api";
1313

1414
export function Login() {
15-
const { loginBasic, loading, error } = useAuth();
15+
const { loginBasic, loginWithTokens, loading, error } = useAuth();
1616
const navigate = useNavigate();
1717
const [server, setServer] = useState(window.location.origin);
1818
const [username, setUsername] = useState("");
@@ -42,6 +42,50 @@ export function Login() {
4242
return s;
4343
}, []);
4444

45+
// Parse SSO redirect style: {server}/access_token=...&refresh_token=...
46+
useEffect(() => {
47+
// Detect tokens embedded after origin (no question mark) e.g. https://srv/access_token=XXX&refresh_token=YYY
48+
// Or possibly inside hash.
49+
try {
50+
const loc = window.location;
51+
const path = loc.pathname.startsWith("/")
52+
? loc.pathname.slice(1)
53+
: loc.pathname;
54+
const hash = loc.hash.startsWith("#") ? loc.hash.slice(1) : loc.hash;
55+
const candidate = path.includes("access_token=") ? path : hash;
56+
if (!candidate) return;
57+
if (!/access_token=/.test(candidate)) return;
58+
// Interpret as key=value pairs separated by & possibly with leading segment (e.g., access_token=...&refresh_token=...)
59+
const pairs = candidate.split("&").filter(Boolean);
60+
const data: Record<string, string> = {};
61+
for (const p of pairs) {
62+
const eq = p.indexOf("=");
63+
if (eq === -1) continue;
64+
const k = decodeURIComponent(p.slice(0, eq));
65+
const v = decodeURIComponent(p.slice(eq + 1));
66+
data[k] = v;
67+
}
68+
if (!data.access_token) return;
69+
// Determine server base: remove the trailing candidate part from URL if it's at pathname
70+
const base = window.location.origin;
71+
(async () => {
72+
try {
73+
await loginWithTokens(base, {
74+
access_token: data.access_token,
75+
refresh_token: data.refresh_token,
76+
});
77+
// Clean URL (remove tokens) then navigate
78+
window.history.replaceState({}, document.title, base + "/login");
79+
navigate("/library", { replace: true });
80+
} catch {
81+
// ignore - error shown via context
82+
}
83+
})();
84+
} catch {
85+
// ignore parsing errors
86+
}
87+
}, [loginWithTokens, navigate]);
88+
4589
const onSubmit = async (e: FormEvent) => {
4690
e.preventDefault();
4791
try {

src/context/AuthContext.tsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ interface AuthContextValue {
2626
loginBasic: (
2727
args: LoginArgs,
2828
) => Promise<{ auth: AuthTokens; user: GamevaultUser }>;
29+
/** Directly initialize auth state from already obtained tokens (e.g. SSO redirect). */
30+
loginWithTokens: (server: string, tokens: AuthTokens) => Promise<{ auth: AuthTokens; user: GamevaultUser }>;
2931
logout: () => void;
3032
authFetch: (input: string, init?: RequestInit) => Promise<Response>;
3133
refreshCurrentUser: () => Promise<GamevaultUser | null>;
@@ -232,6 +234,41 @@ export function AuthProvider({ children }: { children: ReactNode }) {
232234
[],
233235
);
234236

237+
const loginWithTokens = useCallback(
238+
async (server: string, tokens: AuthTokens) => {
239+
setError(null);
240+
setLoading(true);
241+
setUser(null);
242+
setAuth(null);
243+
authRef.current = null;
244+
nextTokenRefreshRef.current = null;
245+
serverRef.current = (server || "").replace(/\/+$/, "");
246+
setServerUrl(serverRef.current);
247+
localStorage.setItem(SERVER_KEY, serverRef.current);
248+
try {
249+
if (!tokens?.access_token) throw new Error("Missing access token");
250+
authRef.current = tokens;
251+
setAuth(tokens);
252+
if (tokens.refresh_token)
253+
localStorage.setItem(REFRESH_KEY, tokens.refresh_token);
254+
nextTokenRefreshRef.current = computeNextTokenRefresh(
255+
tokens.access_token,
256+
);
257+
const me = await fetchCurrentUser();
258+
setUser(me);
259+
return { auth: tokens, user: me };
260+
} catch (e) {
261+
const msg = e instanceof Error ? e.message : String(e);
262+
setError(msg);
263+
throw e;
264+
} finally {
265+
setLoading(false);
266+
setBootstrapping(false);
267+
}
268+
},
269+
[],
270+
);
271+
235272
useEffect(() => {
236273
(async () => {
237274
const storedRefresh = localStorage.getItem(REFRESH_KEY);
@@ -293,6 +330,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
293330
loading,
294331
bootstrapping,
295332
loginBasic,
333+
loginWithTokens,
296334
logout,
297335
authFetch,
298336
refreshCurrentUser,

0 commit comments

Comments
 (0)