Files
ComfyUI_frontend/src/composables/useErrorHandling.ts
Christian Byrne 10748bdac9 [auth] handle auth/requires-recent-login for account deletion and password updates (#6109)
## Summary

Implemented error recovery system to handle Firebase
`auth/requires-recent-login` errors when deleting accounts or updating
passwords.


https://github.com/user-attachments/assets/92a79e2a-cff5-4b18-b529-dbaf5f2303f2

## Changes

- **What**: Added [ErrorRecoveryStrategy
pattern](https://firebase.google.com/docs/auth/web/manage-users#re-authenticate_a_user)
to `useErrorHandling` composable with automatic retry logic for
sensitive Firebase operations
- **Breaking**: None - recovery strategies are optional, all existing
code unchanged

## Technical Details

Firebase enforces
[reauthentication](https://firebase.google.com/docs/reference/js/auth#autherrorcodes)
for security-sensitive operations (account deletion, password changes)
after ~5 minutes of inactivity. Previously these operations failed with
cryptic error messages.

New flow:
1. Operation throws `auth/requires-recent-login`
2. Recovery strategy shows confirmation dialog
3. User logs out and re-authenticates
4. Operation automatically retries

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6109-auth-handle-auth-requires-recent-login-for-account-deletion-and-password-updates-28f6d73d36508119abf4ce30eecea976)
by [Unito](https://www.unito.io)
2025-10-18 01:24:52 -07:00

113 lines
3.3 KiB
TypeScript

import { t } from '@/i18n'
import { useToastStore } from '@/platform/updates/common/toastStore'
/**
* Strategy for recovering from specific error conditions.
* Allows operations to be retried after resolving the error condition.
*
* @template TArgs - The argument types of the operation to be retried
* @template TReturn - The return type of the operation
*
* @example
* ```typescript
* const networkRecovery: ErrorRecoveryStrategy = {
* shouldHandle: (error) => error instanceof NetworkError,
* recover: async (error, retry) => {
* await waitForNetwork()
* await retry()
* }
* }
* ```
*/
export interface ErrorRecoveryStrategy<
TArgs extends unknown[] = unknown[],
TReturn = unknown
> {
/**
* Determines if this strategy should handle the given error.
* @param error - The error to check
* @returns true if this strategy can handle the error
*/
shouldHandle: (error: unknown) => boolean
/**
* Attempts to recover from the error and retry the operation.
* This function is responsible for:
* 1. Resolving the error condition (e.g., reauthentication, network reconnect)
* 2. Calling retry() to re-execute the original operation
* 3. Handling the retry result (success or failure)
*
* @param error - The error that occurred
* @param retry - Function to retry the original operation
* @param args - Original arguments passed to the operation
* @returns Promise that resolves when recovery completes (whether successful or not)
*/
recover: (
error: unknown,
retry: (...args: TArgs) => Promise<TReturn> | TReturn,
args: TArgs
) => Promise<void>
}
export function useErrorHandling() {
const toast = useToastStore()
const toastErrorHandler = (error: unknown) => {
toast.add({
severity: 'error',
summary: t('g.error'),
detail: error instanceof Error ? error.message : t('g.unknownError')
})
console.error(error)
}
const wrapWithErrorHandling =
<TArgs extends unknown[], TReturn>(
action: (...args: TArgs) => TReturn,
errorHandler?: (error: unknown) => void,
finallyHandler?: () => void
) =>
(...args: TArgs): TReturn | undefined => {
try {
return action(...args)
} catch (e) {
;(errorHandler ?? toastErrorHandler)(e)
} finally {
finallyHandler?.()
}
}
const wrapWithErrorHandlingAsync =
<TArgs extends unknown[], TReturn>(
action: (...args: TArgs) => Promise<TReturn> | TReturn,
errorHandler?: (error: unknown) => void,
finallyHandler?: () => void,
recoveryStrategies: ErrorRecoveryStrategy<TArgs, TReturn>[] = []
) =>
async (...args: TArgs): Promise<TReturn | undefined> => {
try {
return await action(...args)
} catch (e) {
for (const strategy of recoveryStrategies) {
if (strategy.shouldHandle(e)) {
try {
await strategy.recover(e, action, args)
return
} catch (recoveryError) {
console.error('Recovery strategy failed:', recoveryError)
}
}
}
;(errorHandler ?? toastErrorHandler)(e)
} finally {
finallyHandler?.()
}
}
return {
wrapWithErrorHandling,
wrapWithErrorHandlingAsync,
toastErrorHandler
}
}