Add useResource hook for fetching resource data
Add Support or authenticating using OIDC
This commit is contained in:
parent
0e0c43c4f5
commit
bd49f4314d
5 changed files with 265 additions and 64 deletions
|
@ -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 />,
|
||||||
|
|
9
src/queries/ResourceQueries.jsx
Normal file
9
src/queries/ResourceQueries.jsx
Normal 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 }
|
||||||
|
}
|
|
@ -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,
|
||||||
|
|
139
src/views/Authorization/Authenticating.jsx
Normal file
139
src/views/Authorization/Authenticating.jsx
Normal 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
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Reference in a new issue