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:
Mo Tarbin
2025-12-25 22:54:39 -05:00
parent 1123fb3ca6
commit 8edd3774d2
20 changed files with 493 additions and 428 deletions

View File

@@ -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')

View File

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

View File

@@ -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('')