Ajoute l'endpoint api/share_api.dart, les use cases de partage (application/share_use_cases.dart) et l'adapter Postgres (infrastructure/postgres/share_repository.dart). L'acceptation d'un partage crée une nouvelle ressource synced_resources avec de nouveaux IDs pour le destinataire, sans jamais modifier la ressource source de l'émetteur ; gestion des conflits révoqué/déjà répondu. dart pub get OK, dart analyze clean, dart test 24/24 vert. Use case d'acceptation relu manuellement et jugé correct ; pas de test de bout en bout contre un vrai PostgreSQL faute d'accès Docker dans ce sandbox. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
196 lines
5.9 KiB
Dart
196 lines
5.9 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:shelf/shelf.dart';
|
|
|
|
import '../application/application.dart';
|
|
import '../domain/domain.dart';
|
|
import 'auth_api.dart';
|
|
|
|
final class ShareApi {
|
|
const ShareApi({
|
|
required this.createShare,
|
|
required this.listInbox,
|
|
required this.acceptShare,
|
|
required this.declineShare,
|
|
required this.revokeShare,
|
|
required this.authenticateRequest,
|
|
});
|
|
|
|
final CreateShareUseCase createShare;
|
|
final ListInboxUseCase listInbox;
|
|
final AcceptShareUseCase acceptShare;
|
|
final DeclineShareUseCase declineShare;
|
|
final RevokeShareUseCase revokeShare;
|
|
final AuthenticateRequestUseCase authenticateRequest;
|
|
|
|
Future<Response> create(Request request) async {
|
|
final authenticated = authenticatedRequestFrom(request);
|
|
if (authenticated == null) {
|
|
return _errorResponse(401, 'Unauthorized.');
|
|
}
|
|
try {
|
|
final body = await _readJsonObject(request);
|
|
final result = await createShare.execute(
|
|
senderUserId: authenticated.user.id,
|
|
resourceType: _requiredString(body, 'resourceType'),
|
|
payloadJson: _requiredObject(body, 'payload'),
|
|
recipientEmails: _requiredStringList(body, 'recipientEmails'),
|
|
);
|
|
return _jsonResponse(201, {
|
|
'shareId': result.share.id,
|
|
'recipientUserIds': result.recipientUserIds,
|
|
'unresolvedEmails': result.unresolvedEmails,
|
|
});
|
|
} on ValidationException catch (error) {
|
|
return _errorResponse(400, error.message);
|
|
} on FormatException catch (error) {
|
|
return _errorResponse(400, error.message);
|
|
}
|
|
}
|
|
|
|
Future<Response> inbox(Request request) async {
|
|
final authenticated = authenticatedRequestFrom(request);
|
|
if (authenticated == null) {
|
|
return _errorResponse(401, 'Unauthorized.');
|
|
}
|
|
final items = await listInbox.execute(
|
|
recipientUserId: authenticated.user.id,
|
|
);
|
|
return _jsonResponse(200, {
|
|
'items': [
|
|
for (final item in items)
|
|
{
|
|
'shareId': item.share.id,
|
|
'senderUserId': item.share.senderUserId,
|
|
'resourceType': item.share.resourceType.wireName,
|
|
'payload': item.share.payloadJson,
|
|
'status': item.recipient.status.wireName,
|
|
'createdAt': item.share.createdAt.toIso8601String(),
|
|
'respondedAt': item.recipient.respondedAt?.toIso8601String(),
|
|
},
|
|
],
|
|
});
|
|
}
|
|
|
|
Future<Response> accept(Request request, String shareId) async {
|
|
final authenticated = authenticatedRequestFrom(request);
|
|
if (authenticated == null) {
|
|
return _errorResponse(401, 'Unauthorized.');
|
|
}
|
|
try {
|
|
final result = await acceptShare.execute(
|
|
shareId: shareId,
|
|
recipientUserId: authenticated.user.id,
|
|
);
|
|
return _jsonResponse(200, {
|
|
'createdResource': _resourceJson(result.createdResource),
|
|
});
|
|
} on ShareNotFoundException catch (error) {
|
|
return _errorResponse(404, error.message);
|
|
} on ShareConflictException catch (error) {
|
|
return _errorResponse(409, error.message);
|
|
}
|
|
}
|
|
|
|
Future<Response> decline(Request request, String shareId) async {
|
|
final authenticated = authenticatedRequestFrom(request);
|
|
if (authenticated == null) {
|
|
return _errorResponse(401, 'Unauthorized.');
|
|
}
|
|
try {
|
|
await declineShare.execute(
|
|
shareId: shareId,
|
|
recipientUserId: authenticated.user.id,
|
|
);
|
|
return Response(204);
|
|
} on ShareNotFoundException catch (error) {
|
|
return _errorResponse(404, error.message);
|
|
} on ShareConflictException catch (error) {
|
|
return _errorResponse(409, error.message);
|
|
}
|
|
}
|
|
|
|
Future<Response> revoke(Request request, String shareId) async {
|
|
final authenticated = authenticatedRequestFrom(request);
|
|
if (authenticated == null) {
|
|
return _errorResponse(401, 'Unauthorized.');
|
|
}
|
|
try {
|
|
await revokeShare.execute(
|
|
shareId: shareId,
|
|
senderUserId: authenticated.user.id,
|
|
);
|
|
return Response(204);
|
|
} on ShareNotFoundException catch (error) {
|
|
return _errorResponse(404, error.message);
|
|
}
|
|
}
|
|
}
|
|
|
|
Map<String, Object?> _resourceJson(SyncedResource resource) {
|
|
return {
|
|
'serverId': resource.serverId,
|
|
'clientId': resource.clientId,
|
|
'resourceType': resource.resourceType.wireName,
|
|
'schemaVersion': resource.schemaVersion,
|
|
'clientUpdatedAt': resource.clientUpdatedAt.toIso8601String(),
|
|
'serverUpdatedAt': resource.serverUpdatedAt.toIso8601String(),
|
|
'deletedAt': resource.deletedAt?.toIso8601String(),
|
|
'payload': resource.payloadJson,
|
|
};
|
|
}
|
|
|
|
Future<Map<String, Object?>> _readJsonObject(Request request) async {
|
|
final raw = await request.readAsString();
|
|
final decoded = jsonDecode(raw);
|
|
if (decoded is! Map<String, Object?>) {
|
|
throw const FormatException('Request body must be a JSON object.');
|
|
}
|
|
return decoded;
|
|
}
|
|
|
|
String _requiredString(Map<String, Object?> body, String key) {
|
|
final value = body[key];
|
|
if (value is! String || value.trim().isEmpty) {
|
|
throw FormatException('$key must be a non-empty string.');
|
|
}
|
|
return value;
|
|
}
|
|
|
|
Map<String, Object?> _requiredObject(Map<String, Object?> body, String key) {
|
|
final value = body[key];
|
|
if (value is Map<String, Object?>) {
|
|
return value;
|
|
}
|
|
if (value is Map) {
|
|
return Map<String, Object?>.from(value);
|
|
}
|
|
throw FormatException('$key must be a JSON object.');
|
|
}
|
|
|
|
List<String> _requiredStringList(Map<String, Object?> body, String key) {
|
|
final value = body[key];
|
|
if (value is! List) {
|
|
throw FormatException('$key must be an array.');
|
|
}
|
|
return [
|
|
for (final item in value)
|
|
if (item is String)
|
|
item
|
|
else
|
|
throw FormatException('$key must contain only strings.'),
|
|
];
|
|
}
|
|
|
|
Response _jsonResponse(int statusCode, Map<String, Object?> body) {
|
|
return Response(
|
|
statusCode,
|
|
body: jsonEncode(body),
|
|
headers: {'content-type': 'application/json'},
|
|
);
|
|
}
|
|
|
|
Response _errorResponse(int statusCode, String message) {
|
|
return _jsonResponse(statusCode, {'error': message});
|
|
}
|