diff --git a/src/app/App.tsx b/src/app/App.tsx index 09956c2..3616784 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -9,9 +9,12 @@ import LocalAuthPlaceholder from './LocalAuthPlaceholder' import UserWrapper from './UserWrapper' import { getWhitelabel } from 'services/whiteLabel' import { AppThemeProvider } from 'theme/AppThemeProvider' +import { LocalAuthProvider, Auth0AuthBridge } from '../providers/authContext' function App() { - const [token, setToken] = useState(undefined) + const [token, setToken] = useState(() => { + return localStorage.getItem('local_auth_token') || undefined + }) const whiteLabel = getWhitelabel() const manifestPath = "/" + whiteLabel.name.replace(/\s/g, "") + "/manifest.json" @@ -59,9 +62,22 @@ function App() { if (!shouldMountAuth0Provider()) { const placeholderReason = isAuth0Configured() && !isAuth0SpaOriginAllowed() ? 'insecure_origin' : 'unconfigured' + + if (!token) { + return ( + + + + ) + } + return ( - + + + + + ) } @@ -81,10 +97,12 @@ function App() { > {/* */} - { token ? - - - + { token ? + + + + + : void } -/** - * Shown when Auth0 is not used (missing config and/or non–secure context). - * Replace with backend-driven login once local auth is implemented. - */ export default function LocalAuthPlaceholder(props: Props) { - const { reason = 'unconfigured' } = props + const { reason = 'unconfigured', setToken } = props const productName = getName() + const [mode, setMode] = useState<'login' | 'signup'>('login') + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const [name, setName] = useState('') + const [error, setError] = useState('') + const [loading, setLoading] = useState(false) + + const apiUrl = import.meta.env.VITE_APP_API_URL + + const handleSignup = async () => { + setError('') + setLoading(true) + try { + const resp = await axios.post(`${apiUrl}/local-auth/signup`, { email, password, name }) + const token = resp.data.token + if (token && setToken) { + localStorage.setItem('local_auth_token', token) + setToken(token) + } + } catch (err: any) { + setError(err.response?.data?.error || 'Signup failed') + } finally { + setLoading(false) + } + } + + const handleLogin = async () => { + setError('') + setLoading(true) + try { + const resp = await axios.post(`${apiUrl}/local-auth/login`, { email, password }) + const token = resp.data.token + if (token && setToken) { + localStorage.setItem('local_auth_token', token) + setToken(token) + } + } catch (err: any) { + setError(err.response?.data?.error || 'Login failed') + } finally { + setLoading(false) + } + } + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + if (mode === 'signup') { + handleSignup() + } else { + handleLogin() + } + } + const explanation = reason === 'insecure_origin' - ? 'This URL is not a secure context for cloud sign-in (Auth0 requires HTTPS, localhost, or 127.0.0.1). Use local account sign-in here instead once it is connected.' - : 'Cloud sign-in (Auth0) is not configured for this deployment. Local account sign-in will be available here.' + ? 'This URL is not a secure context for cloud sign-in. Use local account sign-in instead.' + : 'Local account sign-in for this deployment.' return ( <> @@ -35,25 +85,55 @@ export default function LocalAuthPlaceholder(props: Props) { }} > - - - {productName} - - - {explanation} - - - - - - Buttons are placeholders until the backend login flow is wired up. - - + diff --git a/src/app/UserWrapper.tsx b/src/app/UserWrapper.tsx index f837b70..6b0fd92 100644 --- a/src/app/UserWrapper.tsx +++ b/src/app/UserWrapper.tsx @@ -1,7 +1,6 @@ import { useCallback, useEffect, useRef, useState } from 'react' import './App.css' import { useUserAPI } from '../providers/pond/userAPI' -import { useAuth0 } from '@auth0/auth0-react' import { or } from '../utils/types' import LoadingScreen from './LoadingScreen' import NavigationContainer from '../navigation/NavigationContainer' @@ -63,13 +62,19 @@ export default function UserWrapper(props: Props) { const [loading, setLoading] = useState(false) const userAPI = useUserAPI(); const classes = useStyles(); - const useAuth = useAuth0(); const hasFetched = useRef(false); const [global, setGlobal] = useState(undefined) const snackbar = useSnackbar() const isMobile = useMobile() - const user_id = or(useAuth.user?.sub, "") + const user_id = (() => { + try { + const payload = JSON.parse(atob(token.split('.')[1])) + return or(payload.sub, '') + } catch { + return '' + } + })() const loadUser = useCallback(() => { if (!userAPI.getUserWithTeam) return; diff --git a/src/navigation/BottomNavigator.tsx b/src/navigation/BottomNavigator.tsx index 6395bd4..0cd0366 100644 --- a/src/navigation/BottomNavigator.tsx +++ b/src/navigation/BottomNavigator.tsx @@ -20,7 +20,7 @@ import OmniAirDeviceIcon from "products/AviationIcons/OmniAirDeviceIcon"; import AirportMapIcon from "products/AviationIcons/AirportMapIcon"; import PlaneIcon from "products/AviationIcons/PlaneIcon"; import JobsiteIcon from "products/Construction/JobSiteIcon"; -import { useAuth0 } from "@auth0/auth0-react"; +import { useAuthContext } from "providers/authContext"; interface Props { sideIsOpen: boolean; @@ -33,7 +33,7 @@ export default function BottomNavigator(props: Props) { const navigate = useNavigate(); const location = useLocation(); const prevLocation = usePrevious(location); - const { isAuthenticated } = useAuth0(); + const { isAuthenticated } = useAuthContext(); const [{ user }] = useGlobalState(); const [route, setRoute] = useState(sideIsOpen ? "side" : ""); const isAg = IsAdaptiveAgriculture(); diff --git a/src/navigation/Router.tsx b/src/navigation/Router.tsx index 94ce101..cde11f9 100644 --- a/src/navigation/Router.tsx +++ b/src/navigation/Router.tsx @@ -1,7 +1,7 @@ import { lazy, Suspense } from "react"; import LoadingScreen from "../app/LoadingScreen"; import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom"; -import { useAuth0 } from "@auth0/auth0-react"; +import { useAuthContext } from "providers/authContext"; import Header from "app/Header"; import Logout from "pages/Logout"; import { ErrorBoundary } from "react-error-boundary"; @@ -57,7 +57,7 @@ export const appendToUrl = (appendage: number | string) => { export default function Router() { - const { /*isAuthenticated, loginWithRedirect,*/ isLoading } = useAuth0(); + const { isAuthenticated } = useAuthContext(); const whiteLabel = getWhitelabel(); const [{ user }] = useGlobalState(); @@ -306,13 +306,7 @@ export default function Router() { ); } - if (isLoading) return null; - // if (!isAuthenticated) { - // loginWithRedirect() - // return ( - // null - // ) - // } + if (!isAuthenticated) return null; function ErrorFallback({ error }: { error: Error }) { return
Something went wrong: {error.stack}
; diff --git a/src/navigation/SideNavigator.tsx b/src/navigation/SideNavigator.tsx index 815f30b..95b2afd 100644 --- a/src/navigation/SideNavigator.tsx +++ b/src/navigation/SideNavigator.tsx @@ -35,7 +35,7 @@ import { IsStreamline, } from "services/whiteLabel"; import MiningIcon from "products/ventilation/MiningIcon"; -import { useAuth0 } from "@auth0/auth0-react"; +import { useAuthContext } from "providers/authContext"; import FieldsIcon from "products/AgIcons/FieldsIcon"; import PlaneIcon from "products/AviationIcons/PlaneIcon"; import AirportMapIcon from "products/AviationIcons/AirportMapIcon"; @@ -130,7 +130,7 @@ interface Props { export default function SideNavigator(props: Props) { const { open, onOpen, onClose } = props; - const { isAuthenticated } = useAuth0() + const { isAuthenticated } = useAuthContext() const theme = useTheme(); const width = useWidth(); const classes = useStyles(); diff --git a/src/pages/Logout.tsx b/src/pages/Logout.tsx index 337c7b6..79e4538 100644 --- a/src/pages/Logout.tsx +++ b/src/pages/Logout.tsx @@ -1,9 +1,9 @@ -import { useAuth0 } from "@auth0/auth0-react"; +import { useAuthContext } from "providers/authContext"; import LoadingScreen from "app/LoadingScreen"; import { useEffect } from "react"; export default function Logout() { - const { logout } = useAuth0(); + const { logout } = useAuthContext(); useEffect(() => { logout(); diff --git a/src/providers/http.tsx b/src/providers/http.tsx index 3e93f04..86ba8b5 100644 --- a/src/providers/http.tsx +++ b/src/providers/http.tsx @@ -2,7 +2,7 @@ import axios, { AxiosRequestConfig, AxiosResponse } from "axios"; import moment from "moment"; import { createContext, PropsWithChildren, useContext } from "react"; import PondProvider from "./pond/pond"; -import { useAuth0 } from "@auth0/auth0-react"; +import { useAuthContext } from "./authContext"; import SnackbarProvider from "./Snackbar"; interface IHTTPContext { @@ -30,7 +30,7 @@ export const HTTPContext = createContext({} as IHTTPContext); export default function HTTPProvider(props: Props) { const { children, token } = props; - const { isAuthenticated, loginWithRedirect } = useAuth0(); + const { isAuthenticated, loginWithRedirect } = useAuthContext(); const defaultOptions = (demo: boolean = false) => { if (demo || !isAuthenticated || !token) { @@ -42,7 +42,7 @@ export default function HTTPProvider(props: Props) { } const config = { - headers: { + headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }, @@ -50,31 +50,24 @@ export default function HTTPProvider(props: Props) { return config; }; - function isTokenExpired(token: string): boolean { + function isTokenExpired(token: string | undefined): boolean { + if (!token) return true; try { - // Split the token and decode the payload (second part) const payloadBase64 = token.split('.')[1]; - const decodedPayload = atob(payloadBase64); // Decode base64 - const payload = JSON.parse(decodedPayload); - - // Get expiration time (in seconds) + const payload = JSON.parse(atob(payloadBase64)); const exp = payload.exp; - - if (!exp) return true; // No exp field? Treat as expired - - // Current time in seconds - const now = Math.floor(Date.now() / 1000); - - // Compare - return now >= exp; - } catch (error) { - console.error("Invalid token format:", error); - return true; // Err on the side of caution if decoding fails + if (!exp) return true; + return Math.floor(Date.now() / 1000) >= exp; + } catch { + return true; } } function get(url: string, spreadOptions?: AxiosRequestConfig): Promise> { - if (isTokenExpired(token)) loginWithRedirect() + if (isTokenExpired(token)) { + loginWithRedirect(); + return Promise.reject(new Error("token expired")); + } return axios.get(url, {...defaultOptions(), ...spreadOptions}); } @@ -83,6 +76,10 @@ export default function HTTPProvider(props: Props) { data?: any, spreadOptions?: AxiosRequestConfig ): Promise> { + if (isTokenExpired(token)) { + loginWithRedirect(); + return Promise.reject(new Error("token expired")); + } return axios.put(url, data, { ...defaultOptions(), ...spreadOptions }); } @@ -91,10 +88,18 @@ export default function HTTPProvider(props: Props) { data?: any, spreadOptions?: AxiosRequestConfig ): Promise> { + if (isTokenExpired(token)) { + loginWithRedirect(); + return Promise.reject(new Error("token expired")); + } return axios.post(url, data, { ...defaultOptions(), ...spreadOptions }); } function del(url: string, spreadOptions?: AxiosRequestConfig): Promise> { + if (isTokenExpired(token)) { + loginWithRedirect(); + return Promise.reject(new Error("token expired")); + } return axios.delete(url, { ...defaultOptions(), ...spreadOptions }); } diff --git a/src/user/UserMenu.tsx b/src/user/UserMenu.tsx index a65b935..1eb2da5 100644 --- a/src/user/UserMenu.tsx +++ b/src/user/UserMenu.tsx @@ -5,7 +5,7 @@ import { makeStyles } from "@mui/styles"; import { LockOpen, Person, Lock, Settings, SupervisedUserCircle as TeamIcon, ExitToApp, PersonAdd } from "@mui/icons-material"; import { useState } from "react"; import UserTeamName from "./UserTeamName"; -import { useAuth0 } from "@auth0/auth0-react"; +import { useAuthContext } from "providers/authContext"; import UserSettings from "./UserSettings"; import UserAvatar from "./UserAvatar"; import { purple } from "@mui/material/colors"; @@ -77,7 +77,7 @@ export default function UserMenu() { // const { toggleMode } = useThemeMode() const [{ user, team, as }, dispatch] = useGlobalState(); - const { loginWithRedirect } = useAuth0(); + const { loginWithRedirect } = useAuthContext(); const classes = useStyles(); const theme = useTheme();