larxius commited on
Commit
f6c5cad
Β·
verified Β·
1 Parent(s): a785406

Update frontend/src/components/AuthContext.jsx

Browse files
Files changed (1) hide show
  1. frontend/src/components/AuthContext.jsx +177 -172
frontend/src/components/AuthContext.jsx CHANGED
@@ -1,173 +1,178 @@
1
- import React, { createContext, useContext, useState, useEffect, useRef, useCallback } from 'react';
2
-
3
- const AuthContext = createContext(null);
4
-
5
- const TOKEN_KEY = 'wss_token';
6
- const REFRESH_KEY = 'wss_refresh_token';
7
-
8
- // Parse the expiry time from a JWT without any library
9
- function getTokenExpiry(token) {
10
- try {
11
- const payload = JSON.parse(atob(token.split('.')[1]));
12
- return payload.exp ? payload.exp * 1000 : null; // convert to ms
13
- } catch {
14
- return null;
15
- }
16
- }
17
-
18
- export const AuthProvider = ({ children }) => {
19
- const [user, setUser] = useState(null);
20
- const [token, setToken] = useState(localStorage.getItem(TOKEN_KEY));
21
- const [refreshToken, setRefreshToken] = useState(localStorage.getItem(REFRESH_KEY));
22
- const [loading, setLoading] = useState(true);
23
- const proactiveRefreshRef = useRef(null); // holds the setTimeout handle
24
-
25
- // ── Core logout β€” clear everything ───────────────────────────────────────
26
- const doLogout = useCallback(() => {
27
- if (proactiveRefreshRef.current) clearTimeout(proactiveRefreshRef.current);
28
- setToken(null);
29
- setRefreshToken(null);
30
- setUser(null);
31
- localStorage.removeItem(TOKEN_KEY);
32
- localStorage.removeItem(REFRESH_KEY);
33
- }, []);
34
-
35
- // ── Refresh access token (uses body refresh_token + HttpOnly cookie fallback)
36
- const refreshAccessToken = useCallback(async () => {
37
- const storedRefresh = localStorage.getItem(REFRESH_KEY);
38
- try {
39
- const res = await fetch('/api/auth/refresh', {
40
- method: 'POST',
41
- headers: { 'Content-Type': 'application/json' },
42
- credentials: 'include', // send HttpOnly cookie as fallback
43
- body: JSON.stringify({ refresh_token: storedRefresh || '' }),
44
- });
45
- if (res.ok) {
46
- const data = await res.json();
47
- setToken(data.access_token);
48
- localStorage.setItem(TOKEN_KEY, data.access_token);
49
- if (data.refresh_token) {
50
- setRefreshToken(data.refresh_token);
51
- localStorage.setItem(REFRESH_KEY, data.refresh_token);
52
- }
53
- return data.access_token;
54
- }
55
- // Refresh failed β€” do NOT auto-logout. Just return null and let callers decide.
56
- return null;
57
- } catch {
58
- return null;
59
- }
60
- }, []);
61
-
62
- // ── Schedule a proactive refresh 5 minutes before the access token expires ──
63
- const scheduleProactiveRefresh = useCallback((accessToken) => {
64
- if (proactiveRefreshRef.current) clearTimeout(proactiveRefreshRef.current);
65
- const expiry = getTokenExpiry(accessToken);
66
- if (!expiry) return;
67
-
68
- const now = Date.now();
69
- const refreshAt = expiry - 5 * 60 * 1000; // 5 min before expiry
70
- const delay = Math.max(refreshAt - now, 10_000); // min 10s
71
-
72
- proactiveRefreshRef.current = setTimeout(async () => {
73
- const newToken = await refreshAccessToken();
74
- if (newToken) {
75
- scheduleProactiveRefresh(newToken); // reschedule for the new token
76
- }
77
- // If refresh failed but user is still browsing, keep them logged in
78
- // until they actually hit a 401 (e.g. backend was briefly down)
79
- }, delay);
80
- }, [refreshAccessToken]);
81
-
82
- // ── Fetch user profile from backend ───────────────────────────────────────
83
- const fetchProfile = useCallback(async (activeToken) => {
84
- try {
85
- const res = await fetch('/api/auth/profile', {
86
- headers: { 'Authorization': `Bearer ${activeToken}` },
87
- });
88
- if (res.ok) {
89
- const data = await res.json();
90
- setUser(data.user);
91
- setLoading(false);
92
- scheduleProactiveRefresh(activeToken);
93
- return;
94
- }
95
- if (res.status === 401) {
96
- // Try to silently refresh before giving up
97
- const newToken = await refreshAccessToken();
98
- if (newToken) {
99
- const retryRes = await fetch('/api/auth/profile', {
100
- headers: { 'Authorization': `Bearer ${newToken}` },
101
- });
102
- if (retryRes.ok) {
103
- const data = await retryRes.json();
104
- setUser(data.user);
105
- setLoading(false);
106
- scheduleProactiveRefresh(newToken);
107
- return;
108
- }
109
- }
110
- // Only logout if both the access token AND refresh failed (401 Unauthorized)
111
- doLogout();
112
- return;
113
- }
114
-
115
- // If backend is offline (502, 504), keep loading screen and retry
116
- if (res.status >= 500) {
117
- setTimeout(() => fetchProfile(activeToken), 2000);
118
- return;
119
- }
120
- } catch {
121
- // Network error β€” retry fetching the profile
122
- setTimeout(() => fetchProfile(activeToken), 2000);
123
- }
124
- }, [refreshAccessToken, scheduleProactiveRefresh, doLogout]);
125
-
126
- // ── Sync token changes to localStorage & fetch profile ────────────────────
127
- useEffect(() => {
128
- if (token) {
129
- localStorage.setItem(TOKEN_KEY, token);
130
- fetchProfile(token);
131
- } else {
132
- localStorage.removeItem(TOKEN_KEY);
133
- setUser(null);
134
- setLoading(false);
135
- }
136
- }, [token]); // intentionally only re-run when token changes
137
-
138
- // ── Login β€” called after successful credentials check ─────────────────────
139
- const login = useCallback((newToken, newRefreshToken, userData) => {
140
- if (proactiveRefreshRef.current) clearTimeout(proactiveRefreshRef.current);
141
- setToken(newToken);
142
- setRefreshToken(newRefreshToken);
143
- setUser(userData);
144
- localStorage.setItem(TOKEN_KEY, newToken);
145
- if (newRefreshToken) localStorage.setItem(REFRESH_KEY, newRefreshToken);
146
- scheduleProactiveRefresh(newToken);
147
- }, [scheduleProactiveRefresh]);
148
-
149
- const logout = useCallback(() => {
150
- doLogout();
151
- }, [doLogout]);
152
-
153
- const reloadUser = useCallback(() => {
154
- if (token) {
155
- fetchProfile(token);
156
- }
157
- }, [token, fetchProfile]);
158
-
159
- // ── Cleanup proactive refresh on unmount ──────────────────────────────────
160
- useEffect(() => {
161
- return () => {
162
- if (proactiveRefreshRef.current) clearTimeout(proactiveRefreshRef.current);
163
- };
164
- }, []);
165
-
166
- return (
167
- <AuthContext.Provider value={{ user, token, refreshToken, loading, login, logout, refreshAccessToken, reloadUser }}>
168
- {children}
169
- </AuthContext.Provider>
170
- );
171
- };
172
-
 
 
 
 
 
173
  export const useAuth = () => useContext(AuthContext);
 
1
+ import React, { createContext, useContext, useState, useEffect, useRef, useCallback } from 'react';
2
+
3
+ const AuthContext = createContext(null);
4
+
5
+ const TOKEN_KEY = 'wss_token';
6
+ const REFRESH_KEY = 'wss_refresh_token';
7
+
8
+ // Parse the expiry time from a JWT without any library
9
+ function getTokenExpiry(token) {
10
+ try {
11
+ const payload = JSON.parse(atob(token.split('.')[1]));
12
+ return payload.exp ? payload.exp * 1000 : null; // convert to ms
13
+ } catch {
14
+ return null;
15
+ }
16
+ }
17
+
18
+ export const AuthProvider = ({ children }) => {
19
+ const [user, setUser] = useState(null);
20
+ const [token, setToken] = useState(localStorage.getItem(TOKEN_KEY));
21
+ const [refreshToken, setRefreshToken] = useState(localStorage.getItem(REFRESH_KEY));
22
+ const [loading, setLoading] = useState(true);
23
+ const proactiveRefreshRef = useRef(null); // holds the setTimeout handle
24
+
25
+ // ── Core logout β€” clear everything ───────────────────────────────────────
26
+ const doLogout = useCallback(() => {
27
+ if (proactiveRefreshRef.current) clearTimeout(proactiveRefreshRef.current);
28
+ setToken(null);
29
+ setRefreshToken(null);
30
+ setUser(null);
31
+ localStorage.removeItem(TOKEN_KEY);
32
+ localStorage.removeItem(REFRESH_KEY);
33
+ }, []);
34
+
35
+ // ── Refresh access token (uses body refresh_token + HttpOnly cookie fallback)
36
+ const refreshAccessToken = useCallback(async () => {
37
+ const storedRefresh = localStorage.getItem(REFRESH_KEY);
38
+ try {
39
+ const res = await fetch('/api/auth/refresh', {
40
+ method: 'POST',
41
+ headers: { 'Content-Type': 'application/json' },
42
+ credentials: 'include', // send HttpOnly cookie as fallback
43
+ body: JSON.stringify({ refresh_token: storedRefresh || '' }),
44
+ });
45
+ if (res.ok) {
46
+ const data = await res.json();
47
+ setToken(data.access_token);
48
+ localStorage.setItem(TOKEN_KEY, data.access_token);
49
+ if (data.refresh_token) {
50
+ setRefreshToken(data.refresh_token);
51
+ localStorage.setItem(REFRESH_KEY, data.refresh_token);
52
+ }
53
+ return data.access_token;
54
+ }
55
+ // Refresh failed β€” do NOT auto-logout. Just return null and let callers decide.
56
+ return null;
57
+ } catch {
58
+ return null;
59
+ }
60
+ }, []);
61
+
62
+ // ── Schedule a proactive refresh 5 minutes before the access token expires ──
63
+ const scheduleProactiveRefresh = useCallback((accessToken) => {
64
+ if (proactiveRefreshRef.current) clearTimeout(proactiveRefreshRef.current);
65
+ const expiry = getTokenExpiry(accessToken);
66
+ if (!expiry) return;
67
+
68
+ const now = Date.now();
69
+ const refreshAt = expiry - 5 * 60 * 1000; // 5 min before expiry
70
+ const delay = Math.max(refreshAt - now, 10_000); // min 10s
71
+
72
+ proactiveRefreshRef.current = setTimeout(async () => {
73
+ const newToken = await refreshAccessToken();
74
+ if (newToken) {
75
+ scheduleProactiveRefresh(newToken); // reschedule for the new token
76
+ }
77
+ // If refresh failed but user is still browsing, keep them logged in
78
+ // until they actually hit a 401 (e.g. backend was briefly down)
79
+ }, delay);
80
+ }, [refreshAccessToken]);
81
+
82
+ // ── Fetch user profile from backend ───────────────────────────────────────
83
+ const fetchProfile = useCallback(async (activeToken, retryCount = 0) => {
84
+ try {
85
+ const res = await fetch('/api/auth/profile', {
86
+ headers: { 'Authorization': `Bearer ${activeToken}` },
87
+ });
88
+ if (res.ok) {
89
+ const data = await res.json();
90
+ setUser(data.user);
91
+ setLoading(false);
92
+ scheduleProactiveRefresh(activeToken);
93
+ return;
94
+ }
95
+ if (res.status === 401) {
96
+ // Try to silently refresh before giving up
97
+ const newToken = await refreshAccessToken();
98
+ if (newToken) {
99
+ const retryRes = await fetch('/api/auth/profile', {
100
+ headers: { 'Authorization': `Bearer ${newToken}` },
101
+ });
102
+ if (retryRes.ok) {
103
+ const data = await retryRes.json();
104
+ setUser(data.user);
105
+ setLoading(false);
106
+ scheduleProactiveRefresh(newToken);
107
+ return;
108
+ }
109
+ }
110
+ // Only logout if both the access token AND refresh failed (401 Unauthorized)
111
+ doLogout();
112
+ setLoading(false);
113
+ return;
114
+ }
115
+
116
+ // If backend is offline (502, 504), retry max 2 times then finish loading
117
+ if (res.status >= 500 && retryCount < 2) {
118
+ setTimeout(() => fetchProfile(activeToken, retryCount + 1), 1500);
119
+ return;
120
+ }
121
+ setLoading(false);
122
+ } catch {
123
+ if (retryCount < 2) {
124
+ setTimeout(() => fetchProfile(activeToken, retryCount + 1), 1500);
125
+ return;
126
+ }
127
+ setLoading(false);
128
+ }
129
+ }, [refreshAccessToken, scheduleProactiveRefresh, doLogout]);
130
+
131
+ // ── Sync token changes to localStorage & fetch profile ────────────────────
132
+ useEffect(() => {
133
+ if (token) {
134
+ localStorage.setItem(TOKEN_KEY, token);
135
+ fetchProfile(token);
136
+ } else {
137
+ localStorage.removeItem(TOKEN_KEY);
138
+ setUser(null);
139
+ setLoading(false);
140
+ }
141
+ }, [token]); // intentionally only re-run when token changes
142
+
143
+ // ── Login β€” called after successful credentials check ─────────────────────
144
+ const login = useCallback((newToken, newRefreshToken, userData) => {
145
+ if (proactiveRefreshRef.current) clearTimeout(proactiveRefreshRef.current);
146
+ setToken(newToken);
147
+ setRefreshToken(newRefreshToken);
148
+ setUser(userData);
149
+ localStorage.setItem(TOKEN_KEY, newToken);
150
+ if (newRefreshToken) localStorage.setItem(REFRESH_KEY, newRefreshToken);
151
+ scheduleProactiveRefresh(newToken);
152
+ }, [scheduleProactiveRefresh]);
153
+
154
+ const logout = useCallback(() => {
155
+ doLogout();
156
+ }, [doLogout]);
157
+
158
+ const reloadUser = useCallback(() => {
159
+ if (token) {
160
+ fetchProfile(token);
161
+ }
162
+ }, [token, fetchProfile]);
163
+
164
+ // ── Cleanup proactive refresh on unmount ──────────────────────────────────
165
+ useEffect(() => {
166
+ return () => {
167
+ if (proactiveRefreshRef.current) clearTimeout(proactiveRefreshRef.current);
168
+ };
169
+ }, []);
170
+
171
+ return (
172
+ <AuthContext.Provider value={{ user, token, refreshToken, loading, login, logout, refreshAccessToken, reloadUser }}>
173
+ {children}
174
+ </AuthContext.Provider>
175
+ );
176
+ };
177
+
178
  export const useAuth = () => useContext(AuthContext);