Skip to content

Commit 095c89c

Browse files
committed
fix auth
1 parent dd6f15c commit 095c89c

2 files changed

Lines changed: 87 additions & 51 deletions

File tree

src/components/Login.tsx

Lines changed: 80 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { useLocation, useNavigate } from "react-router";
1111
import ThemeSwitch from "./ThemeSwitch";
1212

1313
export function Login() {
14-
const { loginBasic, loading, error, setAuth, setUser } = useAuth();
14+
const { loginBasic, loading, error, setAuth, setUser, authFetch } = useAuth();
1515
const navigate = useNavigate();
1616
const location = useLocation();
1717

@@ -20,18 +20,72 @@ export function Login() {
2020
const [password, setPassword] = useState("");
2121
const [useSso, setUseSso] = useState(false);
2222

23-
// Refs for focus trap
2423
const serverRef = useRef<HTMLInputElement | null>(null);
2524
const userRef = useRef<HTMLInputElement | null>(null);
2625
const passRef = useRef<HTMLInputElement | null>(null);
2726
const submitRef = useRef<HTMLButtonElement | null>(null);
2827

28+
// Ref for next token refresh; if needed, can alternatively export/set via context
29+
const nextTokenRefreshRef = useRef<Date | null>(null);
30+
2931
useEffect(() => {
30-
// Auto-focus server field on mount
3132
serverRef.current?.focus();
3233
}, []);
3334

34-
// Effect to check query params for tokens and log in if found
35+
const normalizeServer = useCallback((raw: string) => {
36+
if (!raw) return raw;
37+
let s = raw.trim();
38+
if (!/^https?:\/\//i.test(s)) s = `https://${s}`;
39+
s = s.replace(/\/+$/, "");
40+
return s;
41+
}, []);
42+
43+
// Helper to compute next token refresh (same as in AuthContext)
44+
function base64UrlDecode(segment: string): string {
45+
try {
46+
let s = segment.replace(/-/g, "+").replace(/_/g, "/");
47+
const pad = s.length % 4;
48+
if (pad) s += "=".repeat(4 - pad);
49+
if (typeof atob === "function") return atob(s);
50+
if (typeof window === "undefined") return "";
51+
// @ts-ignore
52+
return atob(s);
53+
} catch {
54+
return "";
55+
}
56+
}
57+
function parseJwt(token: string) {
58+
const parts = token.split(".");
59+
if (parts.length !== 3) return null;
60+
try {
61+
const json = base64UrlDecode(parts[1]);
62+
return JSON.parse(json);
63+
} catch {
64+
return null;
65+
}
66+
}
67+
function computeNextTokenRefresh(token: string): Date {
68+
const payload = parseJwt(token);
69+
if (!payload) return new Date();
70+
const exp = payload.Exp ?? payload.exp;
71+
const creation =
72+
payload.Creation ?? payload.creation ?? payload.iat ?? payload.Iat;
73+
const now = Date.now();
74+
if (
75+
typeof exp === "number" &&
76+
typeof creation === "number" &&
77+
exp > creation
78+
) {
79+
const lifetimeSec = exp - creation;
80+
return new Date(now + lifetimeSec * 1000);
81+
}
82+
if (typeof exp === "number") {
83+
return new Date(exp * 1000);
84+
}
85+
return new Date();
86+
}
87+
88+
// Effect to handle login via tokens in URL query params
3589
useEffect(() => {
3690
const params = new URLSearchParams(location.search);
3791
const accessToken = params.get("access_token");
@@ -43,46 +97,40 @@ export function Login() {
4397
const normalizedServer =
4498
normalizeServer(server) || window.location.origin;
4599

46-
// Set tokens in auth context and localStorage
100+
setServer(normalizedServer);
101+
localStorage.setItem("app_server_url", normalizedServer);
102+
47103
setAuth({ access_token: accessToken, refresh_token: refreshToken });
48104
localStorage.setItem("app_refresh_token", refreshToken);
49105

50-
// Fetch current user
51-
const meResponse = await fetch(`${normalizedServer}/api/users/me`, {
52-
headers: {
53-
Authorization: `Bearer ${accessToken}`,
54-
Accept: "application/json",
55-
},
56-
});
57-
58-
if (!meResponse.ok) {
59-
throw new Error("Failed to fetch user data");
60-
}
106+
nextTokenRefreshRef.current = computeNextTokenRefresh(accessToken);
61107

108+
// Use authFetch from context to support token refresh etc.
109+
const meResponse = await authFetch(
110+
`${normalizedServer}/api/users/me`,
111+
);
112+
if (!meResponse.ok) throw new Error("Failed to fetch user");
62113
const me = await meResponse.json();
63114
setUser(me);
64115

65-
// Redirect to library page
66116
navigate("/library", { replace: true });
67117

68-
// Clear query params from URL to prevent reuse
118+
// Clear query parameters from URL
69119
window.history.replaceState({}, document.title, location.pathname);
70-
} catch (err) {
71-
// Could optionally handle errors here
120+
} catch {
121+
// Optionally handle error: e.g. clearing tokens, showing message
72122
}
73123
})();
74124
}
75-
}, [location.search, navigate, server, setAuth, setUser]);
76-
77-
const normalizeServer = useCallback((raw: string) => {
78-
if (!raw) return raw;
79-
let s = raw.trim();
80-
if (!/^https?:\/\//i.test(s)) {
81-
s = `https://${s}`;
82-
}
83-
s = s.replace(/\/+$/, "");
84-
return s;
85-
}, []);
125+
}, [
126+
location.search,
127+
navigate,
128+
server,
129+
setAuth,
130+
setUser,
131+
authFetch,
132+
normalizeServer,
133+
]);
86134

87135
const onSubmit = async (e: FormEvent) => {
88136
e.preventDefault();
@@ -108,10 +156,7 @@ export function Login() {
108156
submitRef.current,
109157
].filter(
110158
(el): el is HTMLInputElement | HTMLButtonElement =>
111-
!!el &&
112-
(typeof (el as any).disabled === "boolean"
113-
? !(el as any).disabled
114-
: true),
159+
!!el && (!(el as any).disabled ?? true),
115160
);
116161
if (elems.length === 0) return;
117162
const active = document.activeElement as HTMLElement | null;

src/context/AuthContext.tsx

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -30,26 +30,17 @@ interface AuthContextValue {
3030
authFetch: (input: string, init?: RequestInit) => Promise<Response>;
3131
refreshCurrentUser: () => Promise<GamevaultUser | null>;
3232

33-
// New setters exposed for external updates
3433
setAuth: (auth: AuthTokens | null) => void;
3534
setUser: (user: GamevaultUser | null) => void;
35+
36+
nextTokenRefreshRef: React.MutableRefObject<Date | null>;
3637
}
3738

3839
const AuthContext = createContext<AuthContextValue | null>(null);
3940

4041
const REFRESH_KEY = "app_refresh_token";
4142
const SERVER_KEY = "app_server_url";
4243

43-
interface InternalJwtPayload {
44-
exp?: number;
45-
Exp?: number;
46-
iat?: number;
47-
Iat?: number;
48-
creation?: number;
49-
Creation?: number;
50-
[k: string]: any;
51-
}
52-
5344
function base64UrlDecode(segment: string): string {
5445
try {
5546
let s = segment.replace(/-/g, "+").replace(/_/g, "/");
@@ -63,7 +54,8 @@ function base64UrlDecode(segment: string): string {
6354
return "";
6455
}
6556
}
66-
function parseJwt(token: string): InternalJwtPayload | null {
57+
58+
function parseJwt(token: string) {
6759
const parts = token.split(".");
6860
if (parts.length !== 3) return null;
6961
try {
@@ -73,6 +65,7 @@ function parseJwt(token: string): InternalJwtPayload | null {
7365
return null;
7466
}
7567
}
68+
7669
function computeNextTokenRefresh(token: string): Date {
7770
const payload = parseJwt(token);
7871
if (!payload) return new Date();
@@ -106,7 +99,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
10699
const nextTokenRefreshRef = useRef<Date | null>(null);
107100
const refreshInFlightRef = useRef<Promise<void> | null>(null);
108101

109-
// Maintain refs and state sync
102+
// Sync React state and refs
110103
const setAuth = useCallback((newAuth: AuthTokens | null) => {
111104
authRef.current = newAuth;
112105
_setAuth(newAuth);
@@ -220,7 +213,6 @@ export function AuthProvider({ children }: { children: ReactNode }) {
220213
serverRef.current = (server || "").replace(/\/+$/, "");
221214
setServerUrl(serverRef.current);
222215
localStorage.setItem(SERVER_KEY, serverRef.current);
223-
224216
try {
225217
if (!server || !username || !password)
226218
throw new Error("All fields are required.");
@@ -309,11 +301,10 @@ export function AuthProvider({ children }: { children: ReactNode }) {
309301
logout,
310302
authFetch,
311303
refreshCurrentUser,
312-
313304
setAuth,
314305
setUser,
306+
nextTokenRefreshRef,
315307
};
316-
317308
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
318309
}
319310

0 commit comments

Comments
 (0)