mirror of
https://github.com/AppFlowy-IO/AppFlowy-Web.git
synced 2026-03-13 10:00:26 +08:00
chore: paser gotrue error
This commit is contained in:
242
src/application/services/js-services/http/gotrue-error.ts
Normal file
242
src/application/services/js-services/http/gotrue-error.ts
Normal file
@@ -0,0 +1,242 @@
|
||||
/**
|
||||
* GoTrue Error Parser
|
||||
* Handles various error formats from GoTrue authentication service
|
||||
*/
|
||||
|
||||
export interface GoTrueError {
|
||||
code: number;
|
||||
message: string;
|
||||
originalError?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Common GoTrue error codes
|
||||
*/
|
||||
export enum GoTrueErrorCode {
|
||||
BAD_REQUEST = 400,
|
||||
UNAUTHORIZED = 401,
|
||||
FORBIDDEN = 403,
|
||||
NOT_FOUND = 404,
|
||||
UNPROCESSABLE_ENTITY = 422,
|
||||
TOO_MANY_REQUESTS = 429,
|
||||
INTERNAL_SERVER_ERROR = 500,
|
||||
UNKNOWN = -1,
|
||||
}
|
||||
|
||||
/**
|
||||
* Known GoTrue error types
|
||||
*/
|
||||
export enum GoTrueErrorType {
|
||||
ACCESS_DENIED = 'access_denied',
|
||||
UNAUTHORIZED_CLIENT = 'unauthorized_client',
|
||||
INVALID_REQUEST = 'invalid_request',
|
||||
INVALID_GRANT = 'invalid_grant',
|
||||
UNSUPPORTED_GRANT_TYPE = 'unsupported_grant_type',
|
||||
SIGNUP_DISABLED = 'signup_disabled',
|
||||
USER_BANNED = 'user_banned',
|
||||
EMAIL_NOT_CONFIRMED = 'email_not_confirmed',
|
||||
BAD_JSON = 'bad_json',
|
||||
BAD_JWT = 'bad_jwt',
|
||||
NOT_ADMIN = 'not_admin',
|
||||
NO_AUTHORIZATION = 'no_authorization',
|
||||
USER_NOT_FOUND = 'user_not_found',
|
||||
SESSION_NOT_FOUND = 'session_not_found',
|
||||
FLOW_STATE_NOT_FOUND = 'flow_state_not_found',
|
||||
FLOW_STATE_EXPIRED = 'flow_state_expired',
|
||||
PKCE_VERIFIER_NOT_FOUND = 'pkce_verifier_not_found',
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse GoTrue error from URL parameters
|
||||
* Handles multiple formats that GoTrue might use in callbacks
|
||||
*/
|
||||
export function parseGoTrueErrorFromUrl(url: string): GoTrueError | null {
|
||||
try {
|
||||
const urlObj = new URL(url);
|
||||
const searchParams = urlObj.searchParams;
|
||||
const hash = urlObj.hash;
|
||||
const hashParams = hash ? new URLSearchParams(hash.slice(1)) : new URLSearchParams();
|
||||
|
||||
// Log for debugging (remove in production)
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('[GoTrue Error Parser] URL:', url);
|
||||
console.log('[GoTrue Error Parser] Search params:', Array.from(searchParams.entries()));
|
||||
console.log('[GoTrue Error Parser] Hash params:', Array.from(hashParams.entries()));
|
||||
}
|
||||
|
||||
// Check all possible error parameter locations and names
|
||||
const error =
|
||||
searchParams.get('error') ||
|
||||
hashParams.get('error') ||
|
||||
searchParams.get('error_type') ||
|
||||
hashParams.get('error_type');
|
||||
|
||||
const errorDescription =
|
||||
searchParams.get('error_description') ||
|
||||
hashParams.get('error_description') ||
|
||||
searchParams.get('message') ||
|
||||
hashParams.get('message') ||
|
||||
searchParams.get('msg') ||
|
||||
hashParams.get('msg') ||
|
||||
searchParams.get('error_msg') ||
|
||||
hashParams.get('error_msg');
|
||||
|
||||
const errorCode =
|
||||
searchParams.get('error_code') ||
|
||||
hashParams.get('error_code') ||
|
||||
searchParams.get('code') ||
|
||||
hashParams.get('code') ||
|
||||
searchParams.get('status') ||
|
||||
hashParams.get('status');
|
||||
|
||||
// If no error indicators found, return null
|
||||
if (!error && !errorDescription && !errorCode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Parse the error details
|
||||
return parseGoTrueError({
|
||||
error,
|
||||
errorDescription,
|
||||
errorCode,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('[GoTrue Error Parser] Failed to parse URL:', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse GoTrue error from response or error object
|
||||
*/
|
||||
export function parseGoTrueError(errorData: {
|
||||
error?: string | null;
|
||||
errorDescription?: string | null;
|
||||
errorCode?: string | null;
|
||||
message?: string;
|
||||
msg?: string;
|
||||
code?: number | string;
|
||||
status?: number;
|
||||
}): GoTrueError {
|
||||
// Get the most descriptive error message available
|
||||
const errorMessage =
|
||||
errorData.errorDescription ||
|
||||
errorData.message ||
|
||||
errorData.msg ||
|
||||
errorData.error ||
|
||||
'Authentication failed';
|
||||
|
||||
// Parse error code from various sources
|
||||
let code = GoTrueErrorCode.UNKNOWN;
|
||||
|
||||
// Try to get code from explicit field
|
||||
if (errorData.code) {
|
||||
code = typeof errorData.code === 'number' ? errorData.code : parseInt(errorData.code);
|
||||
} else if (errorData.errorCode) {
|
||||
code = parseInt(errorData.errorCode);
|
||||
} else if (errorData.status) {
|
||||
code = errorData.status;
|
||||
}
|
||||
|
||||
// Try to extract code from message format like "422: Signups not allowed"
|
||||
if (code === GoTrueErrorCode.UNKNOWN && errorMessage) {
|
||||
const codeMatch = errorMessage.match(/^(\d{3}):/);
|
||||
if (codeMatch) {
|
||||
code = parseInt(codeMatch[1]);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up the message - remove error codes and clean up formatting
|
||||
let cleanMessage = errorMessage
|
||||
.replace(/^\d{3}:\s*/, '') // Remove "422: " prefix
|
||||
.replace(/\+/g, ' ') // Replace + with spaces (URL encoding)
|
||||
.replace(/%20/g, ' ') // Replace %20 with spaces
|
||||
.trim();
|
||||
|
||||
// Return the actual error message from GoTrue (don't replace it)
|
||||
return {
|
||||
code,
|
||||
message: cleanMessage,
|
||||
originalError: errorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhance error messages to be more user-friendly (optional)
|
||||
* You can use this function if you want to provide custom user-friendly messages
|
||||
* while keeping the original error available for debugging
|
||||
*/
|
||||
export function enhanceErrorMessage(message: string, errorType?: string | null, code?: number): string {
|
||||
const lowerMessage = message.toLowerCase();
|
||||
|
||||
// Signup disabled errors
|
||||
if (lowerMessage.includes('signups not allowed') ||
|
||||
lowerMessage.includes('signup disabled') ||
|
||||
errorType === GoTrueErrorType.SIGNUP_DISABLED ||
|
||||
code === GoTrueErrorCode.UNPROCESSABLE_ENTITY) {
|
||||
return 'Sign-ups are currently disabled. Please contact your administrator to request access.';
|
||||
}
|
||||
|
||||
// Invalid credentials
|
||||
if (lowerMessage.includes('invalid login credentials') ||
|
||||
lowerMessage.includes('incorrect password') ||
|
||||
errorType === GoTrueErrorType.INVALID_GRANT) {
|
||||
return 'Invalid email or password. Please try again.';
|
||||
}
|
||||
|
||||
// Email not confirmed
|
||||
if (lowerMessage.includes('email not confirmed') ||
|
||||
errorType === GoTrueErrorType.EMAIL_NOT_CONFIRMED) {
|
||||
return 'Please confirm your email address before signing in.';
|
||||
}
|
||||
|
||||
// User banned
|
||||
if (lowerMessage.includes('user banned') ||
|
||||
errorType === GoTrueErrorType.USER_BANNED) {
|
||||
return 'Your account has been suspended. Please contact support.';
|
||||
}
|
||||
|
||||
// Rate limiting
|
||||
if (lowerMessage.includes('too many requests') ||
|
||||
code === GoTrueErrorCode.TOO_MANY_REQUESTS) {
|
||||
return 'Too many attempts. Please try again later.';
|
||||
}
|
||||
|
||||
// Access denied
|
||||
if (errorType === GoTrueErrorType.ACCESS_DENIED) {
|
||||
return 'Access denied. You do not have permission to perform this action.';
|
||||
}
|
||||
|
||||
// Session expired
|
||||
if (lowerMessage.includes('session expired') ||
|
||||
lowerMessage.includes('token expired')) {
|
||||
return 'Your session has expired. Please sign in again.';
|
||||
}
|
||||
|
||||
// Network or server errors
|
||||
if (code === GoTrueErrorCode.INTERNAL_SERVER_ERROR) {
|
||||
return 'A server error occurred. Please try again later.';
|
||||
}
|
||||
|
||||
// Default - return the cleaned message as-is
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a URL contains GoTrue error parameters
|
||||
*/
|
||||
export function hasGoTrueError(url: string): boolean {
|
||||
const error = parseGoTrueErrorFromUrl(url);
|
||||
return error !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format GoTrue error for display
|
||||
*/
|
||||
export function formatGoTrueError(error: GoTrueError): string {
|
||||
if (process.env.NODE_ENV === 'development' && error.originalError) {
|
||||
// In development, show more details
|
||||
return `${error.message}\n\n[Debug] Original: ${error.originalError} (Code: ${error.code})`;
|
||||
}
|
||||
return error.message;
|
||||
}
|
||||
@@ -4,6 +4,10 @@ import { emit, EventType } from '@/application/session';
|
||||
import { afterAuth } from '@/application/session/sign_in';
|
||||
import { getTokenParsed, refreshToken as refreshSessionToken } from '@/application/session/token';
|
||||
|
||||
import { parseGoTrueError } from './gotrue-error';
|
||||
|
||||
export * from './gotrue-error';
|
||||
|
||||
let axiosInstance: AxiosInstance | null = null;
|
||||
|
||||
export function initGrantService(baseURL: string) {
|
||||
@@ -71,9 +75,18 @@ export async function signInWithPassword(params: { email: string; password: stri
|
||||
// eslint-disable-next-line
|
||||
} catch (e: any) {
|
||||
emit(EventType.SESSION_INVALID);
|
||||
|
||||
// Parse error from response
|
||||
const error = parseGoTrueError({
|
||||
error: e.response?.data?.error,
|
||||
errorDescription: e.response?.data?.error_description || e.response?.data?.msg,
|
||||
errorCode: e.response?.status,
|
||||
message: e.response?.data?.message || 'Incorrect password. Please try again.',
|
||||
});
|
||||
|
||||
return Promise.reject({
|
||||
code: -1,
|
||||
message: 'Incorrect password. Please try again.',
|
||||
code: error.code,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { nanoid } from 'nanoid';
|
||||
import { GlobalComment, Reaction } from '@/application/comment.type';
|
||||
import { ERROR_CODE } from '@/application/constants';
|
||||
import { initGrantService, refreshToken } from '@/application/services/js-services/http/gotrue';
|
||||
import { parseGoTrueErrorFromUrl } from '@/application/services/js-services/http/gotrue-error';
|
||||
import { blobToBytes } from '@/application/services/js-services/http/utils';
|
||||
import { AFCloudConfig } from '@/application/services/services.type';
|
||||
import { getTokenParsed, invalidToken } from '@/application/session/token';
|
||||
@@ -146,7 +147,20 @@ export function initAPIService(config: AFCloudConfig) {
|
||||
}
|
||||
|
||||
export async function signInWithUrl(url: string) {
|
||||
const hash = new URL(url).hash;
|
||||
// First check for GoTrue errors in the URL
|
||||
const gotrueError = parseGoTrueErrorFromUrl(url);
|
||||
|
||||
if (gotrueError) {
|
||||
// GoTrue returned an error, reject with parsed error
|
||||
return Promise.reject({
|
||||
code: gotrueError.code,
|
||||
message: gotrueError.message,
|
||||
});
|
||||
}
|
||||
|
||||
// No errors found, proceed with normal token extraction
|
||||
const urlObj = new URL(url);
|
||||
const hash = urlObj.hash;
|
||||
|
||||
if (!hash) {
|
||||
return Promise.reject('No hash found');
|
||||
|
||||
Reference in New Issue
Block a user