- Replace flutter_appauth with custom WebView implementation to resolve deep link issues - Add KeycloakWebViewAuthService with integrated WebView for seamless authentication - Configure Android manifest for HTTP cleartext traffic support - Add network security config for development environment (192.168.1.11) - Update Keycloak client to use HTTP callback endpoint (http://192.168.1.11:8080/auth/callback) - Remove obsolete keycloak_auth_service.dart and temporary scripts - Clean up dependencies and regenerate injection configuration - Tested successfully on multiple Android devices (Xiaomi 2201116TG, SM A725F) BREAKING CHANGE: Authentication flow now uses WebView instead of external browser - Users will see Keycloak login page within the app instead of browser redirect - Resolves ERR_CLEARTEXT_NOT_PERMITTED and deep link state management issues - Maintains full OIDC compliance with PKCE flow and secure token storage Technical improvements: - WebView with custom navigation delegate for callback handling - Automatic token extraction and user info parsing from JWT - Proper error handling and user feedback - Consistent authentication state management across app lifecycle
272 lines
6.2 KiB
Dart
272 lines
6.2 KiB
Dart
/// Classes d'échec pour la gestion d'erreurs structurée
|
|
abstract class Failure {
|
|
final String message;
|
|
final String? code;
|
|
final Map<String, dynamic>? details;
|
|
|
|
const Failure({
|
|
required this.message,
|
|
this.code,
|
|
this.details,
|
|
});
|
|
|
|
@override
|
|
String toString() => 'Failure(message: $message, code: $code)';
|
|
|
|
@override
|
|
bool operator ==(Object other) {
|
|
if (identical(this, other)) return true;
|
|
return other is Failure &&
|
|
other.message == message &&
|
|
other.code == code;
|
|
}
|
|
|
|
@override
|
|
int get hashCode => message.hashCode ^ code.hashCode;
|
|
}
|
|
|
|
/// Échec réseau (problèmes de connectivité, timeout, etc.)
|
|
class NetworkFailure extends Failure {
|
|
const NetworkFailure({
|
|
required super.message,
|
|
super.code,
|
|
super.details,
|
|
});
|
|
|
|
factory NetworkFailure.noConnection() {
|
|
return const NetworkFailure(
|
|
message: 'Aucune connexion internet disponible',
|
|
code: 'NO_CONNECTION',
|
|
);
|
|
}
|
|
|
|
factory NetworkFailure.timeout() {
|
|
return const NetworkFailure(
|
|
message: 'Délai d\'attente dépassé',
|
|
code: 'TIMEOUT',
|
|
);
|
|
}
|
|
|
|
factory NetworkFailure.serverUnreachable() {
|
|
return const NetworkFailure(
|
|
message: 'Serveur inaccessible',
|
|
code: 'SERVER_UNREACHABLE',
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Échec serveur (erreurs HTTP 5xx, erreurs API, etc.)
|
|
class ServerFailure extends Failure {
|
|
final int? statusCode;
|
|
|
|
const ServerFailure({
|
|
required super.message,
|
|
super.code,
|
|
super.details,
|
|
this.statusCode,
|
|
});
|
|
|
|
factory ServerFailure.internalError() {
|
|
return const ServerFailure(
|
|
message: 'Erreur interne du serveur',
|
|
code: 'INTERNAL_ERROR',
|
|
statusCode: 500,
|
|
);
|
|
}
|
|
|
|
factory ServerFailure.serviceUnavailable() {
|
|
return const ServerFailure(
|
|
message: 'Service temporairement indisponible',
|
|
code: 'SERVICE_UNAVAILABLE',
|
|
statusCode: 503,
|
|
);
|
|
}
|
|
|
|
factory ServerFailure.badGateway() {
|
|
return const ServerFailure(
|
|
message: 'Passerelle défaillante',
|
|
code: 'BAD_GATEWAY',
|
|
statusCode: 502,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Échec de validation (données invalides, contraintes non respectées)
|
|
class ValidationFailure extends Failure {
|
|
final Map<String, List<String>>? fieldErrors;
|
|
|
|
const ValidationFailure({
|
|
required super.message,
|
|
super.code,
|
|
super.details,
|
|
this.fieldErrors,
|
|
});
|
|
|
|
factory ValidationFailure.invalidData(String field, String error) {
|
|
return ValidationFailure(
|
|
message: 'Données invalides',
|
|
code: 'INVALID_DATA',
|
|
fieldErrors: {field: [error]},
|
|
);
|
|
}
|
|
|
|
factory ValidationFailure.requiredField(String field) {
|
|
return ValidationFailure(
|
|
message: 'Champ requis manquant',
|
|
code: 'REQUIRED_FIELD',
|
|
fieldErrors: {field: ['Ce champ est requis']},
|
|
);
|
|
}
|
|
|
|
factory ValidationFailure.multipleErrors(Map<String, List<String>> errors) {
|
|
return ValidationFailure(
|
|
message: 'Plusieurs erreurs de validation',
|
|
code: 'MULTIPLE_ERRORS',
|
|
fieldErrors: errors,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Échec d'authentification (login, permissions, tokens expirés)
|
|
class AuthFailure extends Failure {
|
|
const AuthFailure({
|
|
required super.message,
|
|
super.code,
|
|
super.details,
|
|
});
|
|
|
|
factory AuthFailure.invalidCredentials() {
|
|
return const AuthFailure(
|
|
message: 'Identifiants invalides',
|
|
code: 'INVALID_CREDENTIALS',
|
|
);
|
|
}
|
|
|
|
factory AuthFailure.tokenExpired() {
|
|
return const AuthFailure(
|
|
message: 'Session expirée, veuillez vous reconnecter',
|
|
code: 'TOKEN_EXPIRED',
|
|
);
|
|
}
|
|
|
|
factory AuthFailure.insufficientPermissions() {
|
|
return const AuthFailure(
|
|
message: 'Permissions insuffisantes',
|
|
code: 'INSUFFICIENT_PERMISSIONS',
|
|
);
|
|
}
|
|
|
|
factory AuthFailure.accountLocked() {
|
|
return const AuthFailure(
|
|
message: 'Compte verrouillé',
|
|
code: 'ACCOUNT_LOCKED',
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Échec de données (ressource non trouvée, conflit, etc.)
|
|
class DataFailure extends Failure {
|
|
const DataFailure({
|
|
required super.message,
|
|
super.code,
|
|
super.details,
|
|
});
|
|
|
|
factory DataFailure.notFound(String resource) {
|
|
return DataFailure(
|
|
message: '$resource non trouvé(e)',
|
|
code: 'NOT_FOUND',
|
|
details: {'resource': resource},
|
|
);
|
|
}
|
|
|
|
factory DataFailure.alreadyExists(String resource) {
|
|
return DataFailure(
|
|
message: '$resource existe déjà',
|
|
code: 'ALREADY_EXISTS',
|
|
details: {'resource': resource},
|
|
);
|
|
}
|
|
|
|
factory DataFailure.conflict(String reason) {
|
|
return DataFailure(
|
|
message: 'Conflit de données : $reason',
|
|
code: 'CONFLICT',
|
|
details: {'reason': reason},
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Échec de cache (données expirées, cache corrompu)
|
|
class CacheFailure extends Failure {
|
|
const CacheFailure({
|
|
required super.message,
|
|
super.code,
|
|
super.details,
|
|
});
|
|
|
|
factory CacheFailure.expired() {
|
|
return const CacheFailure(
|
|
message: 'Données en cache expirées',
|
|
code: 'CACHE_EXPIRED',
|
|
);
|
|
}
|
|
|
|
factory CacheFailure.corrupted() {
|
|
return const CacheFailure(
|
|
message: 'Cache corrompu',
|
|
code: 'CACHE_CORRUPTED',
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Échec de fichier (lecture, écriture, format)
|
|
class FileFailure extends Failure {
|
|
const FileFailure({
|
|
required super.message,
|
|
super.code,
|
|
super.details,
|
|
});
|
|
|
|
factory FileFailure.notFound(String filePath) {
|
|
return FileFailure(
|
|
message: 'Fichier non trouvé',
|
|
code: 'FILE_NOT_FOUND',
|
|
details: {'filePath': filePath},
|
|
);
|
|
}
|
|
|
|
factory FileFailure.accessDenied(String filePath) {
|
|
return FileFailure(
|
|
message: 'Accès au fichier refusé',
|
|
code: 'ACCESS_DENIED',
|
|
details: {'filePath': filePath},
|
|
);
|
|
}
|
|
|
|
factory FileFailure.invalidFormat(String expectedFormat) {
|
|
return FileFailure(
|
|
message: 'Format de fichier invalide',
|
|
code: 'INVALID_FORMAT',
|
|
details: {'expectedFormat': expectedFormat},
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Échec générique pour les cas non spécifiés
|
|
class UnknownFailure extends Failure {
|
|
const UnknownFailure({
|
|
required super.message,
|
|
super.code,
|
|
super.details,
|
|
});
|
|
|
|
factory UnknownFailure.fromException(Exception exception) {
|
|
return UnknownFailure(
|
|
message: 'Erreur inattendue : ${exception.toString()}',
|
|
code: 'UNKNOWN_ERROR',
|
|
details: {'exception': exception.toString()},
|
|
);
|
|
}
|
|
}
|