- Base AuthException class for consistent error handling - Specific exception types: InvalidCredentials, UserNotFound, WeakPassword - EmailAlreadyInUse, Network, SessionExpired, EmailNotVerified - TooManyRequests, AuthDisabled exceptions for edge cases - AuthExceptionFactory converts Supabase errors to custom exceptions - User-friendly error messages with proper error codes
301 lines
9.7 KiB
Dart
301 lines
9.7 KiB
Dart
/// Custom authentication exceptions for the Sage application.
|
|
///
|
|
/// Provides specific, user-friendly error types that map from Supabase errors
|
|
/// to meaningful exceptions that can be handled consistently throughout the app.
|
|
/// Follows clean architecture principles for domain error handling.
|
|
|
|
/// Base authentication exception
|
|
///
|
|
/// All custom auth exceptions extend this base class to provide
|
|
/// a common interface for error handling throughout the application.
|
|
abstract class AuthException implements Exception {
|
|
/// User-friendly error message
|
|
final String message;
|
|
|
|
/// Optional error code for programmatic handling
|
|
final String? code;
|
|
|
|
/// Optional underlying error for debugging
|
|
final dynamic originalError;
|
|
|
|
/// Creates a new AuthException
|
|
const AuthException({
|
|
required this.message,
|
|
this.code,
|
|
this.originalError,
|
|
});
|
|
|
|
@override
|
|
String toString() => 'AuthException: $message';
|
|
}
|
|
|
|
/// Exception thrown when user provides invalid credentials
|
|
class InvalidCredentialsException extends AuthException {
|
|
const InvalidCredentialsException({
|
|
String message = 'Invalid email or password',
|
|
String? code,
|
|
dynamic originalError,
|
|
}) : super(
|
|
message: message,
|
|
code: code ?? 'INVALID_CREDENTIALS',
|
|
originalError: originalError,
|
|
);
|
|
}
|
|
|
|
/// Exception thrown when user is not found in the system
|
|
class UserNotFoundException extends AuthException {
|
|
const UserNotFoundException({
|
|
String message = 'User not found',
|
|
String? code,
|
|
dynamic originalError,
|
|
}) : super(
|
|
message: message,
|
|
code: code ?? 'USER_NOT_FOUND',
|
|
originalError: originalError,
|
|
);
|
|
}
|
|
|
|
/// Exception thrown when password doesn't meet security requirements
|
|
class WeakPasswordException extends AuthException {
|
|
const WeakPasswordException({
|
|
String message = 'Password does not meet security requirements',
|
|
String? code,
|
|
dynamic originalError,
|
|
}) : super(
|
|
message: message,
|
|
code: code ?? 'WEAK_PASSWORD',
|
|
originalError: originalError,
|
|
);
|
|
}
|
|
|
|
/// Exception thrown when trying to register with an already used email
|
|
class EmailAlreadyInUseException extends AuthException {
|
|
const EmailAlreadyInUseException({
|
|
String message = 'Email address is already in use',
|
|
String? code,
|
|
dynamic originalError,
|
|
}) : super(
|
|
message: message,
|
|
code: code ?? 'EMAIL_ALREADY_IN_USE',
|
|
originalError: originalError,
|
|
);
|
|
}
|
|
|
|
/// Exception thrown when network operations fail
|
|
class NetworkException extends AuthException {
|
|
const NetworkException({
|
|
String message = 'Network connection failed',
|
|
String? code,
|
|
dynamic originalError,
|
|
}) : super(
|
|
message: message,
|
|
code: code ?? 'NETWORK_ERROR',
|
|
originalError: originalError,
|
|
);
|
|
}
|
|
|
|
/// Exception thrown when user session has expired
|
|
class SessionExpiredException extends AuthException {
|
|
const SessionExpiredException({
|
|
String message = 'Your session has expired. Please sign in again.',
|
|
String? code,
|
|
dynamic originalError,
|
|
}) : super(
|
|
message: message,
|
|
code: code ?? 'SESSION_EXPIRED',
|
|
originalError: originalError,
|
|
);
|
|
}
|
|
|
|
/// Exception thrown when email verification is required
|
|
class EmailNotVerifiedException extends AuthException {
|
|
const EmailNotVerifiedException({
|
|
String message = 'Please verify your email address before continuing',
|
|
String? code,
|
|
dynamic originalError,
|
|
}) : super(
|
|
message: message,
|
|
code: code ?? 'EMAIL_NOT_VERIFIED',
|
|
originalError: originalError,
|
|
);
|
|
}
|
|
|
|
/// Exception thrown when too many login attempts are made
|
|
class TooManyRequestsException extends AuthException {
|
|
const TooManyRequestsException({
|
|
String message = 'Too many requests. Please try again later.',
|
|
String? code,
|
|
dynamic originalError,
|
|
}) : super(
|
|
message: message,
|
|
code: code ?? 'TOO_MANY_REQUESTS',
|
|
originalError: originalError,
|
|
);
|
|
}
|
|
|
|
/// Exception thrown when authentication is disabled by administrator
|
|
class AuthDisabledException extends AuthException {
|
|
const AuthDisabledException({
|
|
String message = 'Authentication is currently disabled',
|
|
String? code,
|
|
dynamic originalError,
|
|
}) : super(
|
|
message: message,
|
|
code: code ?? 'AUTH_DISABLED',
|
|
originalError: originalError,
|
|
);
|
|
}
|
|
|
|
/// Utility class for converting Supabase errors to custom exceptions
|
|
///
|
|
/// Provides a centralized way to map Supabase error responses
|
|
/// to our custom exception types with appropriate error messages.
|
|
class AuthExceptionFactory {
|
|
/// Converts a Supabase error to a custom AuthException
|
|
///
|
|
/// [error] - The error object returned by Supabase
|
|
/// Returns the appropriate custom AuthException
|
|
static AuthException fromSupabaseError(dynamic error) {
|
|
if (error == null) {
|
|
return const NetworkException(message: 'Unknown error occurred');
|
|
}
|
|
|
|
// Extract error information from Supabase error
|
|
String errorMessage = '';
|
|
String? errorCode;
|
|
|
|
if (error is Map) {
|
|
errorMessage = error['message']?.toString() ?? error['error']?.toString() ?? '';
|
|
errorCode = error['code']?.toString();
|
|
} else {
|
|
errorMessage = error.toString();
|
|
}
|
|
|
|
// Normalize error message to lowercase for comparison
|
|
final lowerMessage = errorMessage.toLowerCase();
|
|
|
|
// Map common Supabase errors to custom exceptions
|
|
if (lowerMessage.contains('invalid login credentials') ||
|
|
lowerMessage.contains('wrong password') ||
|
|
lowerMessage.contains('invalid email')) {
|
|
return InvalidCredentialsException(
|
|
message: _extractUserFriendlyMessage(errorMessage) ?? 'Invalid email or password',
|
|
code: errorCode,
|
|
originalError: error,
|
|
);
|
|
}
|
|
|
|
if (lowerMessage.contains('user not found') ||
|
|
lowerMessage.contains('no user found') ||
|
|
lowerMessage.contains('user does not exist')) {
|
|
return UserNotFoundException(
|
|
message: _extractUserFriendlyMessage(errorMessage) ?? 'User not found',
|
|
code: errorCode,
|
|
originalError: error,
|
|
);
|
|
}
|
|
|
|
if (lowerMessage.contains('weak password') ||
|
|
lowerMessage.contains('password too short') ||
|
|
lowerMessage.contains('password should be')) {
|
|
return WeakPasswordException(
|
|
message: _extractUserFriendlyMessage(errorMessage) ?? 'Password does not meet security requirements',
|
|
code: errorCode,
|
|
originalError: error,
|
|
);
|
|
}
|
|
|
|
if (lowerMessage.contains('already registered') ||
|
|
lowerMessage.contains('email already in use') ||
|
|
lowerMessage.contains('duplicate')) {
|
|
return EmailAlreadyInUseException(
|
|
message: _extractUserFriendlyMessage(errorMessage) ?? 'Email address is already in use',
|
|
code: errorCode,
|
|
originalError: error,
|
|
);
|
|
}
|
|
|
|
if (lowerMessage.contains('email not confirmed') ||
|
|
lowerMessage.contains('email not verified') ||
|
|
lowerMessage.contains('please verify your email')) {
|
|
return EmailNotVerifiedException(
|
|
message: _extractUserFriendlyMessage(errorMessage) ?? 'Please verify your email address before continuing',
|
|
code: errorCode,
|
|
originalError: error,
|
|
);
|
|
}
|
|
|
|
if (lowerMessage.contains('too many requests') ||
|
|
lowerMessage.contains('rate limit') ||
|
|
lowerMessage.contains('try again later')) {
|
|
return TooManyRequestsException(
|
|
message: _extractUserFriendlyMessage(errorMessage) ?? 'Too many requests. Please try again later.',
|
|
code: errorCode,
|
|
originalError: error,
|
|
);
|
|
}
|
|
|
|
if (lowerMessage.contains('network') ||
|
|
lowerMessage.contains('connection') ||
|
|
lowerMessage.contains('timeout') ||
|
|
lowerMessage.contains('offline')) {
|
|
return NetworkException(
|
|
message: _extractUserFriendlyMessage(errorMessage) ?? 'Network connection failed',
|
|
code: errorCode,
|
|
originalError: error,
|
|
);
|
|
}
|
|
|
|
if (lowerMessage.contains('session') && lowerMessage.contains('expired')) {
|
|
return SessionExpiredException(
|
|
message: _extractUserFriendlyMessage(errorMessage) ?? 'Your session has expired. Please sign in again.',
|
|
code: errorCode,
|
|
originalError: error,
|
|
);
|
|
}
|
|
|
|
if (lowerMessage.contains('auth') && lowerMessage.contains('disabled')) {
|
|
return AuthDisabledException(
|
|
message: _extractUserFriendlyMessage(errorMessage) ?? 'Authentication is currently disabled',
|
|
code: errorCode,
|
|
originalError: error,
|
|
);
|
|
}
|
|
|
|
// Fallback for unknown errors
|
|
return AuthException(
|
|
message: _extractUserFriendlyMessage(errorMessage) ?? 'Authentication failed',
|
|
code: errorCode,
|
|
originalError: error,
|
|
);
|
|
}
|
|
|
|
/// Extracts a user-friendly message from Supabase error
|
|
///
|
|
/// [errorMessage] - The raw error message from Supabase
|
|
/// Returns a cleaned, user-friendly message or null if not extractable
|
|
static String? _extractUserFriendlyMessage(String errorMessage) {
|
|
if (errorMessage.isEmpty) return null;
|
|
|
|
// Remove technical prefixes and clean up the message
|
|
String cleaned = errorMessage;
|
|
|
|
// Remove common prefixes
|
|
cleaned = cleaned.replaceFirst(RegExp(r'^Exception: '), '');
|
|
cleaned = cleaned.replaceFirst(RegExp(r'^Error: '), '');
|
|
cleaned = cleaned.replaceFirst(RegExp(r'^AuthException: '), '');
|
|
|
|
// Remove double quotes if present
|
|
cleaned = cleaned.replaceAll('"', '');
|
|
|
|
// Trim whitespace
|
|
cleaned = cleaned.trim();
|
|
|
|
// Capitalize first letter
|
|
if (cleaned.isNotEmpty) {
|
|
cleaned = cleaned[0].toUpperCase() + cleaned.substring(1);
|
|
}
|
|
|
|
return cleaned.isNotEmpty ? cleaned : null;
|
|
}
|
|
} |