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:
20
src/App.jsx
20
src/App.jsx
@@ -6,10 +6,10 @@ import { Outlet, useNavigate } from 'react-router-dom'
|
||||
import { useRegisterSW } from 'virtual:pwa-register/react'
|
||||
import { registerCapacitorListeners } from './CapacitorListener'
|
||||
import PageTransition from './components/animations/PageTransition'
|
||||
import { AuthProvider } from './hooks/useAuth.jsx'
|
||||
import { ImpersonateUserProvider } from './contexts/ImpersonateUserContext'
|
||||
import { AuthenticationProvider } from './service/AuthenticationService'
|
||||
import SSEProvider from './contexts/SSEContext'
|
||||
import { useNotification } from './service/NotificationProvider'
|
||||
import { apiManager } from './utils/TokenManager'
|
||||
|
||||
import NetworkBanner from './views/components/NetworkBanner'
|
||||
|
||||
@@ -32,12 +32,6 @@ const startOpenReplay = () => {
|
||||
tracker.start()
|
||||
}
|
||||
|
||||
const startApiManager = async navigate => {
|
||||
await apiManager.init()
|
||||
apiManager.setNavigateToLogin(() => {
|
||||
navigate('/login')
|
||||
})
|
||||
}
|
||||
|
||||
const AppContent = () => {
|
||||
const { showNotification } = useNotification()
|
||||
@@ -100,8 +94,6 @@ const AppContent = () => {
|
||||
}
|
||||
|
||||
function App() {
|
||||
const navigate = useNavigate()
|
||||
startApiManager(navigate)
|
||||
// startOpenReplay()
|
||||
|
||||
const { mode, systemMode } = useColorScheme()
|
||||
@@ -135,9 +127,11 @@ function App() {
|
||||
<>
|
||||
<NetworkBanner />
|
||||
|
||||
<AuthenticationProvider>
|
||||
<AppContent />
|
||||
</AuthenticationProvider>
|
||||
<AuthProvider>
|
||||
<SSEProvider>
|
||||
<AppContent />
|
||||
</SSEProvider>
|
||||
</AuthProvider>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ const UserProfileAvatar = () => {
|
||||
}
|
||||
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem('ca_token')
|
||||
localStorage.removeItem('access_token')
|
||||
localStorage.removeItem('ca_expiration')
|
||||
window.location.href = '/login'
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import { AlertsProvider } from '../service/AlertsProvider'
|
||||
import { NotificationProvider } from '../service/NotificationProvider'
|
||||
import QueryContext from './QueryContext'
|
||||
import RouterContext from './RouterContext'
|
||||
import SSEProvider from './SSEContext'
|
||||
import ThemeContext from './ThemeContext'
|
||||
|
||||
const Contexts = ({ children }) => {
|
||||
@@ -11,7 +10,6 @@ const Contexts = ({ children }) => {
|
||||
ThemeContext,
|
||||
QueryContext,
|
||||
NotificationProvider,
|
||||
SSEProvider,
|
||||
RouterContext,
|
||||
]
|
||||
|
||||
|
||||
144
src/hooks/useAuth.jsx
Normal file
144
src/hooks/useAuth.jsx
Normal file
@@ -0,0 +1,144 @@
|
||||
import { createContext, useContext, useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { API_URL } from '../Config'
|
||||
import { apiClient } from '../utils/ApiClient'
|
||||
|
||||
const AuthContext = createContext(null)
|
||||
|
||||
export const useAuth = () => {
|
||||
const context = useContext(AuthContext)
|
||||
if (!context) {
|
||||
throw new Error('useAuth must be used within an AuthProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
export const AuthProvider = ({ children }) => {
|
||||
const [token, setToken] = useState(() => localStorage.getItem('token'))
|
||||
const [user, setUser] = useState(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const navigate = useNavigate()
|
||||
|
||||
const baseURL = `${API_URL}/api/v1`
|
||||
const isAuthenticated = !!token
|
||||
|
||||
const isTokenExpired = () => {
|
||||
const expiry = localStorage.getItem('token_expiry')
|
||||
if (!expiry) return false
|
||||
return new Date() >= new Date(expiry)
|
||||
}
|
||||
|
||||
const clearAuth = () => {
|
||||
setToken(null)
|
||||
setUser(null)
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('token_expiry')
|
||||
localStorage.removeItem('ca_token')
|
||||
localStorage.removeItem('ca_expiration')
|
||||
localStorage.removeItem('access_token')
|
||||
}
|
||||
|
||||
const login = async credentials => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const response = await fetch(`${baseURL}/auth/login`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(credentials),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
return { success: false, error: error.message || 'Login failed' }
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const userToken = data.token || data.access_token
|
||||
|
||||
if (userToken) {
|
||||
setToken(userToken)
|
||||
localStorage.setItem('token', userToken)
|
||||
|
||||
if (data.expire || data.access_token_expiry) {
|
||||
localStorage.setItem(
|
||||
'token_expiry',
|
||||
data.expire || data.access_token_expiry,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
setIsLoading(false)
|
||||
return { success: true, data }
|
||||
} catch (error) {
|
||||
setIsLoading(false)
|
||||
return { success: false, error: 'Network error' }
|
||||
}
|
||||
}
|
||||
|
||||
const logout = async () => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
await fetch(`${baseURL}/auth/logout`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn('Logout API call failed:', error)
|
||||
} finally {
|
||||
clearAuth()
|
||||
setIsLoading(false)
|
||||
navigate('/login')
|
||||
}
|
||||
}
|
||||
|
||||
const fetchUser = async () => {
|
||||
if (!token) return null
|
||||
|
||||
try {
|
||||
const response = await apiClient.get('/users/profile')
|
||||
|
||||
if (!response.ok) {
|
||||
return null
|
||||
}
|
||||
|
||||
const userData = await response.json()
|
||||
setUser(userData)
|
||||
return userData
|
||||
} catch (error) {
|
||||
console.error('Fetch user error:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const initAuth = async () => {
|
||||
if (token && !isTokenExpired()) {
|
||||
await fetchUser()
|
||||
} else if (token && isTokenExpired()) {
|
||||
// Token is expired, but don't refresh here
|
||||
// Let the first API call handle refresh via ApiClient
|
||||
// Just try to fetch user - if it fails, ApiClient will handle refresh
|
||||
await fetchUser()
|
||||
} else {
|
||||
clearAuth()
|
||||
navigate('/login')
|
||||
}
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
initAuth()
|
||||
}, [token, navigate])
|
||||
|
||||
const value = {
|
||||
token,
|
||||
user,
|
||||
isLoading,
|
||||
isAuthenticated,
|
||||
login,
|
||||
logout,
|
||||
fetchUser,
|
||||
}
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
|
||||
}
|
||||
@@ -4,7 +4,8 @@ import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useUserProfile } from '../queries/UserQueries'
|
||||
import { useAlerts } from '../service/AlertsProvider'
|
||||
import { useNotification } from '../service/NotificationProvider'
|
||||
import { apiManager, isTokenValid } from '../utils/TokenManager'
|
||||
import { apiClient } from '../utils/apiClient'
|
||||
import { useAuth } from './useAuth.jsx'
|
||||
const SSE_STATES = {
|
||||
CONNECTING: 0,
|
||||
OPEN: 1,
|
||||
@@ -17,6 +18,7 @@ const CIRCUIT_BREAKER_RESET_TIME = 600000 // 10 minutes
|
||||
|
||||
export const useSSE = () => {
|
||||
const { data: userProfile } = useUserProfile()
|
||||
const { isAuthenticated, token } = useAuth()
|
||||
const [connectionState, setConnectionState] = useState(SSE_STATES.CLOSED)
|
||||
const [lastEvent, setLastEvent] = useState(null)
|
||||
const [error, setError] = useState(null)
|
||||
@@ -34,14 +36,14 @@ export const useSSE = () => {
|
||||
const { showAlert } = useAlerts()
|
||||
|
||||
const getSSEUrl = useCallback(() => {
|
||||
const token = localStorage.getItem('ca_token')
|
||||
if (!token || !isTokenValid()) {
|
||||
const authToken = token
|
||||
if (!authToken || !isAuthenticated) {
|
||||
console.log('SSE: No valid authentication token')
|
||||
return null
|
||||
}
|
||||
|
||||
// Get the API URL from apiManager
|
||||
const apiUrl = apiManager.getApiURL() // e.g., "http://localhost:8080/api/v1"
|
||||
const apiUrl = apiClient.baseURL // e.g., "http://localhost:8080/api/v1"
|
||||
|
||||
// Build SSE URL - let backend determine circle from authenticated user
|
||||
const sseUrl = `${apiUrl}/realtime/sse`
|
||||
@@ -302,6 +304,7 @@ export const useSSE = () => {
|
||||
'Cache-Control': 'no-cache',
|
||||
Accept: 'text/event-stream',
|
||||
},
|
||||
withCredentials: true,
|
||||
// Increase timeout to prevent premature disconnections
|
||||
// Default is 45000ms (45s), increasing to 2 minutes
|
||||
// TODO: send this in the resource object so it can be configured per instance
|
||||
@@ -447,10 +450,10 @@ export const useSSE = () => {
|
||||
enabled => {
|
||||
console.log('SSE toggleSSEEnabled called:', {
|
||||
enabled,
|
||||
isTokenValid: isTokenValid(),
|
||||
isTokenValid: isAuthenticated,
|
||||
})
|
||||
localStorage.setItem('sse_enabled', enabled.toString())
|
||||
if (enabled && isTokenValid()) {
|
||||
if (enabled && isAuthenticated) {
|
||||
console.log('SSE toggleSSEEnabled: Calling connect()')
|
||||
connect()
|
||||
} else {
|
||||
@@ -468,13 +471,13 @@ export const useSSE = () => {
|
||||
// Auto-connect when SSE is enabled and token is valid
|
||||
useEffect(() => {
|
||||
console.log('SSE auto-connect effect triggered')
|
||||
console.log('Token valid:', isTokenValid())
|
||||
console.log('Token valid:', isAuthenticated)
|
||||
|
||||
// Check if SSE is enabled in settings
|
||||
const isSSEEnabledSetting = localStorage.getItem('sse_enabled') === 'true'
|
||||
console.log('SSE enabled in settings:', isSSEEnabledSetting)
|
||||
|
||||
if (isTokenValid() && isSSEEnabledSetting) {
|
||||
if (isAuthenticated && isSSEEnabledSetting) {
|
||||
console.log('SSE: Conditions met, attempting to connect')
|
||||
connect()
|
||||
} else {
|
||||
@@ -514,7 +517,7 @@ export const useSSE = () => {
|
||||
const isSSEEnabledSetting =
|
||||
localStorage.getItem('sse_enabled') === 'true'
|
||||
if (
|
||||
isTokenValid() &&
|
||||
isAuthenticated &&
|
||||
isSSEEnabledSetting &&
|
||||
connectionState !== SSE_STATES.OPEN
|
||||
) {
|
||||
|
||||
@@ -6,7 +6,17 @@ import {
|
||||
GetDeviceTokens,
|
||||
GetUserProfile,
|
||||
} from '../utils/Fetcher'
|
||||
import { isTokenValid } from '../utils/TokenManager'
|
||||
|
||||
// Helper to check if we have a valid token
|
||||
const isTokenValid = () => {
|
||||
const token = localStorage.getItem('token')
|
||||
if (!token) return false
|
||||
|
||||
const expiry = localStorage.getItem('token_expiry')
|
||||
if (!expiry) return true // No expiry set, assume valid
|
||||
|
||||
return new Date() < new Date(expiry)
|
||||
}
|
||||
|
||||
export const useAllUsers = () => {
|
||||
return useQuery({
|
||||
@@ -37,16 +47,16 @@ export const useUserProfile = () => {
|
||||
queryKey: ['userProfile'],
|
||||
queryFn: async () => {
|
||||
if (!isTokenValid()) {
|
||||
return null // Token is invalid, return null to indicate no profile
|
||||
throw new Error('Invalid or expired token, cannot fetch user profile')
|
||||
}
|
||||
const resp = await GetUserProfile()
|
||||
const result = await resp.json()
|
||||
// if we got 403 then user probably deleted their account and token is still valid. navigate to login
|
||||
if (resp.status === 403) {
|
||||
localStorage.removeItem('ca_token')
|
||||
localStorage.removeItem('access_token')
|
||||
localStorage.removeItem('ca_expiration')
|
||||
window.location.href = '/login'
|
||||
return null
|
||||
throw new Error('User account deleted or access forbidden')
|
||||
}
|
||||
|
||||
return result.res // Return the actual user profile data
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { createContext, useState } from 'react'
|
||||
|
||||
const AuthenticationContext = createContext({})
|
||||
|
||||
const AuthenticationProvider = ({ children }) => {
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(false)
|
||||
const [userProfile, setUserProfile] = useState({})
|
||||
return (
|
||||
<AuthenticationContext.Provider
|
||||
value={{ isLoggedIn, setIsLoggedIn, userProfile, setUserProfile }}
|
||||
>
|
||||
{children}
|
||||
</AuthenticationContext.Provider>
|
||||
)
|
||||
}
|
||||
export { AuthenticationContext, AuthenticationProvider }
|
||||
|
||||
// export default AuthenticationProvider;
|
||||
180
src/utils/ApiClient.js
Normal file
180
src/utils/ApiClient.js
Normal file
@@ -0,0 +1,180 @@
|
||||
import { API_URL } from '../Config'
|
||||
import { RefreshToken } from './Fetcher'
|
||||
|
||||
class ApiClient {
|
||||
constructor() {
|
||||
this.baseURL = `${API_URL}/api/v1`
|
||||
this.isRefreshing = false
|
||||
this.failedQueue = []
|
||||
}
|
||||
|
||||
getToken() {
|
||||
return localStorage.getItem('token')
|
||||
}
|
||||
|
||||
getHeaders(customHeaders = {}) {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
...customHeaders,
|
||||
}
|
||||
|
||||
const token = this.getToken()
|
||||
if (token) {
|
||||
headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
|
||||
const impersonateUserId = localStorage.getItem('impersonatedUserId')
|
||||
if (impersonateUserId) {
|
||||
headers['X-Impersonate-User-ID'] = impersonateUserId
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
// Process queued requests after refresh attempt
|
||||
processQueue(error, token = null) {
|
||||
this.failedQueue.forEach(({ resolve, reject }) => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
} else {
|
||||
resolve(token)
|
||||
}
|
||||
})
|
||||
|
||||
this.failedQueue = []
|
||||
}
|
||||
|
||||
// Helper to avoid repeating cleanup code
|
||||
handleLogout() {
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('token_expiry')
|
||||
window.location.href = '/login'
|
||||
}
|
||||
async request(endpoint, options = {}) {
|
||||
const url = `${this.baseURL}${endpoint}`
|
||||
const config = {
|
||||
// credentials: 'include',
|
||||
...options,
|
||||
headers: this.getHeaders(options.headers),
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. Initial Request
|
||||
let response = await fetch(url, config)
|
||||
|
||||
// 2. Check for 401 (Unauthorized)
|
||||
if (response.status === 401) {
|
||||
if (!this.isRefreshing) {
|
||||
this.isRefreshing = true
|
||||
|
||||
try {
|
||||
// Attempt to refresh token
|
||||
const refreshReq = await RefreshToken()
|
||||
|
||||
if (refreshReq.ok) {
|
||||
const data = await refreshReq.json()
|
||||
const newToken = data.token || data.access_token
|
||||
|
||||
// Update Local Storage
|
||||
localStorage.setItem('token', newToken)
|
||||
if (data.expire || data.access_token_expiry) {
|
||||
localStorage.setItem(
|
||||
'token_expiry',
|
||||
data.expire || data.access_token_expiry,
|
||||
)
|
||||
}
|
||||
|
||||
// Process queue with success
|
||||
this.processQueue(null, newToken)
|
||||
|
||||
// Retry the original request with new token
|
||||
const newHeaders = this.getHeaders(options?.headers)
|
||||
const retryConfig = {
|
||||
...config,
|
||||
headers: newHeaders,
|
||||
}
|
||||
|
||||
response = await fetch(url, retryConfig)
|
||||
|
||||
// If it fails again with 401, force logout
|
||||
if (response.status === 401) {
|
||||
this.handleLogout()
|
||||
return null
|
||||
}
|
||||
} else {
|
||||
// Refresh failed (e.g., refresh token expired)
|
||||
this.processQueue(new Error('Token refresh failed'), null)
|
||||
this.handleLogout()
|
||||
return null
|
||||
}
|
||||
} finally {
|
||||
this.isRefreshing = false
|
||||
}
|
||||
} else {
|
||||
// Token is currently being refreshed, queue this request
|
||||
return new Promise((resolve, reject) => {
|
||||
this.failedQueue.push({
|
||||
resolve: (token) => {
|
||||
// Retry the original request with new token
|
||||
const newHeaders = this.getHeaders(options?.headers)
|
||||
const retryConfig = {
|
||||
...config,
|
||||
headers: newHeaders,
|
||||
}
|
||||
resolve(fetch(url, retryConfig))
|
||||
},
|
||||
reject
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('Request failed', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async get(endpoint, options = {}) {
|
||||
return this.request(endpoint, { ...options, method: 'GET' })
|
||||
}
|
||||
|
||||
async post(endpoint, data, options = {}) {
|
||||
return this.request(endpoint, {
|
||||
...options,
|
||||
method: 'POST',
|
||||
body: data ? JSON.stringify(data) : undefined,
|
||||
})
|
||||
}
|
||||
|
||||
async put(endpoint, data, options = {}) {
|
||||
return this.request(endpoint, {
|
||||
...options,
|
||||
method: 'PUT',
|
||||
body: data ? JSON.stringify(data) : undefined,
|
||||
})
|
||||
}
|
||||
|
||||
async delete(endpoint, options = {}) {
|
||||
return this.request(endpoint, { ...options, method: 'DELETE' })
|
||||
}
|
||||
|
||||
async upload(endpoint, formData, options = {}) {
|
||||
const headers = options.headers || {}
|
||||
delete headers['Content-Type']
|
||||
|
||||
return this.request(endpoint, {
|
||||
...options,
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers,
|
||||
})
|
||||
}
|
||||
|
||||
getAssetURL(path) {
|
||||
return `${this.baseURL}/assets/${path}`
|
||||
}
|
||||
}
|
||||
|
||||
export const apiClient = new ApiClient()
|
||||
@@ -100,8 +100,8 @@ export const isOfficialDonetickInstance = async () => {
|
||||
export const isOfficialDonetickInstanceSync = () => {
|
||||
try {
|
||||
// Dynamic import to avoid circular dependencies
|
||||
return import('../utils/TokenManager').then(({ apiManager }) => {
|
||||
const currentApiUrl = apiManager.getApiURL()
|
||||
return import('./apiClient').then(({ apiClient }) => {
|
||||
const currentApiUrl = apiClient.baseURL
|
||||
// Check if the API URL contains donetick.com
|
||||
return currentApiUrl.toLowerCase().includes('donetick.com')
|
||||
}).catch(error => {
|
||||
|
||||
@@ -1,4 +1,18 @@
|
||||
import { Fetch, HEADERS, apiManager } from './TokenManager'
|
||||
import { apiClient } from './apiClient'
|
||||
|
||||
// Migration helpers to maintain compatibility with existing code
|
||||
const Fetch = async (endpoint, options = {}) => {
|
||||
const response = await apiClient.request(endpoint, options)
|
||||
return response
|
||||
}
|
||||
|
||||
const HEADERS = () => {
|
||||
return apiClient.getHeaders()
|
||||
}
|
||||
|
||||
const apiManager = {
|
||||
getApiURL: () => apiClient.baseURL,
|
||||
}
|
||||
|
||||
const createChore = userID => {
|
||||
return Fetch(`/chores/`, {
|
||||
@@ -547,12 +561,36 @@ const RedeemPoints = (userId, points, circleID) => {
|
||||
body: JSON.stringify({ points, userId }),
|
||||
})
|
||||
}
|
||||
const RefreshToken = () => {
|
||||
const RefreshToken = async () => {
|
||||
const basedURL = apiManager.getApiURL()
|
||||
return fetch(`${basedURL}/auth/refresh`, {
|
||||
method: 'GET',
|
||||
headers: HEADERS(),
|
||||
})
|
||||
|
||||
// Check if running on native platform
|
||||
const isNative = typeof window !== 'undefined' && window.Capacitor?.isNativePlatform?.()
|
||||
|
||||
if (isNative) {
|
||||
// For native platforms, send refresh token in request body
|
||||
const { Preferences } = await import('@capacitor/preferences')
|
||||
const { value: refreshToken } = await Preferences.get({ key: 'refresh_token' })
|
||||
|
||||
if (!refreshToken) {
|
||||
throw new Error('No refresh token available')
|
||||
}
|
||||
|
||||
return fetch(`${basedURL}/auth/refresh`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ refresh_token: refreshToken }),
|
||||
})
|
||||
} else {
|
||||
// For web, continue using cookies
|
||||
return fetch(`${basedURL}/auth/refresh`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: HEADERS(),
|
||||
})
|
||||
}
|
||||
}
|
||||
const GetChoresHistory = async (limit, includeMembers) => {
|
||||
var url = `/chores/history`
|
||||
@@ -757,6 +795,7 @@ export {
|
||||
CreateBackup,
|
||||
CreateChildUser,
|
||||
CreateChore,
|
||||
createChore,
|
||||
CreateLabel,
|
||||
CreateLongLiveToken,
|
||||
CreateThing,
|
||||
@@ -777,10 +816,10 @@ export {
|
||||
GetChoreByID,
|
||||
GetChoreDetailById,
|
||||
GetChoreHistory,
|
||||
GetChoreTimer,
|
||||
GetChores,
|
||||
GetChoresHistory,
|
||||
GetChoresNew,
|
||||
GetChoreTimer,
|
||||
GetCircleMemberRequests,
|
||||
GetDeviceTokens,
|
||||
GetLabels,
|
||||
@@ -795,6 +834,7 @@ export {
|
||||
GetUserProfile,
|
||||
JoinCircle,
|
||||
LeaveCircle,
|
||||
login,
|
||||
MarkChoreComplete,
|
||||
NudgeChore,
|
||||
PauseChore,
|
||||
@@ -811,6 +851,7 @@ export {
|
||||
SaveChore,
|
||||
SaveThing,
|
||||
SetupMFA,
|
||||
signUp,
|
||||
SkipChore,
|
||||
StartChore,
|
||||
UnArchiveChore,
|
||||
@@ -828,7 +869,4 @@ export {
|
||||
UpdateTimeSession,
|
||||
UpdateUserDetails,
|
||||
VerifyMFA,
|
||||
createChore,
|
||||
login,
|
||||
signUp,
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import moment from 'moment'
|
||||
import { getAssetURL } from './TokenManager'
|
||||
import { apiClient } from './apiClient'
|
||||
|
||||
const isPlusAccount = userProfile => {
|
||||
return userProfile?.expiration && moment(userProfile?.expiration).isAfter()
|
||||
@@ -11,7 +11,7 @@ const resolvePhotoURL = url => {
|
||||
return url
|
||||
}
|
||||
if (url.startsWith('assets')) {
|
||||
return getAssetURL(url)
|
||||
return apiClient.getAssetURL(url)
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
@@ -1,247 +0,0 @@
|
||||
import { Preferences } from '@capacitor/preferences'
|
||||
import Cookies from 'js-cookie'
|
||||
import murmurhash from 'murmurhash'
|
||||
import { API_URL } from '../Config'
|
||||
import { FEATURES, isFeatureEnabled } from '../utils/FeatureToggle'
|
||||
import { networkManager } from '../hooks/NetworkManager'
|
||||
import { RefreshToken } from './Fetcher'
|
||||
import { localStore } from './LocalStore'
|
||||
|
||||
class ApiManager {
|
||||
constructor() {
|
||||
this.customServerURL = `${API_URL}/api/v1`
|
||||
this.initialized = false
|
||||
this.initPromise = null
|
||||
this.navigateToLogin = () => {}
|
||||
}
|
||||
|
||||
async init() {
|
||||
if (this.initPromise) {
|
||||
return this.initPromise
|
||||
}
|
||||
|
||||
if (this.initialized) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
this.initPromise = this._doInit()
|
||||
return this.initPromise
|
||||
}
|
||||
|
||||
async _doInit() {
|
||||
const { value: serverURL } = await Preferences.get({
|
||||
key: 'customServerUrl',
|
||||
})
|
||||
|
||||
this.customServerURL = `${serverURL || API_URL}/api/v1`
|
||||
this.initialized = true
|
||||
await localStore.initDatabase()
|
||||
}
|
||||
|
||||
getApiURL() {
|
||||
return this.customServerURL
|
||||
}
|
||||
|
||||
updateApiURL(url) {
|
||||
this.customServerURL = url
|
||||
this.init()
|
||||
}
|
||||
setNavigateToLogin(callback) {
|
||||
this.navigateToLogin = callback
|
||||
}
|
||||
}
|
||||
|
||||
export const apiManager = new ApiManager()
|
||||
|
||||
export const getAssetURL = path => {
|
||||
const baseURL = apiManager.getApiURL()
|
||||
return `${baseURL}/assets/${path}`
|
||||
}
|
||||
export async function UploadFile(url, options) {
|
||||
await apiManager.init()
|
||||
|
||||
if (!isTokenValid()) {
|
||||
Cookies.set('ca_redirect', window.location.pathname)
|
||||
window.location.href = '/login'
|
||||
}
|
||||
|
||||
if (!options) {
|
||||
options = {}
|
||||
}
|
||||
const headers = HEADERS()
|
||||
options.headers = { Authorization: headers['Authorization'] }
|
||||
|
||||
const baseURL = apiManager.getApiURL()
|
||||
const fullURL = `${baseURL}${url}`
|
||||
|
||||
return fetch(fullURL, options)
|
||||
}
|
||||
|
||||
export async function Fetch(url, options) {
|
||||
await apiManager.init()
|
||||
if (!isTokenValid()) {
|
||||
Cookies.set('ca_redirect', window.location.pathname)
|
||||
if (window.location.pathname !== '/login') {
|
||||
window.location.href = '/login'
|
||||
}
|
||||
}
|
||||
|
||||
if (!options) {
|
||||
options = {}
|
||||
}
|
||||
// clone options to avoid mutation
|
||||
options.headers = { ...options.headers, ...HEADERS() }
|
||||
|
||||
const baseURL = apiManager.getApiURL()
|
||||
const fullURL = `${baseURL}${url}`
|
||||
|
||||
// const networkStatus = await Network.getStatus()
|
||||
|
||||
// if (!networkStatus.connected) {
|
||||
// return handleOfflineRequest(fullURL, options)
|
||||
// }
|
||||
|
||||
// Online: Perform the fetch
|
||||
try {
|
||||
const response = await fetch(fullURL, options)
|
||||
|
||||
if (response.ok) {
|
||||
// Only cache data if offline mode is enabled
|
||||
if (isFeatureEnabled(FEATURES.OFFLINE_MODE)) {
|
||||
const data = await response.clone().json()
|
||||
const optionWithoutToken = { ...options }
|
||||
delete optionWithoutToken.headers.Authorization
|
||||
const optionsHash = murmurhash.v3(JSON.stringify(optionWithoutToken))
|
||||
await localStore.saveToCache(fullURL + optionsHash, data)
|
||||
}
|
||||
networkManager.setOnline()
|
||||
} else if (response.status === 401) {
|
||||
// Handle 401 Unauthorized
|
||||
const errorData = await response.json()
|
||||
console.error('Unauthorized:', errorData)
|
||||
localStorage.removeItem('ca_token')
|
||||
localStorage.removeItem('ca_expiration')
|
||||
apiManager.navigateToLogin()
|
||||
} else if (
|
||||
response.status === 503 ||
|
||||
response.type === 'opaque' ||
|
||||
response.status === 0
|
||||
) {
|
||||
networkManager.setOffline()
|
||||
// Only handle offline requests if offline mode is enabled
|
||||
if (isFeatureEnabled(FEATURES.OFFLINE_MODE)) {
|
||||
return handleOfflineRequest(fullURL, options)
|
||||
}
|
||||
// If offline mode is disabled, just throw the error
|
||||
throw new Error(`Request failed with status ${response.status}`)
|
||||
}
|
||||
// return promise that resolves to response object:
|
||||
return Promise.resolve(response)
|
||||
} catch (error) {
|
||||
networkManager.setOffline()
|
||||
console.error('Fetch error:', error)
|
||||
// Only handle offline requests if offline mode is enabled
|
||||
if (isFeatureEnabled(FEATURES.OFFLINE_MODE)) {
|
||||
return handleOfflineRequest(fullURL, options)
|
||||
}
|
||||
// If offline mode is disabled, just throw the error
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export const HEADERS = () => {
|
||||
// Import here to avoid circular dependency issues
|
||||
const impersonateUserId = localStorage.getItem('impersonatedUserId')
|
||||
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: 'Bearer ' + localStorage.getItem('ca_token'),
|
||||
...(impersonateUserId && { 'X-Impersonate-User-ID': impersonateUserId }),
|
||||
}
|
||||
}
|
||||
|
||||
export const isTokenValid = () => {
|
||||
const expiration = localStorage.getItem('ca_expiration')
|
||||
const token = localStorage.getItem('ca_token')
|
||||
|
||||
if (token) {
|
||||
const now = new Date()
|
||||
const expire = new Date(expiration)
|
||||
if (now < expire) {
|
||||
if (now.getTime() + 24 * 60 * 60 * 1000 > expire.getTime()) {
|
||||
refreshAccessToken()
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
localStorage.removeItem('ca_token')
|
||||
localStorage.removeItem('ca_expiration')
|
||||
}
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export const refreshAccessToken = () => {
|
||||
RefreshToken().then(res => {
|
||||
if (res.status === 200) {
|
||||
res.json().then(data => {
|
||||
localStorage.setItem('ca_token', data.token)
|
||||
localStorage.setItem('ca_expiration', data.expire)
|
||||
})
|
||||
} else {
|
||||
return res.json().then(error => {
|
||||
console.log(error)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function handleOfflineRequest(url, options) {
|
||||
// Only handle offline requests if offline mode is enabled
|
||||
if (!isFeatureEnabled(FEATURES.OFFLINE_MODE)) {
|
||||
throw new Error('Network request failed and offline mode is disabled')
|
||||
}
|
||||
|
||||
// if get request then attempt to fetch from cache otherewise queue it :
|
||||
if (options.method === 'GET') {
|
||||
return attemptFetchFromCache(url, options)
|
||||
} else {
|
||||
// Queue the request for later processing
|
||||
const requestId = murmurhash.v3(JSON.stringify({ url, options }))
|
||||
await localStore.queueRequest(requestId, { url, options })
|
||||
console.log('Request queued for later processing:', requestId)
|
||||
return Promise.reject({
|
||||
error: 'Offline and request queued',
|
||||
requestId,
|
||||
queued: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
async function attemptFetchFromCache(url, options) {
|
||||
// Only attempt cache fetch if offline mode is enabled
|
||||
if (!isFeatureEnabled(FEATURES.OFFLINE_MODE)) {
|
||||
throw new Error('Cache access disabled - offline mode is not enabled')
|
||||
}
|
||||
|
||||
const optionsHash = murmurhash.v3(JSON.stringify(options))
|
||||
const cachedData = await localStore.getFromCache(url + optionsHash)
|
||||
networkManager.setOffline()
|
||||
|
||||
if (cachedData) {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => cachedData,
|
||||
})
|
||||
} else {
|
||||
// TODO: change this to throw error instead of returning promise
|
||||
return Promise.reject(
|
||||
new Error(
|
||||
'No cached data found for URL: ' +
|
||||
url +
|
||||
' with options hash: ' +
|
||||
optionsHash,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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