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 { 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: <SignupView />,
|
||||
},
|
||||
{
|
||||
path: '/auth/:provider',
|
||||
element: <AuthenticationLoading />,
|
||||
},
|
||||
{
|
||||
path: '/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()
|
||||
}
|
||||
|
||||
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,
|
||||
|
|
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 { 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 (
|
||||
<Container
|
||||
component='main'
|
||||
|
@ -296,6 +320,8 @@ const LoginView = () => {
|
|||
</>
|
||||
)}
|
||||
<Divider> or </Divider>
|
||||
{import.meta.env.VITE_IS_SELF_HOSTED !== 'true' && (
|
||||
<>
|
||||
{!Capacitor.isNativePlatform() && (
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<LoginSocialGoogle
|
||||
|
@ -333,7 +359,6 @@ const LoginView = () => {
|
|||
</LoginSocialGoogle>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{Capacitor.isNativePlatform() && (
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<Button
|
||||
|
@ -361,6 +386,20 @@ const LoginView = () => {
|
|||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{resource?.identity_provider?.client_id && (
|
||||
<Button
|
||||
fullWidth
|
||||
color='neutral'
|
||||
variant='soft'
|
||||
size='lg'
|
||||
sx={{ mt: 3, mb: 2 }}
|
||||
onClick={handleAuthentikLogin}
|
||||
>
|
||||
Continue with {resource?.identity_provider?.name}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={() => {
|
||||
|
|
Loading…
Add table
Reference in a new issue