Support Refresh token and improve authentication and security.
Refactor authentication handling and API client integration - Updated UserProfileAvatar to use 'access_token' instead of 'ca_token' for logout. - Removed SSEProvider from Contexts and adjusted related imports. - Introduced useAuth hook for centralized authentication logic, including login, logout, and token management. - Refactored useSSE to utilize the new useAuth hook for token validation. - Updated UserQueries to check for token validity using the new method. - Deleted AuthenticationService as its functionality is now handled by useAuth. - Created a new ApiClient utility for handling API requests and token management. - Updated various components and views to use the new ApiClient for API interactions. - Removed TokenManager and migrated its functionality to the new ApiClient. - Adjusted LoginView and related components to utilize the new authentication flow. - Cleaned up unused variables and improved code consistency across components.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { Box, Button, CircularProgress, Container, Typography } from '@mui/joy'
|
||||
import { useEffect, useState } from 'react'
|
||||
import Logo from '../../Logo'
|
||||
import { apiManager } from '../../utils/TokenManager'
|
||||
import { apiClient } from '../../utils/apiClient'
|
||||
|
||||
import Cookies from 'js-cookie'
|
||||
import { useRef } from 'react'
|
||||
@@ -58,7 +58,7 @@ const AuthenticationLoading = () => {
|
||||
}
|
||||
|
||||
if (code) {
|
||||
const baseURL = apiManager.getApiURL()
|
||||
const baseURL = apiClient.baseURL
|
||||
fetch(`${baseURL}/auth/${provider}/callback`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -71,7 +71,7 @@ const AuthenticationLoading = () => {
|
||||
}).then(response => {
|
||||
if (response.status === 200) {
|
||||
return response.json().then(data => {
|
||||
localStorage.setItem('ca_token', data.token)
|
||||
localStorage.setItem('token', data.token)
|
||||
localStorage.setItem('ca_expiration', data.expire)
|
||||
|
||||
const redirectUrl = Cookies.get('ca_redirect')
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useNavigate } from 'react-router-dom'
|
||||
import { API_URL } from '../../Config'
|
||||
import Logo from '../../Logo'
|
||||
import { useNotification } from '../../service/NotificationProvider'
|
||||
import { apiManager } from '../../utils/TokenManager'
|
||||
import { apiClient } from '../../utils/apiClient'
|
||||
const LoginSettings = () => {
|
||||
const Navigate = useNavigate()
|
||||
const [serverURL, setServerURL] = React.useState('')
|
||||
@@ -115,7 +115,7 @@ const LoginSettings = () => {
|
||||
key: 'customServerUrl',
|
||||
value: serverURL,
|
||||
}).then(() => {
|
||||
apiManager.updateApiURL(serverURL + '/api/v1')
|
||||
apiClient.baseURL = serverURL + '/api/v1'
|
||||
Navigate('/login')
|
||||
})
|
||||
}}
|
||||
|
||||
@@ -30,8 +30,8 @@ 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 { useAuth } from '../../hooks/useAuth.jsx'
|
||||
import { apiClient } from '../../utils/apiClient'
|
||||
import { buildChildUsername, getUserDisplayInfo } from '../../utils/UserHelpers'
|
||||
import MFAVerificationModal from './MFAVerificationModal'
|
||||
|
||||
@@ -60,6 +60,7 @@ const LoginView = () => {
|
||||
}
|
||||
const { data: resource } = useResource()
|
||||
const { showError } = useNotification()
|
||||
const { isAuthenticated, login: authLogin, user } = useAuth()
|
||||
const Navigate = useNavigate()
|
||||
useEffect(() => {
|
||||
const initializeSocialLogin = async () => {
|
||||
@@ -89,18 +90,11 @@ const LoginView = () => {
|
||||
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')
|
||||
}
|
||||
})
|
||||
if (isAuthenticated && user) {
|
||||
setUserProfile(user)
|
||||
Navigate('/chores')
|
||||
}
|
||||
}, [])
|
||||
}, [isAuthenticated, user, Navigate])
|
||||
const handleSubmit = async e => {
|
||||
e.preventDefault()
|
||||
|
||||
@@ -144,98 +138,72 @@ const LoginView = () => {
|
||||
? buildChildUsername(parentUsername, childName)
|
||||
: username
|
||||
|
||||
login(actualUsername, 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
|
||||
}
|
||||
const result = await authLogin({ username: actualUsername, password })
|
||||
|
||||
// Normal login without MFA
|
||||
localStorage.setItem('ca_token', data.token)
|
||||
localStorage.setItem('ca_expiration', data.expire)
|
||||
if (result.success) {
|
||||
if (result.data?.mfaRequired) {
|
||||
setMfaSessionToken(result.data.sessionToken)
|
||||
setMfaModalOpen(true)
|
||||
return
|
||||
}
|
||||
|
||||
// Refetch user profile after successful login
|
||||
queryClient.refetchQueries(['userProfile'])
|
||||
// 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('/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 redirectUrl = Cookies.get('ca_redirect')
|
||||
if (redirectUrl && redirectUrl !== '/') {
|
||||
Cookies.remove('ca_redirect')
|
||||
Navigate(redirectUrl)
|
||||
} else {
|
||||
Navigate('/chores')
|
||||
}
|
||||
} else {
|
||||
showError({
|
||||
title: 'Login Failed',
|
||||
message: result.error || 'An error occurred, please try again',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const loggedWithProvider = function (provider, data) {
|
||||
const baseURL = apiManager.getApiURL()
|
||||
|
||||
const loggedWithProvider = async function (provider, data) {
|
||||
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']
|
||||
} else if (data['response'] && data['response']['id_token']) {
|
||||
// Apple Sign In returns id_token in response
|
||||
return data['response']['id_token']
|
||||
} else if (data['id_token']) {
|
||||
// Direct id_token for Apple (fallback)
|
||||
return data['id_token']
|
||||
}
|
||||
}
|
||||
|
||||
return fetch(`${baseURL}/auth/${provider}/callback`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
try {
|
||||
const response = await apiClient.post(`/auth/${provider}/callback`, {
|
||||
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)
|
||||
if (response.ok) {
|
||||
const responseData = await response.json()
|
||||
|
||||
// Check if MFA is required for OAuth login
|
||||
if (responseData.mfaRequired) {
|
||||
setMfaSessionToken(responseData.sessionToken)
|
||||
setMfaModalOpen(true)
|
||||
return
|
||||
}
|
||||
|
||||
// Use new auth system to handle token storage
|
||||
if (responseData.token || responseData.access_token) {
|
||||
const token = responseData.token || responseData.access_token
|
||||
const expiry = responseData.expire || responseData.access_token_expiry
|
||||
|
||||
localStorage.setItem('token', token)
|
||||
if (expiry) {
|
||||
localStorage.setItem('token_expiry', expiry)
|
||||
}
|
||||
|
||||
// Refetch user profile after successful OAuth login
|
||||
queryClient.invalidateQueries(['userProfile'])
|
||||
@@ -247,16 +215,21 @@ const LoginView = () => {
|
||||
} else {
|
||||
getUserProfileAndNavigateToHome()
|
||||
}
|
||||
})
|
||||
}
|
||||
return response.json().then(() => {
|
||||
}
|
||||
} else {
|
||||
const providerName = provider === 'apple' ? 'Apple' : 'Google'
|
||||
showError({
|
||||
title: `${providerName} Login Failed`,
|
||||
message: `Couldn't log in with ${providerName}, please try again`,
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
const providerName = provider === 'apple' ? 'Apple' : 'Google'
|
||||
showError({
|
||||
title: `${providerName} Login Error`,
|
||||
message: 'Network error occurred, please try again',
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
const getUserProfileAndNavigateToHome = () => {
|
||||
// Refetch user profile after login using React Query
|
||||
@@ -273,8 +246,8 @@ const LoginView = () => {
|
||||
}
|
||||
|
||||
const handleMFASuccess = data => {
|
||||
localStorage.setItem('ca_token', data.token)
|
||||
localStorage.setItem('ca_expiration', data.expire)
|
||||
localStorage.setItem('token', data.token)
|
||||
localStorage.setItem('token_expiry', data.expire)
|
||||
setMfaModalOpen(false)
|
||||
setMfaSessionToken('')
|
||||
|
||||
|
||||
@@ -242,7 +242,7 @@ const ActivitiesCard = ({ title = 'Recent Activities' }) => {
|
||||
|
||||
// Refresh function to refetch all data
|
||||
const handleRefresh = async () => {
|
||||
await Promise.all([refetchChores(), refetchHistory(), refetchMembers()])
|
||||
await Promise.all([refetchChores(), refetchHistory, refetchMembers])
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
|
||||
@@ -125,7 +125,7 @@ const MyChores = () => {
|
||||
localStorage.getItem('selectedChoreFilter') || 'anyone',
|
||||
)
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const [performers, setPerformers] = useState([])
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState(null)
|
||||
const [viewMode, setViewMode] = useState(
|
||||
localStorage.getItem('choreCardViewMode') || 'default',
|
||||
@@ -202,7 +202,6 @@ const MyChores = () => {
|
||||
choresData?.res
|
||||
) {
|
||||
const processEffectAsync = async () => {
|
||||
setPerformers(membersData.res)
|
||||
setChores(processedChores)
|
||||
setFilteredChores(processedChores)
|
||||
|
||||
@@ -1022,7 +1021,7 @@ const MyChores = () => {
|
||||
<CardComponent
|
||||
key={key || chore.id}
|
||||
chore={chore}
|
||||
performers={performers}
|
||||
performers={membersData?.res}
|
||||
userLabels={userLabels}
|
||||
onChipClick={handleLabelFiltering}
|
||||
onAction={handleChoreAction}
|
||||
@@ -1508,7 +1507,7 @@ const MyChores = () => {
|
||||
if (
|
||||
isUserProfileLoading ||
|
||||
userLabelsLoading ||
|
||||
performers.length === 0 ||
|
||||
membersLoading ||
|
||||
choresLoading
|
||||
) {
|
||||
return (
|
||||
@@ -1745,7 +1744,7 @@ const MyChores = () => {
|
||||
const filterFunction = FILTERS[filter]
|
||||
const filteredChores =
|
||||
filterFunction.length === 2
|
||||
? filterFunction(chores, userProfile.id)
|
||||
? filterFunction(chores, userProfile?.id)
|
||||
: filterFunction(chores)
|
||||
setFilteredChores(filteredChores)
|
||||
setSearchFilter(filter)
|
||||
@@ -1757,7 +1756,7 @@ const MyChores = () => {
|
||||
color={searchFilter === filter ? 'primary' : 'neutral'}
|
||||
>
|
||||
{FILTERS[filter].length === 2
|
||||
? FILTERS[filter](chores, userProfile.id).length
|
||||
? FILTERS[filter](chores, userProfile?.id).length
|
||||
: FILTERS[filter](chores).length}
|
||||
</Chip>
|
||||
</MenuItem>
|
||||
@@ -2323,7 +2322,7 @@ const MyChores = () => {
|
||||
<CompactChoreCard
|
||||
key={`calendar-${chore.id}`}
|
||||
chore={chore}
|
||||
performers={performers}
|
||||
performers={membersData?.res || []}
|
||||
userLabels={userLabels}
|
||||
onChipClick={handleLabelFiltering}
|
||||
onAction={handleChoreAction}
|
||||
@@ -2501,7 +2500,7 @@ const MyChores = () => {
|
||||
)}
|
||||
</Container>
|
||||
|
||||
<Sidepanel chores={chores} performers={performers} />
|
||||
<Sidepanel chores={chores} performers={membersData?.res || []} />
|
||||
|
||||
{/* Multi-select Help - only show when in multi-select mode */}
|
||||
{/* <MultiSelectHelp isVisible={isMultiSelectMode} /> */}
|
||||
@@ -2537,7 +2536,7 @@ const MyChores = () => {
|
||||
{activeModal === 'changeAssignee' && modalChore && (
|
||||
<SelectModal
|
||||
isOpen={true}
|
||||
options={performers}
|
||||
options={membersData?.res || []}
|
||||
displayKey='displayName'
|
||||
title={`Delegate to someone else`}
|
||||
placeholder={'Select a performer'}
|
||||
|
||||
@@ -18,7 +18,7 @@ import { useNotification } from '../../service/NotificationProvider'
|
||||
import { UpdateUserDetails } from '../../utils/Fetcher'
|
||||
import { resolvePhotoURL } from '../../utils/Helpers'
|
||||
import { getCroppedImg } from '../../utils/imageCropUtils'
|
||||
import { UploadFile } from '../../utils/TokenManager'
|
||||
import { apiClient } from '../../utils/apiClient'
|
||||
import SettingsLayout from './SettingsLayout'
|
||||
|
||||
const ProfileSettings = () => {
|
||||
@@ -85,10 +85,7 @@ const ProfileSettings = () => {
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('file', compressedFile, 'profile.jpg')
|
||||
const response = await UploadFile('/users/profile_photo', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
const response = await apiClient.upload('/users/profile_photo', formData)
|
||||
if (!response.ok) throw new Error('Upload failed')
|
||||
const data = await response.json()
|
||||
const url = resolvePhotoURL(data.url || data.sign)
|
||||
|
||||
@@ -87,12 +87,14 @@ const links = [
|
||||
]
|
||||
|
||||
import { SafeArea } from 'capacitor-plugin-safe-area'
|
||||
import { useAuth } from '../../hooks/useAuth.jsx'
|
||||
import Z_INDEX from '../../constants/zIndex'
|
||||
import { useResource } from '../../queries/ResourceQueries'
|
||||
|
||||
const publicPages = ['/landing', '/privacy', '/terms']
|
||||
const NavBar = () => {
|
||||
const { data: resource } = useResource()
|
||||
const { logout } = useAuth()
|
||||
|
||||
const navigate = useNavigate()
|
||||
const [drawerOpen, setDrawerOpen] = useState(false)
|
||||
@@ -264,12 +266,7 @@ const NavBar = () => {
|
||||
<ListItemContent>Upgrade to Plus</ListItemContent>
|
||||
</ListItemButton> */}
|
||||
<ListItemButton
|
||||
onClick={() => {
|
||||
localStorage.removeItem('ca_token')
|
||||
localStorage.removeItem('ca_expiration')
|
||||
// go to login page:
|
||||
window.location.href = '/login'
|
||||
}}
|
||||
onClick={logout}
|
||||
sx={{
|
||||
py: 1.2,
|
||||
}}
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import { useUserProfile } from '../../queries/UserQueries'
|
||||
import { useNotification } from '../../service/NotificationProvider'
|
||||
import { isPlusAccount, resolvePhotoURL } from '../../utils/Helpers'
|
||||
import { UploadFile } from '../../utils/TokenManager'
|
||||
import { apiClient } from '../../utils/apiClient'
|
||||
import './RichTextEditor.css'
|
||||
|
||||
const RichTextEditor = forwardRef(
|
||||
@@ -106,10 +106,7 @@ const RichTextEditor = forwardRef(
|
||||
formData.append('entityId', entityId)
|
||||
formData.append('entityType', entityType)
|
||||
|
||||
const response = await UploadFile('/assets/chore', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
const response = await apiClient.upload('/assets/chore', formData)
|
||||
|
||||
if (response.status === 507) {
|
||||
showError({
|
||||
|
||||
Reference in New Issue
Block a user