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 create(Request request) async { final authenticated = authenticatedRequestFrom(request); if (authenticated == null) { return _errorResponse(401, 'Unauthorized.'); } try { final body = await _readJsonObject(request); final shareKind = _optionalString(body, 'shareKind') ?? 'single'; final result = await createShare.execute( senderUserId: authenticated.user.id, shareKind: shareKind, packName: _optionalString(body, 'packName'), resourceType: _optionalString(body, 'resourceType'), payloadJson: _optionalObject(body, 'payload'), packItems: shareKind == ShareKind.pack.wireName ? _packItems(body['items']) : const [], recipientEmails: _requiredStringList(body, 'recipientEmails'), ); return _jsonResponse(201, { 'shareId': result.share.id, 'shareKind': result.share.kind.wireName, '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 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, 'shareKind': item.share.kind.wireName, 'packName': item.share.packName, '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 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, { if (result.createdResources.length == 1) 'createdResource': _resourceJson(result.createdResource), 'createdResources': [ for (final resource in result.createdResources) _resourceJson(resource), ], }); } on ShareNotFoundException catch (error) { return _errorResponse(404, error.message); } on ShareConflictException catch (error) { return _errorResponse(409, error.message); } } Future 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 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 _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, }; } List _packItems(Object? rawItems) { if (rawItems is! List) { throw const FormatException('items must be an array.'); } return [ for (final rawItem in rawItems) if (rawItem is Map) _packItem(Map.from(rawItem)) else throw const FormatException('items must contain only objects.'), ]; } SharePackItemInput _packItem(Map item) { return SharePackItemInput( resourceType: _requiredString(item, 'resourceType'), payloadJson: _requiredObject(item, 'payload'), ); } Future> _readJsonObject(Request request) async { final raw = await request.readAsString(); final decoded = jsonDecode(raw); if (decoded is! Map) { throw const FormatException('Request body must be a JSON object.'); } return decoded; } String _requiredString(Map 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; } String? _optionalString(Map body, String key) { final value = body[key]; if (value == null) { return null; } if (value is! String) { throw FormatException('$key must be a string.'); } final trimmed = value.trim(); return trimmed.isEmpty ? null : trimmed; } Map _requiredObject(Map body, String key) { final value = body[key]; if (value is Map) { return value; } if (value is Map) { return Map.from(value); } throw FormatException('$key must be a JSON object.'); } Map? _optionalObject(Map body, String key) { if (!body.containsKey(key)) { return null; } return _requiredObject(body, key); } List _requiredStringList(Map 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 body) { return Response( statusCode, body: jsonEncode(body), headers: {'content-type': 'application/json'}, ); } Response _errorResponse(int statusCode, String message) { return _jsonResponse(statusCode, {'error': message}); }