feat(01-02): create custom authentication exceptions
- 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
This commit is contained in:
301
lib/core/errors/auth_exceptions.dart
Normal file
301
lib/core/errors/auth_exceptions.dart
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user