feat(mobile): Implement Keycloak WebView authentication with HTTP callback
- 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
This commit is contained in:
271
unionflow-mobile-apps/lib/core/failures/failures.dart
Normal file
271
unionflow-mobile-apps/lib/core/failures/failures.dart
Normal file
@@ -0,0 +1,271 @@
|
||||
/// 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()},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user