Files
unionflow-mobile-apps/lib/features/communication/data/datasources/messaging_remote_datasource.dart
dahoud 04896349d6 fix: BUG-02 IC-03 session expirée sur Workflow Financier et Messagerie
Ajoute KeycloakAuthService.getValidAccessToken() qui vérifie l'expiration
du JWT et rafraîchit automatiquement avant de retourner le token.

FinanceWorkflowRemoteDatasource et MessagingRemoteDatasource injectent
désormais KeycloakAuthService au lieu de FlutterSecureStorage directement,
ce qui évite d'envoyer un Bearer expiré et d'obtenir un 401 silencieux.
2026-04-10 20:53:28 +00:00

322 lines
10 KiB
Dart

/// Datasource distant pour la communication (API)
library messaging_remote_datasource;
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'package:injectable/injectable.dart';
import '../../../../core/config/environment.dart';
import '../../../../core/error/exceptions.dart';
import '../../../authentication/data/datasources/keycloak_auth_service.dart';
import '../models/message_model.dart';
import '../models/conversation_model.dart';
import '../../domain/entities/message.dart';
@lazySingleton
class MessagingRemoteDatasource {
final http.Client client;
final KeycloakAuthService authService;
MessagingRemoteDatasource({
required this.client,
required this.authService,
});
/// Headers HTTP avec authentification — rafraîchit le token si expiré (fix IC-03)
Future<Map<String, String>> _getHeaders() async {
final token = await authService.getValidAccessToken();
return {
'Content-Type': 'application/json',
'Accept': 'application/json',
if (token != null) 'Authorization': 'Bearer $token',
};
}
// === CONVERSATIONS ===
Future<List<ConversationModel>> getConversations({
String? organizationId,
bool includeArchived = false,
}) async {
final uri = Uri.parse('${AppConfig.apiBaseUrl}/api/conversations')
.replace(queryParameters: {
if (organizationId != null) 'organisationId': organizationId,
'includeArchived': includeArchived.toString(),
});
final response = await client.get(uri, headers: await _getHeaders());
if (response.statusCode == 200) {
final List<dynamic> jsonList = json.decode(response.body);
return jsonList
.map((json) => ConversationModel.fromJson(json))
.toList();
} else if (response.statusCode == 401) {
throw UnauthorizedException();
} else {
throw ServerException('Erreur lors de la récupération des conversations');
}
}
Future<ConversationModel> getConversationById(String conversationId) async {
final uri = Uri.parse(
'${AppConfig.apiBaseUrl}/api/conversations/$conversationId');
final response = await client.get(uri, headers: await _getHeaders());
if (response.statusCode == 200) {
return ConversationModel.fromJson(json.decode(response.body));
} else if (response.statusCode == 404) {
throw NotFoundException('Conversation non trouvée');
} else if (response.statusCode == 401) {
throw UnauthorizedException();
} else {
throw ServerException('Erreur lors de la récupération de la conversation');
}
}
Future<ConversationModel> createConversation({
required String name,
required List<String> participantIds,
String? organizationId,
String? description,
}) async {
final uri =
Uri.parse('${AppConfig.apiBaseUrl}/api/conversations');
final body = json.encode({
'name': name,
'participantIds': participantIds,
'type': 'GROUP', // Default to GROUP for multi-participant conversations
if (organizationId != null) 'organisationId': organizationId,
if (description != null) 'description': description,
});
final response = await client.post(
uri,
headers: await _getHeaders(),
body: body,
);
if (response.statusCode == 201 || response.statusCode == 200) {
return ConversationModel.fromJson(json.decode(response.body));
} else if (response.statusCode == 401) {
throw UnauthorizedException();
} else {
throw ServerException('Erreur lors de la création de la conversation');
}
}
// === MESSAGES ===
Future<List<MessageModel>> getMessages({
required String conversationId,
int? limit,
String? beforeMessageId,
}) async {
final uri = Uri.parse('${AppConfig.apiBaseUrl}/api/messages')
.replace(queryParameters: {
'conversationId': conversationId,
if (limit != null) 'limit': limit.toString(),
// beforeMessageId not supported by backend yet, omit
});
final response = await client.get(uri, headers: await _getHeaders());
if (response.statusCode == 200) {
final List<dynamic> jsonList = json.decode(response.body);
return jsonList.map((json) => MessageModel.fromJson(json)).toList();
} else if (response.statusCode == 401) {
throw UnauthorizedException();
} else {
throw ServerException('Erreur lors de la récupération des messages');
}
}
Future<MessageModel> sendMessage({
required String conversationId,
required String content,
List<String>? attachments,
MessagePriority priority = MessagePriority.normal,
}) async {
final uri = Uri.parse('${AppConfig.apiBaseUrl}/api/messages');
final body = json.encode({
'conversationId': conversationId,
'content': content,
if (attachments != null) 'attachments': attachments,
'priority': priority.name.toUpperCase(),
});
final response = await client.post(
uri,
headers: await _getHeaders(),
body: body,
);
if (response.statusCode == 201 || response.statusCode == 200) {
return MessageModel.fromJson(json.decode(response.body));
} else if (response.statusCode == 401) {
throw UnauthorizedException();
} else {
throw ServerException('Erreur lors de l\'envoi du message');
}
}
Future<MessageModel> sendBroadcast({
required String organizationId,
required String subject,
required String content,
MessagePriority priority = MessagePriority.normal,
List<String>? attachments,
}) async {
final uri = Uri.parse('${AppConfig.apiBaseUrl}/api/messaging/broadcast');
final body = json.encode({
'organizationId': organizationId,
'subject': subject,
'content': content,
'priority': priority.name,
if (attachments != null) 'attachments': attachments,
});
final response = await client.post(
uri,
headers: await _getHeaders(),
body: body,
);
if (response.statusCode == 201 || response.statusCode == 200) {
return MessageModel.fromJson(json.decode(response.body));
} else if (response.statusCode == 401) {
throw UnauthorizedException();
} else if (response.statusCode == 403) {
throw ForbiddenException('Permission insuffisante pour envoyer un broadcast');
} else {
throw ServerException('Erreur lors de l\'envoi du broadcast');
}
}
// === CONVERSATION ACTIONS ===
Future<void> archiveConversation(String conversationId, {bool archive = true}) async {
final uri = Uri.parse(
'${AppConfig.apiBaseUrl}/api/conversations/$conversationId/archive')
.replace(queryParameters: {'archive': archive.toString()});
final response = await client.put(uri, headers: await _getHeaders());
if (response.statusCode != 200 && response.statusCode != 204) {
if (response.statusCode == 401) {
throw UnauthorizedException();
} else {
throw ServerException('Erreur lors de l\'archivage de la conversation');
}
}
}
Future<void> markConversationAsRead(String conversationId) async {
final uri = Uri.parse(
'${AppConfig.apiBaseUrl}/api/conversations/$conversationId/mark-read');
final response = await client.put(uri, headers: await _getHeaders());
if (response.statusCode != 200 && response.statusCode != 204) {
if (response.statusCode == 401) {
throw UnauthorizedException();
} else {
throw ServerException('Erreur lors du marquage de la conversation comme lue');
}
}
}
Future<void> toggleMuteConversation(String conversationId) async {
final uri = Uri.parse(
'${AppConfig.apiBaseUrl}/api/conversations/$conversationId/toggle-mute');
final response = await client.put(uri, headers: await _getHeaders());
if (response.statusCode != 200 && response.statusCode != 204) {
if (response.statusCode == 401) {
throw UnauthorizedException();
} else {
throw ServerException('Erreur lors du toggle mute de la conversation');
}
}
}
Future<void> togglePinConversation(String conversationId) async {
final uri = Uri.parse(
'${AppConfig.apiBaseUrl}/api/conversations/$conversationId/toggle-pin');
final response = await client.put(uri, headers: await _getHeaders());
if (response.statusCode != 200 && response.statusCode != 204) {
if (response.statusCode == 401) {
throw UnauthorizedException();
} else {
throw ServerException('Erreur lors du toggle pin de la conversation');
}
}
}
// === MESSAGE ACTIONS ===
Future<MessageModel> editMessage({
required String messageId,
required String newContent,
}) async {
final uri = Uri.parse('${AppConfig.apiBaseUrl}/api/messages/$messageId');
final body = json.encode({'content': newContent});
final response = await client.put(
uri,
headers: await _getHeaders(),
body: body,
);
if (response.statusCode == 200) {
return MessageModel.fromJson(json.decode(response.body));
} else if (response.statusCode == 401) {
throw UnauthorizedException();
} else if (response.statusCode == 404) {
throw NotFoundException('Message non trouvé');
} else {
throw ServerException('Erreur lors de l\'édition du message');
}
}
Future<void> deleteMessage(String messageId) async {
final uri = Uri.parse('${AppConfig.apiBaseUrl}/api/messages/$messageId');
final response = await client.delete(uri, headers: await _getHeaders());
if (response.statusCode != 200 && response.statusCode != 204) {
if (response.statusCode == 401) {
throw UnauthorizedException();
} else if (response.statusCode == 404) {
throw NotFoundException('Message non trouvé');
} else {
throw ServerException('Erreur lors de la suppression du message');
}
}
}
Future<void> markMessageAsRead(String messageId) async {
// Backend has no per-message read endpoint — use markConversationAsRead
if (AppConfig.enableLogging) {
debugPrint('[Messaging] markMessageAsRead ignored (no per-message endpoint), messageId=$messageId');
}
}
Future<int> getUnreadCount({String? organizationId}) async {
try {
final conversations = await getConversations(organizationId: organizationId);
return conversations.fold<int>(0, (sum, c) => sum + c.unreadCount);
} catch (_) {
return 0;
}
}
}