JWTAuthContext.js
1 import { createContext, useEffect, useReducer } from "react"; 2 import axios from "axios"; 3 import { MatxLoading } from "app/components"; 4 import { applyLanguage } from "app/i18n"; 5 6 const initialState = { 7 user: null, 8 isInitialized: false, 9 isAuthenticated: false, 10 isImpersonating: false, 11 }; 12 13 function assignRole(user) { 14 if (user.is_admin) user.role = "ADMIN"; 15 else if (user.admin_teams && user.admin_teams.length > 0) user.role = "TEAM_ADMIN"; 16 else user.role = "USER"; 17 } 18 19 const reducer = (state, action) => { 20 switch (action.type) { 21 case "INIT": { 22 const { isAuthenticated, user, isImpersonating } = action.payload; 23 return { ...state, isAuthenticated, isInitialized: true, user, isImpersonating: isImpersonating || false }; 24 } 25 26 case "LOGIN": { 27 return { ...state, isAuthenticated: true, user: action.payload.user }; 28 } 29 30 case "LOGOUT": { 31 return { ...state, isAuthenticated: false, user: null, isImpersonating: false }; 32 } 33 34 default: 35 return state; 36 } 37 }; 38 39 const AuthContext = createContext({ 40 ...initialState, 41 method: "JWT", 42 login: () => {}, 43 verifyTotp: () => {}, 44 checkAuth: () => {}, 45 logout: () => {}, 46 impersonate: () => {}, 47 exitImpersonation: () => {}, 48 }); 49 50 export const AuthProvider = ({ children }) => { 51 const [state, dispatch] = useReducer(reducer, initialState); 52 53 const apiUrl = process.env.REACT_APP_RESTAI_API_URL || ""; 54 55 const login = async (email, password) => { 56 try { 57 const response = await axios.post( 58 `${apiUrl}/auth/login`, 59 {}, 60 { auth: { username: email, password: password } } 61 ); 62 63 const data = response.data; 64 65 // 2FA required — return token for TOTP verification 66 if (data.requires_totp) { 67 return { requires_totp: true, totp_token: data.totp_token }; 68 } 69 70 // Stash the password-age warning so the first authenticated page 71 // can surface it. One-shot: the banner component clears the key 72 // after rendering once per login session. 73 if (data.password_warning) { 74 try { sessionStorage.setItem("password_warning", JSON.stringify(data.password_warning)); } catch {} 75 } 76 77 // Normal login — fetch user profile 78 const whoami = await axios.get(`${apiUrl}/auth/whoami`, { withCredentials: true }); 79 const user = whoami.data; 80 assignRole(user); 81 // Apply the user's saved UI language (if any) as soon as whoami 82 // resolves. `options` is either a UserOptions dict or (legacy) 83 // a JSON-string — probe for both shapes. 84 try { 85 const opts = typeof user.options === "string" ? JSON.parse(user.options) : user.options; 86 if (opts && opts.language) applyLanguage(opts.language); 87 } catch {} 88 89 dispatch({ type: "INIT", payload: { isAuthenticated: true, user, isImpersonating: false } }); 90 return { requires_totp: false }; 91 } catch (err) { 92 const detail = err.response?.data?.detail || "Login failed. Check your credentials."; 93 throw new Error(detail); 94 } 95 }; 96 97 const verifyTotp = async (token, code) => { 98 try { 99 const response = await axios.post(`${apiUrl}/auth/verify-totp`, { token, code }, { withCredentials: true }); 100 if (response.data && response.data.password_warning) { 101 try { sessionStorage.setItem("password_warning", JSON.stringify(response.data.password_warning)); } catch {} 102 } 103 const whoami = await axios.get(`${apiUrl}/auth/whoami`, { withCredentials: true }); 104 const user = whoami.data; 105 assignRole(user); 106 // Apply the user's saved UI language (if any) as soon as whoami 107 // resolves. `options` is either a UserOptions dict or (legacy) 108 // a JSON-string — probe for both shapes. 109 try { 110 const opts = typeof user.options === "string" ? JSON.parse(user.options) : user.options; 111 if (opts && opts.language) applyLanguage(opts.language); 112 } catch {} 113 dispatch({ type: "INIT", payload: { isAuthenticated: true, user, isImpersonating: false } }); 114 } catch (err) { 115 const detail = err.response?.data?.detail || "Invalid code."; 116 throw new Error(detail); 117 } 118 }; 119 120 const checkAuth = async () => { 121 try { 122 const response = await axios.get(`${apiUrl}/auth/whoami`, { withCredentials: true }); 123 const user = response.data; 124 assignRole(user); 125 // Apply the user's saved UI language (if any) as soon as whoami 126 // resolves. `options` is either a UserOptions dict or (legacy) 127 // a JSON-string — probe for both shapes. 128 try { 129 const opts = typeof user.options === "string" ? JSON.parse(user.options) : user.options; 130 if (opts && opts.language) applyLanguage(opts.language); 131 } catch {} 132 dispatch({ type: "INIT", payload: { isAuthenticated: true, user, isImpersonating: user.impersonating || false } }); 133 } catch (err) { 134 dispatch({ type: "LOGOUT" }); 135 } 136 }; 137 138 const logout = () => { 139 localStorage.removeItem("user"); 140 axios.post(`${apiUrl}/auth/logout`, {}, { withCredentials: true }).catch(console.error); 141 dispatch({ type: "LOGOUT" }); 142 }; 143 144 const impersonate = async (username) => { 145 try { 146 await axios.post(`${apiUrl}/auth/impersonate/${username}`, {}, { withCredentials: true }); 147 await checkAuth(); 148 } catch (err) { 149 console.error("Impersonation failed:", err); 150 } 151 }; 152 153 const exitImpersonation = async () => { 154 try { 155 await axios.post(`${apiUrl}/auth/exit-impersonation`, {}, { withCredentials: true }); 156 await checkAuth(); 157 } catch (err) { 158 console.error("Exit impersonation failed:", err); 159 } 160 }; 161 162 useEffect(() => { 163 (async () => { 164 try { 165 const response = await axios.get(`${apiUrl}/auth/whoami`, { withCredentials: true }); 166 const user = response.data; 167 assignRole(user); 168 // Apply the user's saved UI language (if any) as soon as whoami 169 // resolves. `options` is either a UserOptions dict or (legacy) 170 // a JSON-string — probe for both shapes. 171 try { 172 const opts = typeof user.options === "string" ? JSON.parse(user.options) : user.options; 173 if (opts && opts.language) applyLanguage(opts.language); 174 } catch {} 175 dispatch({ type: "INIT", payload: { isAuthenticated: true, user, isImpersonating: user.impersonating || false } }); 176 } catch (err) { 177 console.error(err); 178 dispatch({ type: "INIT", payload: { isAuthenticated: false, user: null, isImpersonating: false } }); 179 } 180 })(); 181 }, []); 182 183 if (!state.isInitialized) return <MatxLoading />; 184 185 return ( 186 <AuthContext.Provider value={{ ...state, method: "JWT", login, verifyTotp, checkAuth, logout, impersonate, exitImpersonation }}> 187 {children} 188 </AuthContext.Provider> 189 ); 190 }; 191 192 export default AuthContext;