diff --git a/lib/core/errors/auth_exceptions.dart b/lib/core/errors/auth_exceptions.dart new file mode 100644 index 0000000..8c8b7da --- /dev/null +++ b/lib/core/errors/auth_exceptions.dart @@ -0,0 +1,301 @@ +/// 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; + } +} \ No newline at end of file