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:
Dani B
2026-01-28 09:12:01 -05:00
parent c45bb22971
commit b46cabe9fa

View 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;
}
}