diff --git a/src/application/services/js-services/http/api-client.ts b/src/application/services/js-services/http/api-client.ts deleted file mode 100644 index 77569f1e..00000000 --- a/src/application/services/js-services/http/api-client.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { AxiosRequestConfig } from 'axios'; -import { getAxiosInstance } from './http_api'; - -/** - * Type-safe API client wrapper that guarantees response data exists - * These functions work with the interceptor to provide clean API access - */ - -function ensureAxiosInstance() { - const instance = getAxiosInstance(); - - if (!instance) { - throw new Error('Axios instance not initialized'); - } - - return instance; -} - -/** - * GET request with guaranteed response data - */ -export async function apiGet(url: string, config?: AxiosRequestConfig): Promise { - const instance = ensureAxiosInstance(); - const response = await instance.get(url, config); - - return response.data; -} - -/** - * POST request with guaranteed response data - */ -export async function apiPost(url: string, data?: unknown, config?: AxiosRequestConfig): Promise { - const instance = ensureAxiosInstance(); - const response = await instance.post(url, data, config); - - return response.data; -} - -/** - * PUT request with guaranteed response data - */ -export async function apiPut(url: string, data?: unknown, config?: AxiosRequestConfig): Promise { - const instance = ensureAxiosInstance(); - const response = await instance.put(url, data, config); - - return response.data; -} - -/** - * PATCH request with guaranteed response data - */ -export async function apiPatch(url: string, data?: unknown, config?: AxiosRequestConfig): Promise { - const instance = ensureAxiosInstance(); - const response = await instance.patch(url, data, config); - - return response.data; -} - -/** - * DELETE request with guaranteed response data - */ -export async function apiDelete(url: string, config?: AxiosRequestConfig): Promise { - const instance = ensureAxiosInstance(); - const response = await instance.delete(url, config); - - return response.data; -} - -/** - * HEAD request with guaranteed response data - */ -export async function apiHead(url: string, config?: AxiosRequestConfig): Promise { - const instance = ensureAxiosInstance(); - const response = await instance.head(url, config); - - return response.data; -} - -/** - * For void responses (no data expected) - */ -export async function apiGetVoid(url: string, config?: AxiosRequestConfig): Promise { - await apiGet(url, config); -} - -export async function apiPostVoid(url: string, data?: unknown, config?: AxiosRequestConfig): Promise { - await apiPost(url, data, config); -} - -export async function apiPutVoid(url: string, data?: unknown, config?: AxiosRequestConfig): Promise { - await apiPut(url, data, config); -} - -export async function apiPatchVoid(url: string, data?: unknown, config?: AxiosRequestConfig): Promise { - await apiPatch(url, data, config); -} - -export async function apiDeleteVoid(url: string, config?: AxiosRequestConfig): Promise { - await apiDelete(url, config); -} \ No newline at end of file diff --git a/src/application/services/js-services/http/api-utils.ts b/src/application/services/js-services/http/api-utils.ts deleted file mode 100644 index a8bf072c..00000000 --- a/src/application/services/js-services/http/api-utils.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { AppResponseError, ErrorCode, apiErrorHandler } from './error-handler'; - -export interface ApiResponse { - code: ErrorCode; - data?: T; - message: string; -} - -/** - * Process API response and handle errors consistently - */ -export async function processApiResponse( - response: ApiResponse, - options?: { - showNotification?: boolean; - throwError?: boolean; - customMessage?: string; - onAuthError?: () => void; - onPermissionError?: () => void; - } -): Promise { - if (response.code === ErrorCode.Ok) { - if (response.data === undefined) { - const error: AppResponseError = { - code: ErrorCode.MissingPayload, - message: 'Response data is missing' - }; - - await apiErrorHandler.handleError(error, options); - throw error; - } - - return response.data; - } - - const error: AppResponseError = { - code: Object.values(ErrorCode).includes(response.code) ? response.code : ErrorCode.Unhandled, - message: response.message || 'An error occurred' - }; - - await apiErrorHandler.handleError(error, options); - throw error; -} - -/** - * Process void API response (no data expected) - */ -export async function processVoidApiResponse( - response: ApiResponse, - options?: Parameters[1] -): Promise { - if (response.code === ErrorCode.Ok) { - return; - } - - const error: AppResponseError = { - code: Object.values(ErrorCode).includes(response.code) ? response.code : ErrorCode.Unhandled, - message: response.message || 'An error occurred' - }; - - await apiErrorHandler.handleError(error, options); - throw error; -} - -/** - * Internal API call wrapper with automatic error handling - */ -async function processApiCall( - fn: () => Promise>, - options?: Parameters[1] -): Promise { - try { - const response = await fn(); - - return await processApiResponse(response, options); - } catch (error: unknown) { - // If it's already an AppResponseError, just re-throw - if ((error as AppResponseError)?.code !== undefined) { - throw error; - } - - // Otherwise, wrap in a generic error - const apiError: AppResponseError = { - code: ErrorCode.Unhandled, - message: (error as Error)?.message || 'An unexpected error occurred' - }; - - await apiErrorHandler.handleError(apiError, options); - throw apiError; - } -} - -/** - * API call with mapping function for easy data transformation - */ -export async function apiCall( - fn: () => Promise | undefined>, - mapper: (data: T) => R, - options?: Parameters[1] -): Promise; - -/** - * API call for void responses (no mapper needed) - */ -export async function apiCall( - fn: () => Promise | undefined>, - options?: Parameters[1] -): Promise; - -export async function apiCall( - fn: () => Promise | undefined>, - mapperOrOptions?: ((data: T) => R) | Parameters[1], - options?: Parameters[1] -): Promise { - const wrappedFn = async (): Promise> => { - const response = await fn(); - - if (!response) { - throw new Error('No response data'); - } - - return response; - }; - - // If second parameter is a function, it's a mapper - if (typeof mapperOrOptions === 'function') { - const data = await processApiCall(wrappedFn, options); - - return mapperOrOptions(data); - } - - // Otherwise, it's options (void response) - await processApiCall(wrappedFn, mapperOrOptions); -} diff --git a/src/application/services/js-services/http/axios-config.ts b/src/application/services/js-services/http/axios-config.ts deleted file mode 100644 index 86904623..00000000 --- a/src/application/services/js-services/http/axios-config.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { AxiosRequestConfig } from 'axios'; - -/** - * Extended axios config that includes custom options - */ -export interface ExtendedAxiosConfig extends AxiosRequestConfig { - /** - * Whether to show error notifications for this request - * Default: false - */ - showNotification?: boolean; - - /** - * Custom error message to show instead of the default API error message - */ - customErrorMessage?: string; - - /** - * Whether to throw errors or silently handle them - * Default: true - */ - throwError?: boolean; -} - -/** - * Helper function to create axios config with notification enabled - */ -export function withNotification(config?: ExtendedAxiosConfig): ExtendedAxiosConfig { - return { - ...config, - showNotification: true, - }; -} - -/** - * Helper function to create axios config with custom error message - */ -export function withCustomError(message: string, config?: ExtendedAxiosConfig): ExtendedAxiosConfig { - return { - ...config, - customErrorMessage: message, - showNotification: true, - }; -} \ No newline at end of file diff --git a/src/application/services/js-services/http/axios-raw.ts b/src/application/services/js-services/http/axios-raw.ts deleted file mode 100644 index 0c6e3afd..00000000 --- a/src/application/services/js-services/http/axios-raw.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { AxiosRequestConfig, AxiosResponse } from 'axios'; -import { getAxiosInstance } from './http_api'; - -/** - * Raw axios access for endpoints that don't return ApiResponse format - * These bypass the ApiResponse processing in the interceptor - */ - -function ensureAxiosInstance() { - const instance = getAxiosInstance(); - - if (!instance) { - throw new Error('Axios instance not initialized'); - } - - return instance; -} - -/** - * Raw GET request - returns full axios response without ApiResponse processing - * Use this for endpoints that return raw data, blobs, or non-standard formats - */ -export async function rawGet(url: string, config?: AxiosRequestConfig): Promise> { - const instance = ensureAxiosInstance(); - - return await instance.get(url, config); -} - -/** - * Raw POST request - returns full axios response without ApiResponse processing - */ -export async function rawPost(url: string, data?: unknown, config?: AxiosRequestConfig): Promise> { - const instance = ensureAxiosInstance(); - - return await instance.post(url, data, config); -} - -/** - * Raw PUT request - returns full axios response without ApiResponse processing - */ -export async function rawPut(url: string, data?: unknown, config?: AxiosRequestConfig): Promise> { - const instance = ensureAxiosInstance(); - - return await instance.put(url, data, config); -} - -/** - * Raw PATCH request - returns full axios response without ApiResponse processing - */ -export async function rawPatch(url: string, data?: unknown, config?: AxiosRequestConfig): Promise> { - const instance = ensureAxiosInstance(); - - return await instance.patch(url, data, config); -} - -/** - * Raw DELETE request - returns full axios response without ApiResponse processing - */ -export async function rawDelete(url: string, config?: AxiosRequestConfig): Promise> { - const instance = ensureAxiosInstance(); - - return await instance.delete(url, config); -} - -/** - * Download file as blob - */ -export async function downloadFile(url: string, config?: AxiosRequestConfig): Promise { - const instance = ensureAxiosInstance(); - const response = await instance.get(url, { - ...config, - responseType: 'blob' - }); - - return response.data; -} - -/** - * Get raw text response - */ -export async function getRawText(url: string, config?: AxiosRequestConfig): Promise { - const instance = ensureAxiosInstance(); - const response = await instance.get(url, { - ...config, - responseType: 'text' - }); - - return response.data; -} \ No newline at end of file diff --git a/src/application/services/js-services/http/error-handler.ts b/src/application/services/js-services/http/error-handler.ts deleted file mode 100644 index bb3c10ff..00000000 --- a/src/application/services/js-services/http/error-handler.ts +++ /dev/null @@ -1,465 +0,0 @@ -import { invalidToken } from '@/application/session/token'; -import { notify } from '@/components/_shared/notify'; - -// Error codes from AppFlowy-Cloud-Premium -export enum ErrorCode { - Ok = 0, - Unhandled = -1, - RecordNotFound = -2, - RecordAlreadyExists = -3, - RecordDeleted = -4, - RetryLater = -5, - InvalidEmail = 1001, - InvalidPassword = 1002, - OAuthError = 1003, - MissingPayload = 1004, - DBError = 1005, - OpenError = 1006, - InvalidUrl = 1007, - InvalidRequest = 1008, - InvalidOAuthProvider = 1009, - NotLoggedIn = 1011, - NotEnoughPermissions = 1012, - StorageSpaceNotEnough = 1015, - PayloadTooLarge = 1016, - Internal = 1017, - UuidError = 1018, - IOError = 1019, - SqlxError = 1020, - S3ResponseError = 1021, - SerdeError = 1022, - NetworkError = 1023, - UserUnAuthorized = 1024, - NoRequiredData = 1025, - WorkspaceLimitExceeded = 1026, - WorkspaceMemberLimitExceeded = 1027, - FileStorageLimitExceeded = 1028, - OverrideWithIncorrectData = 1029, - PublishNamespaceNotSet = 1030, - PublishNamespaceAlreadyTaken = 1031, - AIServiceUnavailable = 1032, - AIResponseLimitExceeded = 1033, - StringLengthLimitReached = 1034, - SqlxArgEncodingError = 1035, - InvalidContentType = 1036, - SingleUploadLimitExceeded = 1037, - AppleRevokeTokenError = 1038, - InvalidPublishedOutline = 1039, - InvalidFolderView = 1040, - NotInviteeOfWorkspaceInvitation = 1041, - MissingView = 1042, - AccessRequestAlreadyExists = 1043, - CustomNamespaceDisabled = 1044, - CustomNamespaceDisallowed = 1045, - TooManyImportTask = 1046, - CustomNamespaceTooShort = 1047, - CustomNamespaceTooLong = 1048, - CustomNamespaceReserved = 1049, - PublishNameAlreadyExists = 1050, - PublishNameInvalidCharacter = 1051, - PublishNameTooLong = 1052, - CustomNamespaceInvalidCharacter = 1053, - ServiceTemporaryUnavailable = 1054, - DecodeUpdateError = 1055, - ApplyUpdateError = 1056, - ActionTimeout = 1057, - AIImageResponseLimitExceeded = 1058, - MailerError = 1059, - LicenseError = 1060, - AIMaxRequired = 1061, - InvalidPageData = 1062, - MemberNotFound = 1063, - InvalidBlock = 1064, - RequestTimeout = 1065, - AIResponseError = 1066, - FeatureNotAvailable = 1067, - InvalidInvitationCode = 1068, - InvalidGuest = 1069, - FreePlanGuestLimitExceeded = 1070, - PaidPlanGuestLimitExceeded = 1071, - CommercialError = 1072, - TooManyExportTask = 1073, - InvalidSubscriptionPlan = 1076, - AlreadySubscribed = 1077, - UserIsNotCustomer = 1078, - TooManyRequests = 1079, - JsonWebTokenError = 1081, - LicenseExpired = 1082, - LicenseDeleted = 1083, - LicenseReachLimit = 1084, - ImportError = 1085, - ExportError = 1086, - UploadFileNotFound = 1087, - UploadFileExpired = 1088, - UploadFileTooLarge = 1089, - UpgradeRequired = 1090, - UnzipError = 1091, - CannotOpenWorkspace = 1092, - StreamGroupNotExist = 1093, - S3ServiceUnavailable = 1094, - ImportCollabError = 1095, - RealtimeProtocolError = 1096, - CollabAwarenessError = 1097, - RealtimeDecodingError = 1098, - UnexpectedRealtimeData = 1099, - ExpectInitSync = 1100, - CollabError = 1101, - NotEnoughPermissionToWrite = 1102, - NotEnoughPermissionToRead = 1103, - RealtimeUserNotFound = 1104, - GroupNotFound = 1105, - CreateGroupWorkspaceIdMismatch = 1106, - CreateGroupCannotGetCollabData = 1107, - NoRequiredCollabData = 1108, - TooManyRealtimeMessages = 1109, - LockTimeout = 1110, - CollabStreamError = 1111, - CannotCreateGroup = 1112, - BincodeCollabError = 1113, - CreateSnapshotFailed = 1114, - GetLatestSnapshotFailed = 1115, - CollabSchemaError = 1116, - LeaseError = 1117, - SendWSMessageFailed = 1118, - IndexerStreamGroupNotExist = 1119, - LicenseLimitHit = 1120, -} - -export interface AppResponseError { - code: ErrorCode; - message: string; -} - - -// Error categories for better handling -export const isAuthError = (code: ErrorCode): boolean => { - return [ - ErrorCode.UserUnAuthorized, - ErrorCode.NotLoggedIn, - ErrorCode.OAuthError, - ErrorCode.InvalidPassword, - ErrorCode.AppleRevokeTokenError, - ErrorCode.JsonWebTokenError, - ].includes(code); -}; - -export const isPermissionError = (code: ErrorCode): boolean => { - return [ - ErrorCode.NotEnoughPermissions, - ErrorCode.NotEnoughPermissionToWrite, - ErrorCode.NotEnoughPermissionToRead, - ErrorCode.NotInviteeOfWorkspaceInvitation, - ErrorCode.InvalidGuest, - ].includes(code); -}; - -export const isResourceError = (code: ErrorCode): boolean => { - return [ - ErrorCode.RecordNotFound, - ErrorCode.RecordDeleted, - ErrorCode.MissingView, - ErrorCode.UploadFileNotFound, - ErrorCode.MemberNotFound, - ].includes(code); -}; - -export const isDuplicateError = (code: ErrorCode): boolean => { - return [ - ErrorCode.RecordAlreadyExists, - ErrorCode.PublishNameAlreadyExists, - ErrorCode.PublishNamespaceAlreadyTaken, - ErrorCode.AccessRequestAlreadyExists, - ErrorCode.AlreadySubscribed, - ].includes(code); -}; - -export const isOk = (code: ErrorCode): boolean => { - return code === ErrorCode.Ok; -}; - -export const isLimitError = (code: ErrorCode): boolean => { - return [ - ErrorCode.StorageSpaceNotEnough, - ErrorCode.PayloadTooLarge, - ErrorCode.WorkspaceLimitExceeded, - ErrorCode.WorkspaceMemberLimitExceeded, - ErrorCode.FileStorageLimitExceeded, - ErrorCode.StringLengthLimitReached, - ErrorCode.SingleUploadLimitExceeded, - ErrorCode.FreePlanGuestLimitExceeded, - ErrorCode.PaidPlanGuestLimitExceeded, - ErrorCode.UploadFileTooLarge, - ErrorCode.TooManyImportTask, - ErrorCode.TooManyExportTask, - ErrorCode.AIResponseLimitExceeded, - ErrorCode.AIImageResponseLimitExceeded, - ErrorCode.LicenseReachLimit, - ErrorCode.LicenseLimitHit, - ].includes(code); -}; - - -export const isValidationError = (code: ErrorCode): boolean => { - return [ - ErrorCode.InvalidEmail, - ErrorCode.InvalidPassword, - ErrorCode.InvalidRequest, - ErrorCode.InvalidUrl, - ErrorCode.InvalidContentType, - ErrorCode.InvalidPageData, - ErrorCode.InvalidBlock, - ErrorCode.InvalidFolderView, - ErrorCode.InvalidPublishedOutline, - ErrorCode.PublishNameInvalidCharacter, - ErrorCode.PublishNameTooLong, - ErrorCode.CustomNamespaceInvalidCharacter, - ErrorCode.CustomNamespaceTooShort, - ErrorCode.CustomNamespaceTooLong, - ErrorCode.InvalidInvitationCode, - ErrorCode.InvalidSubscriptionPlan, - ].includes(code); -}; - -// Get user-friendly error message -export const getUserFriendlyMessage = (error: AppResponseError): string => { - // Authentication errors - if (isAuthError(error.code)) { - switch (error.code) { - case ErrorCode.UserUnAuthorized: - return 'Your session has expired. Please sign in again.'; - case ErrorCode.NotLoggedIn: - return 'Please sign in to continue.'; - case ErrorCode.InvalidPassword: - return 'Invalid password. Please try again.'; - default: - return 'Authentication failed. Please sign in again.'; - } - } - - // Permission errors - if (isPermissionError(error.code)) { - switch (error.code) { - case ErrorCode.NotEnoughPermissions: - return 'You do not have permission to perform this action.'; - case ErrorCode.NotEnoughPermissionToWrite: - return 'You have read-only access to this resource.'; - case ErrorCode.NotEnoughPermissionToRead: - return 'You do not have permission to view this resource.'; - default: - return 'Access denied. Insufficient permissions.'; - } - } - - // Resource errors - if (isResourceError(error.code)) { - switch (error.code) { - case ErrorCode.RecordNotFound: - return 'The requested item could not be found.'; - case ErrorCode.RecordDeleted: - return 'This item has been deleted.'; - case ErrorCode.MissingView: - return 'The page or view could not be found.'; - default: - return 'Resource not available.'; - } - } - - // Duplicate errors - if (isDuplicateError(error.code)) { - switch (error.code) { - case ErrorCode.RecordAlreadyExists: - return 'This item already exists.'; - case ErrorCode.PublishNameAlreadyExists: - return 'This publish name is already taken. Please choose another.'; - case ErrorCode.AlreadySubscribed: - return 'You are already subscribed to this plan.'; - default: - return 'This action would create a duplicate.'; - } - } - - // Limit errors - if (isLimitError(error.code)) { - switch (error.code) { - case ErrorCode.StorageSpaceNotEnough: - return 'Storage space limit reached. Please upgrade your plan.'; - case ErrorCode.PayloadTooLarge: - return 'The file or content is too large.'; - case ErrorCode.UploadFileTooLarge: - return 'File size exceeds the maximum allowed. Please upgrade for larger uploads.'; - case ErrorCode.WorkspaceLimitExceeded: - return 'Workspace limit reached. Please upgrade your plan.'; - case ErrorCode.FreePlanGuestLimitExceeded: - return 'Guest limit reached for free plan. Please upgrade to add more guests.'; - default: - return 'Limit exceeded. Please upgrade your plan or reduce usage.'; - } - } - - // Validation errors - if (isValidationError(error.code)) { - switch (error.code) { - case ErrorCode.InvalidEmail: - return 'Please enter a valid email address.'; - case ErrorCode.InvalidRequest: - return 'Invalid request. Please check your input.'; - case ErrorCode.InvalidInvitationCode: - return 'Invalid or expired invitation code.'; - case ErrorCode.PublishNameTooLong: - return 'The publish name is too long. Please use a shorter name.'; - default: - return 'Invalid input. Please check and try again.'; - } - } - - // Service/Network errors - switch (error.code) { - case ErrorCode.ServiceTemporaryUnavailable: - return 'Service temporarily unavailable. Please try again later.'; - case ErrorCode.TooManyRequests: - return 'Too many requests. Please wait a moment and try again.'; - case ErrorCode.RequestTimeout: - return 'Request timed out. Please try again.'; - case ErrorCode.NetworkError: - return 'Network error. Please check your connection.'; - } - - // Special cases - switch (error.code) { - case ErrorCode.UpgradeRequired: - return 'Please upgrade to the latest version to continue.'; - case ErrorCode.LicenseExpired: - return 'Your license has expired. Please renew to continue.'; - case ErrorCode.FeatureNotAvailable: - return 'This feature is not available in your current plan.'; - case ErrorCode.AIServiceUnavailable: - return 'AI service is currently unavailable. Please try again later.'; - case ErrorCode.ImportError: - return 'Import failed. Please check your file and try again.'; - case ErrorCode.ExportError: - return 'Export failed. Please try again.'; - default: - // Fall back to server message if available, otherwise generic message - return error.message || 'An unexpected error occurred. Please try again.'; - } -}; - -// Main error handler -export class ApiErrorHandler { - private static instance: ApiErrorHandler; - - static getInstance(): ApiErrorHandler { - if (!ApiErrorHandler.instance) { - ApiErrorHandler.instance = new ApiErrorHandler(); - } - - return ApiErrorHandler.instance; - } - - /** - * Handle API error response - */ - async handleError( - error: AppResponseError, - options: { - showNotification?: boolean; - throwError?: boolean; - customMessage?: string; - onAuthError?: () => void; - onPermissionError?: () => void; - } = {} - ): Promise { - const { - showNotification = true, - throwError = true, - customMessage, - onAuthError, - onPermissionError, - } = options; - - // Log error for debugging - console.error(`API Error [${error.code}]: ${error.message}`); - - // Handle authentication errors - if (isAuthError(error.code)) { - invalidToken(); - if (onAuthError) { - onAuthError(); - } else { - // Default: redirect to login - window.location.href = '/login'; - } - - return; - } - - // Handle permission errors - if (isPermissionError(error.code)) { - if (onPermissionError) { - onPermissionError(); - } - - if (showNotification) { - notify.error(customMessage || getUserFriendlyMessage(error)); - } - - if (throwError) { - throw error; - } - - return; - } - - // Show user-friendly notification - if (showNotification) { - const message = customMessage || getUserFriendlyMessage(error); - - if (isLimitError(error.code)) { - // For limit errors, show warning with upgrade prompt - notify.warning(message); - } else if (isValidationError(error.code)) { - // For validation errors, show as warning - notify.warning(message); - } else { - // For other errors, show as error - notify.error(message); - } - } - - // Throw error if requested - if (throwError) { - throw error; - } - } - - /** - * Wrap API call with error handling - */ - async wrapApiCall( - apiCall: () => Promise, - options?: Parameters[1] - ): Promise { - try { - return await apiCall(); - } catch (error: unknown) { - // Check if it's an API response error - if ((error as AppResponseError)?.code !== undefined) { - await this.handleError(error as AppResponseError, options); - } - - throw error; - } - } - -} - -// Export singleton instance -export const apiErrorHandler = ApiErrorHandler.getInstance(); - -// Helper function for easy use -export const handleApiError = ( - error: AppResponseError, - options?: Parameters[1] -) => { - return apiErrorHandler.handleError(error, options); -}; \ No newline at end of file diff --git a/src/application/services/js-services/http/http_api.ts b/src/application/services/js-services/http/http_api.ts index b1747fcc..c8e1bb3d 100644 --- a/src/application/services/js-services/http/http_api.ts +++ b/src/application/services/js-services/http/http_api.ts @@ -1,13 +1,10 @@ import { RepeatedChatMessage } from '@appflowyinc/ai-chat'; -import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'; +import axios, { AxiosInstance } from 'axios'; import dayjs from 'dayjs'; import { omit } from 'lodash-es'; import { nanoid } from 'nanoid'; import { GlobalComment, Reaction } from '@/application/comment.type'; -import { apiGet, apiPostVoid } from '@/application/services/js-services/http/api-client'; -import { ApiResponse } from '@/application/services/js-services/http/api-utils'; -import { apiErrorHandler, AppResponseError, ErrorCode } from '@/application/services/js-services/http/error-handler'; import { initGrantService, refreshToken } from '@/application/services/js-services/http/gotrue'; import { blobToBytes } from '@/application/services/js-services/http/utils'; import { AFCloudConfig } from '@/application/services/services.type'; @@ -60,6 +57,7 @@ import { Workspace, WorkspaceMember, } from '@/application/types'; +import { notify } from '@/components/_shared/notify'; export * from './gotrue'; @@ -120,117 +118,28 @@ export function initAPIService(config: AFCloudConfig) { } ); - axiosInstance.interceptors.response.use( - async (response) => { - const status = response.status; + axiosInstance.interceptors.response.use(async (response) => { + const status = response.status; - if (status === 401) { - const token = getTokenParsed(); + if (status === 401) { + const token = getTokenParsed(); - if (!token) { - invalidToken(); - return response; - } - - const refresh_token = token.refresh_token; - - try { - await refreshToken(refresh_token); - } catch (e) { - invalidToken(); - } - } - - // Check if this looks like an ApiResponse - // ApiResponse should have a 'code' field that's a number - const responseData = response.data; - const isApiResponse = responseData && - typeof responseData === 'object' && - 'code' in responseData && - typeof responseData.code === 'number'; - - if (!isApiResponse) { - // Not an API response, check HTTP status to determine success - if (response.status !== 200) { - // Non-200 status for non-API response - const error: AppResponseError = { - code: ErrorCode.Unhandled, - message: `HTTP ${response.status}: ${response.statusText}` - }; - - const showNotification = (response.config as AxiosRequestConfig & { showNotification?: boolean })?.showNotification ?? false; - - await apiErrorHandler.handleError(error, { showNotification }); - return Promise.reject(error); - } - - // Status 200 - success, return data as-is - response.data = responseData; + if (!token) { + invalidToken(); return response; } - // Now we know it's an ApiResponse - const apiResponse = responseData as ApiResponse; + const refresh_token = token.refresh_token; - // Handle error codes - if (apiResponse.code !== ErrorCode.Ok) { - const error: AppResponseError = { - code: Object.values(ErrorCode).includes(apiResponse.code) ? apiResponse.code : ErrorCode.Unhandled, - message: apiResponse.message || 'An error occurred' - }; - - // Check if this request wants to show notifications - const showNotification = (response.config as AxiosRequestConfig & { showNotification?: boolean })?.showNotification ?? false; - - await apiErrorHandler.handleError(error, { showNotification }); - - return Promise.reject(error); - } - - // Success - return just the data portion - // For void responses, data will be undefined which is fine - response.data = apiResponse.data; - - return response; - }, - async (error) => { - // Handle network errors or non-2xx status codes - if (error.response?.status === 401) { + try { + await refreshToken(refresh_token); + } catch (e) { invalidToken(); } - - const showNotification = (error.config as AxiosRequestConfig & { showNotification?: boolean })?.showNotification ?? false; - - // If we got a response, try to parse it as ApiResponse - if (error.response?.data) { - const responseData = error.response.data; - const isApiResponse = responseData && - typeof responseData === 'object' && - 'code' in responseData && - typeof responseData.code === 'number'; - - if (isApiResponse) { - const apiResponse = responseData; - const appError: AppResponseError = { - code: Object.values(ErrorCode).includes(apiResponse.code) ? apiResponse.code : ErrorCode.Unhandled, - message: apiResponse.message || 'An error occurred' - }; - - await apiErrorHandler.handleError(appError, { showNotification }); - return Promise.reject(appError); - } - } - - // Fallback for network errors or unparseable responses - const appError: AppResponseError = { - code: ErrorCode.NetworkError, - message: error.message || 'Network error occurred' - }; - - await apiErrorHandler.handleError(appError, { showNotification }); - return Promise.reject(appError); } - ); + + return response; + }); } export async function signInWithUrl(url: string) { @@ -272,45 +181,76 @@ export async function signInWithUrl(url: string) { export async function verifyToken(accessToken: string) { const url = `/api/user/verify/${accessToken}`; - - return await apiGet<{ is_new: boolean }>(url); + const response = await axiosInstance?.get<{ + code: number; + data?: { + is_new: boolean; + }; + message: string; + }>(url); + + const data = response?.data; + + if (data?.code === 0 && data.data) { + return data.data; + } + + return Promise.reject(data); } export async function getCurrentUser(): Promise { const url = '/api/user/profile'; - - const data = await apiGet<{ - uid: number; - uuid: string; - email: string; - name: string; - metadata: Record; - latest_workspace_id: string; - updated_at: number; + const response = await axiosInstance?.get<{ + code: number; + data?: { + uid: number; + uuid: string; + email: string; + name: string; + metadata: Record; + encryption_sign: null; + latest_workspace_id: string; + updated_at: number; + }; + message: string; }>(url); - const { uid, uuid, email, name, metadata } = data; + const data = response?.data; - return { - uid: String(uid), - uuid, - email, - name, - avatar: (metadata?.icon_url as string) || null, - latestWorkspaceId: data.latest_workspace_id, - metadata: metadata || {}, - }; + if (data?.code === 0 && data.data) { + const { uid, uuid, email, name, metadata } = data.data; + + return { + uid: String(uid), + uuid, + email, + name, + avatar: (metadata?.icon_url as string) || null, + latestWorkspaceId: data.data.latest_workspace_id, + metadata: metadata || {}, + }; + } + + return Promise.reject(data); } export async function updateUserProfile(metadata: Record): Promise { - if (!metadata || Object.keys(metadata).length === 0) { + const url = 'api/user/update'; + const response = await axiosInstance?.post<{ + code: number; + message: string; + }>(url, { + metadata + }); + + const data = response?.data; + + if (data?.code === 0) { return; } - const url = 'api/user/update'; - - await apiPostVoid(url, { metadata }); + return Promise.reject(data); } interface AFWorkspace { @@ -1755,10 +1695,28 @@ export async function uploadFile( ) { const url = `/api/file_storage/${workspaceId}/v1/blob/${viewId}`; + // Check file size, if over 7MB, check subscription plan + if (file.size > 7 * 1024 * 1024) { + const plan = await getActiveSubscription(workspaceId); + + if (plan?.length === 0 || plan?.[0] === SubscriptionPlan.Free) { + notify.error('Your file is over 7 MB limit of the Free plan. Upgrade for unlimited uploads.'); + + return Promise.reject({ + code: 413, + message: 'File size is too large. Please upgrade your plan for unlimited uploads.', + }); + } + } + try { - const axiosResponse = await axiosInstance?.put>(url, file, { + const response = await axiosInstance?.put<{ + code: number; + message: string; + data: { + file_id: string; + }; + }>(url, file, { onUploadProgress: (progressEvent) => { const { progress = 0 } = progressEvent; @@ -1769,28 +1727,26 @@ export async function uploadFile( }, }); - const response = axiosResponse?.data; - - if (response?.code === ErrorCode.Ok && response?.data) { + if (response?.data.code === 0) { const baseURL = axiosInstance?.defaults.baseURL; - const fileUrl = `${baseURL}/api/file_storage/${workspaceId}/v1/blob/${viewId}/${response.data.file_id}`; + const url = `${baseURL}/api/file_storage/${workspaceId}/v1/blob/${viewId}/${response?.data.data.file_id}`; - return fileUrl; + return url; } - return Promise.reject(response); + return Promise.reject(response?.data); // eslint-disable-next-line } catch (e: any) { - if (e.response?.status === 413) { + if (e.response.status === 413) { return Promise.reject({ - code: ErrorCode.PayloadTooLarge, + code: 413, message: 'File size is too large. Please upgrade your plan for unlimited uploads.', }); } } return Promise.reject({ - code: ErrorCode.Unhandled, + code: -1, message: 'Upload file failed.', }); } diff --git a/src/application/services/js-services/http/useApiError.ts b/src/application/services/js-services/http/useApiError.ts deleted file mode 100644 index 2c776670..00000000 --- a/src/application/services/js-services/http/useApiError.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { useCallback } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { AppResponseError, ErrorCode, apiErrorHandler } from './error-handler'; - -/** - * React hook for handling API errors in components - */ -export function useApiError() { - const navigate = useNavigate(); - - const handleError = useCallback( - async ( - error: AppResponseError, - options?: { - showNotification?: boolean; - customMessage?: string; - } - ) => { - return apiErrorHandler.handleError(error, { - ...options, - onAuthError: () => { - // Redirect to login on auth errors - navigate('/login'); - }, - onPermissionError: () => { - // Could show upgrade modal or permission denied page - console.warn('Permission denied:', error.message); - }, - }); - }, - [navigate] - ); - - const wrapApiCall = useCallback( - async ( - apiCall: () => Promise, - options?: Parameters[1] - ): Promise => { - try { - return await apiCall(); - } catch (error: unknown) { - if ((error as AppResponseError)?.code !== undefined) { - await handleError(error as AppResponseError, options); - } - - throw error; - } - }, - [handleError] - ); - - return { - handleError, - wrapApiCall, - }; -} - -/** - * Hook for handling specific error types - */ -export function useApiErrorHandler() { - const navigate = useNavigate(); - - const handleAuthError = useCallback(() => { - navigate('/login'); - }, [navigate]); - - const handlePermissionError = useCallback(() => { - // Show permission denied message or upgrade prompt - console.warn('Permission denied'); - }, []); - - const handleResourceNotFound = useCallback(() => { - navigate('/404'); - }, [navigate]); - - const handleLimitExceeded = useCallback(() => { - // Show upgrade modal - console.info('Limit exceeded - show upgrade prompt'); - }, []); - - const handleApiError = useCallback( - (error: AppResponseError) => { - switch (error.code) { - case ErrorCode.UserUnAuthorized: - case ErrorCode.NotLoggedIn: - handleAuthError(); - break; - case ErrorCode.NotEnoughPermissions: - case ErrorCode.NotEnoughPermissionToWrite: - case ErrorCode.NotEnoughPermissionToRead: - handlePermissionError(); - break; - case ErrorCode.RecordNotFound: - case ErrorCode.MissingView: - handleResourceNotFound(); - break; - case ErrorCode.StorageSpaceNotEnough: - case ErrorCode.WorkspaceLimitExceeded: - case ErrorCode.PayloadTooLarge: - case ErrorCode.UploadFileTooLarge: - handleLimitExceeded(); - break; - default: - // Let the global error handler deal with it - void apiErrorHandler.handleError(error); - } - }, - [handleAuthError, handlePermissionError, handleResourceNotFound, handleLimitExceeded] - ); - - return { - handleApiError, - handleAuthError, - handlePermissionError, - handleResourceNotFound, - handleLimitExceeded, - }; -} \ No newline at end of file