Files
donetick/src/views/Authorization/LoginView.jsx
Mo Tarbin 8b8345d0e6 Refactor modals to use FadeModal component for consistent styling and improved user experience
- fix https://github.com/donetick/donetick/issues/222

- Replaced Modal and ModalDialog with FadeModal in LabelModal, PasswordChangeModal, SelectModal, TextModal, UserModal, WriteNFCModal, RedeemPointsModal, and AddTaskModal.
- Updated modal structure and layout to maintain functionality while enhancing visual consistency.
- Removed unused imports and commented-out code for cleaner codebase.
2025-06-27 01:42:38 -04:00

527 lines
16 KiB
JavaScript

import { Capacitor } from '@capacitor/core'
// import { GoogleAuth } from '@codetrix-studio/capacitor-google-auth'
import { SocialLogin } from '@capgo/capacitor-social-login'
import { Settings } from '@mui/icons-material'
import GoogleIcon from '@mui/icons-material/Google'
import {
Avatar,
Box,
Button,
Container,
Divider,
IconButton,
Input,
Sheet,
Typography,
} from '@mui/joy'
import { useQueryClient } from '@tanstack/react-query'
import Cookies from 'js-cookie'
import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { LoginSocialGoogle } from 'reactjs-social-login'
import { GOOGLE_CLIENT_ID, REDIRECT_URL } from '../../Config'
import Logo from '../../Logo'
import { useResource } from '../../queries/ResourceQueries'
import { useNotification } from '../../service/NotificationProvider'
import { GetUserProfile, login } from '../../utils/Fetcher'
import { apiManager, isTokenValid } from '../../utils/TokenManager'
import MFAVerificationModal from './MFAVerificationModal'
const LoginView = () => {
// Use React Query client directly to invalidate the user profile query
const queryClient = useQueryClient()
const [userProfile, setUserProfile] = useState(null)
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [mfaModalOpen, setMfaModalOpen] = useState(false)
const [mfaSessionToken, setMfaSessionToken] = useState('')
const { data: resource } = useResource()
const { showError } = useNotification()
const Navigate = useNavigate()
useEffect(() => {
const initializeSocialLogin = async () => {
await SocialLogin.initialize({
google: {
webClientId: import.meta.env.VITE_APP_GOOGLE_CLIENT_ID,
iOSClientId: import.meta.env.VITE_APP_IOS_CLIENT_ID,
mode: 'online', // replaces grantOfflineAccess
},
})
}
initializeSocialLogin()
}, [])
useEffect(() => {
if (isTokenValid()) {
GetUserProfile().then(response => {
if (response.status === 200) {
return response.json().then(data => {
setUserProfile(data.res)
})
} else {
console.log('Failed to fetch user profile')
}
})
}
}, [])
const handleSubmit = async e => {
e.preventDefault()
login(username, password)
.then(response => {
if (response.status === 200) {
return response.json().then(data => {
// Check if MFA is required
if (data.mfaRequired) {
setMfaSessionToken(data.sessionToken)
setMfaModalOpen(true)
return
}
// Normal login without MFA
localStorage.setItem('ca_token', data.token)
localStorage.setItem('ca_expiration', data.expire)
// Refetch user profile after successful login
queryClient.refetchQueries(['userProfile'])
const redirectUrl = Cookies.get('ca_redirect')
if (redirectUrl && redirectUrl !== '/') {
console.log('Redirecting to', redirectUrl)
Cookies.remove('ca_redirect')
Navigate(redirectUrl)
} else {
Cookies.remove('ca_redirect')
Navigate('/my/chores')
}
})
} else if (response.status === 401) {
showError({
title: 'Login Failed',
message: 'Wrong username or password',
})
} else {
showError({
title: 'Login Failed',
message: 'An error occurred, please try again',
})
console.log('Login failed')
}
})
.catch(err => {
showError({
title: 'Connection Error',
message: 'Unable to communicate with server, please try again',
})
console.log('Login failed', err)
})
}
const loggedWithProvider = function (provider, data) {
const baseURL = apiManager.getApiURL()
const getAccessToken = data => {
if (data['access_token']) {
// data["access_token"] is for Google
return data['access_token']
} else if (data['accessToken']) {
// data["accessToken"] is for Google Capacitor
return data['accessToken']['token']
}
}
return fetch(`${baseURL}/auth/${provider}/callback`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
provider: provider,
token: getAccessToken(data),
data: data,
}),
}).then(response => {
if (response.status === 200) {
return response.json().then(data => {
// Check if MFA is required for OAuth login
if (data.mfaRequired) {
setMfaSessionToken(data.sessionToken)
setMfaModalOpen(true)
return
}
// Normal OAuth login without MFA
localStorage.setItem('ca_token', data.token)
localStorage.setItem('ca_expiration', data.expire)
// Refetch user profile after successful OAuth login
queryClient.invalidateQueries(['userProfile'])
const redirectUrl = Cookies.get('ca_redirect')
if (redirectUrl) {
Cookies.remove('ca_redirect')
Navigate(redirectUrl)
} else {
getUserProfileAndNavigateToHome()
}
})
}
return response.json().then(() => {
showError({
title: 'Google Login Failed',
message: "Couldn't log in with Google, please try again",
})
})
})
}
const getUserProfileAndNavigateToHome = () => {
// Refetch user profile after login using React Query
queryClient.invalidateQueries(['userProfile']).then(() => {
// 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 handleMFASuccess = data => {
localStorage.setItem('ca_token', data.token)
localStorage.setItem('ca_expiration', data.expire)
setMfaModalOpen(false)
setMfaSessionToken('')
// Refetch user profile after MFA success
queryClient.invalidateQueries(['userProfile'])
const redirectUrl = Cookies.get('ca_redirect')
if (redirectUrl) {
Cookies.remove('ca_redirect')
Navigate(redirectUrl)
} else {
Navigate('/my/chores')
}
}
const handleMFAError = errorMessage => {
showError({
title: 'Two-Factor Authentication Failed',
message: errorMessage,
})
}
const handleMFAClose = () => {
setMfaModalOpen(false)
setMfaSessionToken('')
}
const handleForgotPassword = () => {
Navigate('/forgot-password')
}
const generateRandomState = () => {
const randomState = Math.random().toString(32).substring(5)
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'
maxWidth='xs'
// make content center in the middle of the page:
>
<Box
sx={{
marginTop: 4,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<Sheet
component='form'
sx={{
mt: 1,
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
padding: 2,
borderRadius: '8px',
boxShadow: 'md',
}}
>
{Capacitor.isNativePlatform() && (
<IconButton
// on top right of the screen:
sx={{ position: 'absolute', top: 2, right: 2, color: 'black' }}
onClick={() => {
Navigate('/login/settings')
}}
>
{' '}
<Settings />
</IconButton>
)}
<Logo />
<Typography level='h2'>
Done
<span style={{ color: '#06b6d4' }}>tick</span>
</Typography>
{userProfile && (
<>
<Avatar
src={userProfile?.image}
alt={userProfile?.username}
size='lg'
sx={{ mt: 2, width: '96px', height: '96px', mb: 1 }}
/>
<Typography level='body-md' alignSelf={'center'}>
Welcome back,{' '}
{userProfile?.displayName || userProfile?.username}
</Typography>
<Button
fullWidth
size='lg'
sx={{ mt: 3, mb: 2 }}
onClick={() => {
getUserProfileAndNavigateToHome()
}}
>
Continue as {userProfile.displayName || userProfile.username}
</Button>
<Button
type='submit'
fullWidth
size='lg'
variant='plain'
sx={{
width: '100%',
mb: 2,
border: 'moccasin',
borderRadius: '8px',
}}
onClick={() => {
localStorage.removeItem('ca_token')
localStorage.removeItem('ca_expiration')
// go to login page:
window.location.href = '/login'
}}
>
Logout
</Button>
</>
)}
{!userProfile && (
<>
<Typography level='body2'>
Sign in to your account to continue
</Typography>
<Typography level='body2' alignSelf={'start'} mt={4}>
Username
</Typography>
<Input
margin='normal'
required
fullWidth
id='email'
label='Email Address'
name='email'
autoComplete='email'
autoFocus
value={username}
onChange={e => {
setUsername(e.target.value)
}}
/>
<Typography level='body2' alignSelf={'start'}>
Password:
</Typography>
<Input
margin='normal'
required
fullWidth
name='password'
label='Password'
type='password'
id='password'
value={password}
onChange={e => {
setPassword(e.target.value)
}}
/>
<Button
type='submit'
fullWidth
size='lg'
variant='solid'
sx={{
width: '100%',
mt: 3,
mb: 2,
border: 'moccasin',
borderRadius: '8px',
}}
onClick={handleSubmit}
>
Sign In
</Button>
<Button
type='submit'
fullWidth
size='lg'
variant='plain'
sx={{
width: '100%',
mb: 2,
border: 'moccasin',
borderRadius: '8px',
}}
onClick={handleForgotPassword}
>
Forgot password?
</Button>
</>
)}
<Divider> or </Divider>
{import.meta.env.VITE_IS_SELF_HOSTED !== 'true' && (
<>
{!Capacitor.isNativePlatform() && (
<Box sx={{ width: '100%' }}>
<LoginSocialGoogle
client_id={GOOGLE_CLIENT_ID}
redirect_uri={REDIRECT_URL}
scope='openid profile email'
discoveryDocs='claims_supported'
access_type='online'
isOnlyGetToken={true}
onResolve={({ provider, data }) => {
loggedWithProvider(provider, data)
}}
onReject={() => {
showError({
title: 'Google Login Failed',
message:
"Couldn't log in with Google, please try again",
})
}}
>
<Button
variant='soft'
color='neutral'
size='lg'
fullWidth
sx={{
width: '100%',
mt: 1,
mb: 1,
border: 'moccasin',
borderRadius: '8px',
}}
>
<div className='flex gap-2'>
<GoogleIcon />
Continue with Google
</div>
</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)
// })
SocialLogin.login({
provider: 'google',
options: { scopes: ['profile', 'email', 'openid'] },
}).then(user => {
console.log('Google user', user)
loggedWithProvider('google', user.result)
})
}}
>
<div className='flex gap-2'>
<GoogleIcon />
Continue with Google
</div>
</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={() => {
Navigate('/signup')
}}
fullWidth
variant='soft'
size='lg'
// sx={{ mt: 3, mb: 2 }}
>
Create new account
</Button>
</Sheet>
</Box>
<MFAVerificationModal
open={mfaModalOpen}
onClose={handleMFAClose}
sessionToken={mfaSessionToken}
onSuccess={handleMFASuccess}
onError={handleMFAError}
/>
</Container>
)
}
export default LoginView