From bd49f4314dc3ba2d32cad5519da7ad96826e478e Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Thu, 6 Feb 2025 19:35:33 -0500 Subject: [PATCH] Add useResource hook for fetching resource data Add Support or authenticating using OIDC --- src/contexts/RouterContext.jsx | 5 + src/queries/ResourceQueries.jsx | 9 ++ src/utils/Fetcher.jsx | 11 +- src/views/Authorization/Authenticating.jsx | 139 +++++++++++++++++ src/views/Authorization/LoginView.jsx | 165 +++++++++++++-------- 5 files changed, 265 insertions(+), 64 deletions(-) create mode 100644 src/queries/ResourceQueries.jsx create mode 100644 src/views/Authorization/Authenticating.jsx diff --git a/src/contexts/RouterContext.jsx b/src/contexts/RouterContext.jsx index 212148e..2a2da2f 100644 --- a/src/contexts/RouterContext.jsx +++ b/src/contexts/RouterContext.jsx @@ -5,6 +5,7 @@ import Error from '@/views/Error' import Settings from '@/views/Settings/Settings' import { Capacitor } from '@capacitor/core' import { RouterProvider, createBrowserRouter } from 'react-router-dom' +import AuthenticationLoading from '../views/Authorization/Authenticating' import ForgotPasswordView from '../views/Authorization/ForgotPasswordView' import LoginSettings from '../views/Authorization/LoginSettings' import LoginView from '../views/Authorization/LoginView' @@ -92,6 +93,10 @@ const Router = createBrowserRouter([ path: '/signup', element: , }, + { + path: '/auth/:provider', + element: , + }, { path: '/landing', element: , diff --git a/src/queries/ResourceQueries.jsx b/src/queries/ResourceQueries.jsx new file mode 100644 index 0000000..c310631 --- /dev/null +++ b/src/queries/ResourceQueries.jsx @@ -0,0 +1,9 @@ +import { useQuery } from 'react-query' +import { GetResource } from '../utils/Fetcher' + +export const useResource = () => { + const { data, isLoading, error } = useQuery('resource', GetResource, { + cacheTime: 1000 * 60 * 10, // 1 minute + }) + return { data, isLoading, error } +} diff --git a/src/utils/Fetcher.jsx b/src/utils/Fetcher.jsx index e98e60a..bc18823 100644 --- a/src/utils/Fetcher.jsx +++ b/src/utils/Fetcher.jsx @@ -375,6 +375,15 @@ const GetLabels = async () => { return resp.json() } +const GetResource = async () => { + const basedURL = apiManager.getApiURL() + const resp = await fetch(`${basedURL}/resource`, { + method: 'GET', + headers: HEADERS(), + }) + return resp.json() +} + const UpdateLabel = label => { return Fetch(`/labels`, { method: 'PUT', @@ -430,7 +439,6 @@ const RedeemPoints = (userId, points, circleID) => { body: JSON.stringify({ points, userId }), }) } - const RefreshToken = () => { const basedURL = apiManager.getApiURL() return fetch(`${basedURL}/auth/refresh`, { @@ -481,6 +489,7 @@ export { GetCircleMemberRequests, GetLabels, GetLongLiveTokens, + GetResource, GetSubscriptionSession, GetThingHistory, GetThings, diff --git a/src/views/Authorization/Authenticating.jsx b/src/views/Authorization/Authenticating.jsx new file mode 100644 index 0000000..0003fdd --- /dev/null +++ b/src/views/Authorization/Authenticating.jsx @@ -0,0 +1,139 @@ +import { Box, Button, CircularProgress, Container, Typography } from '@mui/joy' +import { useContext, useEffect, useState } from 'react' +import Logo from '../../Logo' +import { apiManager } from '../../utils/TokenManager' + +import Cookies from 'js-cookie' +import { useRef } from 'react' +import { Link, useNavigate, useParams } from 'react-router-dom' +import { UserContext } from '../../contexts/UserContext' +import { GetUserProfile } from '../../utils/Fetcher' + +const AuthenticationLoading = () => { + const { userProfile, setUserProfile } = useContext(UserContext) + const Navigate = useNavigate() + const hasCalledHandleOAuth2 = useRef(false) + const [message, setMessage] = useState('Authenticating') + const [subMessage, setSubMessage] = useState('Please wait') + const [status, setStatus] = useState('pending') + const { provider } = useParams() + useEffect(() => { + if (provider === 'oauth2' && !hasCalledHandleOAuth2.current) { + hasCalledHandleOAuth2.current = true + handleOAuth2() + } else if (provider !== 'oauth2') { + setMessage('Unknown Authentication Provider') + setSubMessage('Please contact support') + } + }, [provider]) + const getUserProfileAndNavigateToHome = () => { + GetUserProfile().then(data => { + data.json().then(data => { + setUserProfile(data.res) + // check if redirect url is set in cookie: + const redirectUrl = Cookies.get('ca_redirect') + if (redirectUrl) { + Cookies.remove('ca_redirect') + Navigate(redirectUrl) + } else { + Navigate('/my/chores') + } + }) + }) + } + const handleOAuth2 = () => { + // get provider from params: + const urlParams = new URLSearchParams(window.location.search) + const code = urlParams.get('code') + const returnedState = urlParams.get('state') + + const storedState = localStorage.getItem('authState') + + if (returnedState !== storedState) { + setMessage('Authentication failed') + setSubMessage('State does not match') + setStatus('error') + return + } + + if (code) { + const baseURL = apiManager.getApiURL() + fetch(`${baseURL}/auth/${provider}/callback`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + code, + state: returnedState, + }), + }).then(response => { + if (response.status === 200) { + return response.json().then(data => { + localStorage.setItem('ca_token', data.token) + localStorage.setItem('ca_expiration', data.expire) + + const redirectUrl = Cookies.get('ca_redirect') + if (redirectUrl) { + Cookies.remove('ca_redirect') + Navigate(redirectUrl) + } else { + getUserProfileAndNavigateToHome() + } + }) + } else { + console.error('Authentication failed') + setMessage('Authentication failed') + setSubMessage('Please try again') + setStatus('error') + } + }) + } + } + + return ( + + + + + + + {message} + + + {subMessage} + + + {status === 'error' && ( + + )} + + + ) +} + +export default AuthenticationLoading diff --git a/src/views/Authorization/LoginView.jsx b/src/views/Authorization/LoginView.jsx index 0e871ca..476f468 100644 --- a/src/views/Authorization/LoginView.jsx +++ b/src/views/Authorization/LoginView.jsx @@ -21,6 +21,7 @@ import { LoginSocialGoogle } from 'reactjs-social-login' import { GOOGLE_CLIENT_ID, REDIRECT_URL } from '../../Config' import { UserContext } from '../../contexts/UserContext' import Logo from '../../Logo' +import { useResource } from '../../queries/ResourceQueries' import { GetUserProfile, login } from '../../utils/Fetcher' import { apiManager } from '../../utils/TokenManager' @@ -29,6 +30,7 @@ const LoginView = () => { const [username, setUsername] = React.useState('') const [password, setPassword] = React.useState('') const [error, setError] = React.useState(null) + const { data: resource } = useResource() const Navigate = useNavigate() const handleSubmit = async e => { e.preventDefault() @@ -112,6 +114,28 @@ const LoginView = () => { const handleForgotPassword = () => { Navigate('/forgot-password') } + const generateRandomState = () => { + const randomState = Math.random().toString(36).substring(7) + localStorage.setItem('authState', randomState) + + return randomState + } + + const handleAuthentikLogin = () => { + const authentikAuthorizeUrl = resource?.identity_provider?.auth_url + + const params = new URLSearchParams({ + response_type: 'code', + client_id: resource?.identity_provider?.client_id, + redirect_uri: `${window.location.origin}/auth/oauth2`, + scope: 'openid profile email', // Your scopes + state: generateRandomState(), + }) + console.log('redirect', `${authentikAuthorizeUrl}?${params.toString()}`) + + window.location.href = `${authentikAuthorizeUrl}?${params.toString()}` + } + return ( { )} or - {!Capacitor.isNativePlatform() && ( - - { - loggedWithProvider(provider, data) - }} - onReject={err => { - setError("Couldn't log in with Google, please try again") - }} - > - - - + {import.meta.env.VITE_IS_SELF_HOSTED !== 'true' && ( + <> + {!Capacitor.isNativePlatform() && ( + + { + loggedWithProvider(provider, data) + }} + onReject={err => { + setError("Couldn't log in with Google, please try again") + }} + > + + + + )} + {Capacitor.isNativePlatform() && ( + + + + )} + )} - - {Capacitor.isNativePlatform() && ( - - - + {resource?.identity_provider?.client_id && ( + )}