Add useResource hook for fetching resource data

Add Support or authenticating using OIDC
This commit is contained in:
Mo Tarbin 2025-02-06 19:35:33 -05:00
parent 0e0c43c4f5
commit bd49f4314d
5 changed files with 265 additions and 64 deletions

View file

@ -5,6 +5,7 @@ import Error from '@/views/Error'
import Settings from '@/views/Settings/Settings' import Settings from '@/views/Settings/Settings'
import { Capacitor } from '@capacitor/core' import { Capacitor } from '@capacitor/core'
import { RouterProvider, createBrowserRouter } from 'react-router-dom' import { RouterProvider, createBrowserRouter } from 'react-router-dom'
import AuthenticationLoading from '../views/Authorization/Authenticating'
import ForgotPasswordView from '../views/Authorization/ForgotPasswordView' import ForgotPasswordView from '../views/Authorization/ForgotPasswordView'
import LoginSettings from '../views/Authorization/LoginSettings' import LoginSettings from '../views/Authorization/LoginSettings'
import LoginView from '../views/Authorization/LoginView' import LoginView from '../views/Authorization/LoginView'
@ -92,6 +93,10 @@ const Router = createBrowserRouter([
path: '/signup', path: '/signup',
element: <SignupView />, element: <SignupView />,
}, },
{
path: '/auth/:provider',
element: <AuthenticationLoading />,
},
{ {
path: '/landing', path: '/landing',
element: <Landing />, element: <Landing />,

View file

@ -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 }
}

View file

@ -375,6 +375,15 @@ const GetLabels = async () => {
return resp.json() 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 => { const UpdateLabel = label => {
return Fetch(`/labels`, { return Fetch(`/labels`, {
method: 'PUT', method: 'PUT',
@ -430,7 +439,6 @@ const RedeemPoints = (userId, points, circleID) => {
body: JSON.stringify({ points, userId }), body: JSON.stringify({ points, userId }),
}) })
} }
const RefreshToken = () => { const RefreshToken = () => {
const basedURL = apiManager.getApiURL() const basedURL = apiManager.getApiURL()
return fetch(`${basedURL}/auth/refresh`, { return fetch(`${basedURL}/auth/refresh`, {
@ -481,6 +489,7 @@ export {
GetCircleMemberRequests, GetCircleMemberRequests,
GetLabels, GetLabels,
GetLongLiveTokens, GetLongLiveTokens,
GetResource,
GetSubscriptionSession, GetSubscriptionSession,
GetThingHistory, GetThingHistory,
GetThings, GetThings,

View file

@ -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 (
<Container className='flex h-full items-center justify-center'>
<Box
className='flex flex-col items-center justify-center'
sx={{
minHeight: '80vh',
}}
>
<CircularProgress
determinate={status === 'error'}
color={status === 'pending' ? 'primary' : 'danger'}
sx={{ '--CircularProgress-size': '200px' }}
>
<Logo />
</CircularProgress>
<Box
className='flex items-center gap-2'
sx={{
fontWeight: 700,
fontSize: 24,
mt: 2,
}}
>
{message}
</Box>
<Typography level='body-md' fontWeight={500} textAlign={'center'}>
{subMessage}
</Typography>
{status === 'error' && (
<Button
size='lg'
variant='outlined'
sx={{
mt: 4,
}}
>
<Link to='/login'>Go back Login</Link>
</Button>
)}
</Box>
</Container>
)
}
export default AuthenticationLoading

View file

@ -21,6 +21,7 @@ import { LoginSocialGoogle } from 'reactjs-social-login'
import { GOOGLE_CLIENT_ID, REDIRECT_URL } from '../../Config' import { GOOGLE_CLIENT_ID, REDIRECT_URL } from '../../Config'
import { UserContext } from '../../contexts/UserContext' import { UserContext } from '../../contexts/UserContext'
import Logo from '../../Logo' import Logo from '../../Logo'
import { useResource } from '../../queries/ResourceQueries'
import { GetUserProfile, login } from '../../utils/Fetcher' import { GetUserProfile, login } from '../../utils/Fetcher'
import { apiManager } from '../../utils/TokenManager' import { apiManager } from '../../utils/TokenManager'
@ -29,6 +30,7 @@ const LoginView = () => {
const [username, setUsername] = React.useState('') const [username, setUsername] = React.useState('')
const [password, setPassword] = React.useState('') const [password, setPassword] = React.useState('')
const [error, setError] = React.useState(null) const [error, setError] = React.useState(null)
const { data: resource } = useResource()
const Navigate = useNavigate() const Navigate = useNavigate()
const handleSubmit = async e => { const handleSubmit = async e => {
e.preventDefault() e.preventDefault()
@ -112,6 +114,28 @@ const LoginView = () => {
const handleForgotPassword = () => { const handleForgotPassword = () => {
Navigate('/forgot-password') 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 ( return (
<Container <Container
component='main' component='main'
@ -296,70 +320,85 @@ const LoginView = () => {
</> </>
)} )}
<Divider> or </Divider> <Divider> or </Divider>
{!Capacitor.isNativePlatform() && ( {import.meta.env.VITE_IS_SELF_HOSTED !== 'true' && (
<Box sx={{ width: '100%' }}> <>
<LoginSocialGoogle {!Capacitor.isNativePlatform() && (
client_id={GOOGLE_CLIENT_ID} <Box sx={{ width: '100%' }}>
redirect_uri={REDIRECT_URL} <LoginSocialGoogle
scope='openid profile email' client_id={GOOGLE_CLIENT_ID}
discoveryDocs='claims_supported' redirect_uri={REDIRECT_URL}
access_type='online' scope='openid profile email'
isOnlyGetToken={true} discoveryDocs='claims_supported'
onResolve={({ provider, data }) => { access_type='online'
loggedWithProvider(provider, data) isOnlyGetToken={true}
}} onResolve={({ provider, data }) => {
onReject={err => { loggedWithProvider(provider, data)
setError("Couldn't log in with Google, please try again") }}
}} onReject={err => {
> setError("Couldn't log in with Google, please try again")
<Button }}
variant='soft' >
color='neutral' <Button
size='lg' variant='soft'
fullWidth color='neutral'
sx={{ size='lg'
width: '100%', fullWidth
mt: 1, sx={{
mb: 1, width: '100%',
border: 'moccasin', mt: 1,
borderRadius: '8px', mb: 1,
}} border: 'moccasin',
> borderRadius: '8px',
<div className='flex gap-2'> }}
<GoogleIcon /> >
Continue with Google <div className='flex gap-2'>
</div> <GoogleIcon />
</Button> Continue with Google
</LoginSocialGoogle> </div>
</Box> </Button>
</LoginSocialGoogle>
</Box>
)}
{Capacitor.isNativePlatform() && (
<Box sx={{ width: '100%' }}>
<Button
fullWidth
variant='soft'
size='lg'
sx={{ mt: 3, mb: 2 }}
onClick={() => {
GoogleAuth.initialize({
clientId: import.meta.env.VITE_APP_GOOGLE_CLIENT_ID,
scopes: ['profile', 'email', 'openid'],
grantOfflineAccess: true,
})
GoogleAuth.signIn().then(user => {
console.log('Google user', user)
loggedWithProvider('google', user.authentication)
})
}}
>
<div className='flex gap-2'>
<GoogleIcon />
Continue with Google
</div>
</Button>
</Box>
)}
</>
)} )}
{resource?.identity_provider?.client_id && (
{Capacitor.isNativePlatform() && ( <Button
<Box sx={{ width: '100%' }}> fullWidth
<Button color='neutral'
fullWidth variant='soft'
variant='soft' size='lg'
size='lg' sx={{ mt: 3, mb: 2 }}
sx={{ mt: 3, mb: 2 }} onClick={handleAuthentikLogin}
onClick={() => { >
GoogleAuth.initialize({ Continue with {resource?.identity_provider?.name}
clientId: import.meta.env.VITE_APP_GOOGLE_CLIENT_ID, </Button>
scopes: ['profile', 'email', 'openid'],
grantOfflineAccess: true,
})
GoogleAuth.signIn().then(user => {
console.log('Google user', user)
loggedWithProvider('google', user.authentication)
})
}}
>
<div className='flex gap-2'>
<GoogleIcon />
Continue with Google
</div>
</Button>
</Box>
)} )}
<Button <Button