diff --git a/apps/ff_server/src/ff_identity_codec.erl b/apps/ff_server/src/ff_identity_codec.erl index 3446602..0cd3e6d 100644 --- a/apps/ff_server/src/ff_identity_codec.erl +++ b/apps/ff_server/src/ff_identity_codec.erl @@ -14,7 +14,6 @@ -export([marshal/2]). -export([unmarshal/2]). - %% This special functions hasn't got opposite functions. -spec unmarshal_identity_params(ff_proto_identity_thrift:'IdentityParams'()) -> ff_identity_machine:params(). diff --git a/apps/wapi/src/wapi_identity_backend.erl b/apps/wapi/src/wapi_identity_backend.erl index 688ba0b..2370f6c 100644 --- a/apps/wapi/src/wapi_identity_backend.erl +++ b/apps/wapi/src/wapi_identity_backend.erl @@ -6,6 +6,9 @@ -type id() :: binary(). -type status() :: binary(). -type result(T, E) :: {ok, T} | {error, E}. +-type identity_state() :: ff_proto_identity_thrift:'IdentityState'(). + +-export_type([identity_state/0]). -export([create_identity/2]). -export([get_identity/2]). @@ -16,6 +19,8 @@ -export([get_identity_challenge_events/2]). -export([get_identity_challenge_event/2]). +-export([get_thrift_identity/2]). + -include_lib("fistful_proto/include/ff_proto_identity_thrift.hrl"). -include_lib("fistful_proto/include/ff_proto_base_thrift.hrl"). @@ -27,17 +32,11 @@ {error, {identity, unauthorized}} . get_identity(IdentityID, HandlerContext) -> - Request = {fistful_identity, 'Get', [IdentityID, #'EventRange'{}]}, - case service_call(Request, HandlerContext) of + case get_thrift_identity(IdentityID, HandlerContext) of {ok, IdentityThrift} -> - case wapi_access_backend:check_resource(identity, IdentityThrift, HandlerContext) of - ok -> - {ok, unmarshal(identity, IdentityThrift)}; - {error, unauthorized} -> - {error, {identity, unauthorized}} - end; - {exception, #fistful_IdentityNotFound{}} -> - {error, {identity, notfound}} + {ok, unmarshal(identity, IdentityThrift)}; + {error, _} = Error -> + Error end. -spec create_identity(params(), handler_context()) -> result(map(), @@ -235,6 +234,25 @@ get_identity_challenge_event_(#{ {error, Details} end. +-spec get_thrift_identity(id(), handler_context()) -> + {ok, identity_state()} | + {error, {identity, notfound}} | + {error, {identity, unauthorized}} . + +get_thrift_identity(IdentityID, HandlerContext) -> + Request = {fistful_identity, 'Get', [IdentityID, #'EventRange'{}]}, + case service_call(Request, HandlerContext) of + {ok, IdentityThrift} -> + case wapi_access_backend:check_resource(identity, IdentityThrift, HandlerContext) of + ok -> + {ok, IdentityThrift}; + {error, unauthorized} -> + {error, {identity, unauthorized}} + end; + {exception, #fistful_IdentityNotFound{}} -> + {error, {identity, notfound}} + end. + %% %% Internal %% diff --git a/apps/wapi/src/wapi_report_backend.erl b/apps/wapi/src/wapi_report_backend.erl new file mode 100644 index 0000000..6e0fae1 --- /dev/null +++ b/apps/wapi/src/wapi_report_backend.erl @@ -0,0 +1,197 @@ +-module(wapi_report_backend). + +-include_lib("fistful_reporter_proto/include/ff_reporter_reports_thrift.hrl"). +-include_lib("file_storage_proto/include/fs_file_storage_thrift.hrl"). +-include_lib("fistful_proto/include/ff_proto_base_thrift.hrl"). +-include_lib("fistful_proto/include/ff_proto_identity_thrift.hrl"). + +-export([create_report/2]). +-export([get_report/3]). +-export([get_reports/2]). +-export([download_file/3]). + +-type id() :: binary(). +-type req_data() :: wapi_handler:req_data(). +-type handler_context() :: wapi_handler:context(). +-type response_data() :: wapi_handler:response_data(). + +-spec create_report(req_data(), handler_context()) -> + {ok, response_data()} | {error, Error} + when Error :: + {identity, unauthorized} | + {identity, notfound} | + invalid_request | + invalid_contract. + +create_report(#{ + identityID := IdentityID, + 'ReportParams' := ReportParams +}, HandlerContext) -> + case get_contract_id_from_identity(IdentityID, HandlerContext) of + {ok, ContractID} -> + Req = create_report_request(#{ + party_id => wapi_handler_utils:get_owner(HandlerContext), + contract_id => ContractID, + from_time => get_time(<<"fromTime">>, ReportParams), + to_time => get_time(<<"toTime">>, ReportParams) + }), + Call = {fistful_report, 'GenerateReport', [Req, maps:get(<<"reportType">>, ReportParams)]}, + case wapi_handler_utils:service_call(Call, HandlerContext) of + {ok, ReportID} -> + get_report(contractID, ReportID, ContractID, HandlerContext); + {exception, #ff_reports_InvalidRequest{}} -> + {error, invalid_request}; + {exception, #ff_reports_ContractNotFound{}} -> + {error, invalid_contract} + end; + {error, _} = Error -> + Error + end. + +-spec get_report(integer(), binary(), handler_context()) -> + {ok, response_data()} | {error, Error} + when Error :: + {identity, unauthorized} | + {identity, notfound} | + notfound. + +get_report(ReportID, IdentityID, HandlerContext) -> + get_report(identityID, ReportID, IdentityID, HandlerContext). + +get_report(identityID, ReportID, IdentityID, HandlerContext) -> + case get_contract_id_from_identity(IdentityID, HandlerContext) of + {ok, ContractID} -> + get_report(contractID, ReportID, ContractID, HandlerContext); + {error, _} = Error -> + Error + end; +get_report(contractID, ReportID, ContractID, HandlerContext) -> + PartyID = wapi_handler_utils:get_owner(HandlerContext), + Call = {fistful_report, 'GetReport', [PartyID, ContractID, ReportID]}, + case wapi_handler_utils:service_call(Call, HandlerContext) of + {ok, Report} -> + {ok, unmarshal_report(Report)}; + {exception, #ff_reports_ReportNotFound{}} -> + {error, notfound} + end. + +-spec get_reports(req_data(), handler_context()) -> + {ok, response_data()} | {error, Error} + when Error :: + {identity, unauthorized} | + {identity, notfound} | + invalid_request | + {dataset_too_big, integer()}. + +get_reports(#{identityID := IdentityID} = Params, HandlerContext) -> + case get_contract_id_from_identity(IdentityID, HandlerContext) of + {ok, ContractID} -> + Req = create_report_request(#{ + party_id => wapi_handler_utils:get_owner(HandlerContext), + contract_id => ContractID, + from_time => get_time(fromTime, Params), + to_time => get_time(toTime, Params) + }), + Call = {fistful_report, 'GetReports', [Req, [genlib:to_binary(maps:get(type, Params))]]}, + case wapi_handler_utils:service_call(Call, HandlerContext) of + {ok, ReportList} -> + {ok, unmarshal_reports(ReportList)}; + {exception, #ff_reports_InvalidRequest{}} -> + {error, invalid_request}; + {exception, #ff_reports_DatasetTooBig{limit = Limit}} -> + {error, {dataset_too_big, Limit}} + end; + {error, _} = Error -> + Error + end. + +-spec download_file(binary(), binary(), handler_context()) -> + {ok, response_data()} | {error, Error} + when Error :: + notfound. +download_file(FileID, ExpiresAt, HandlerContext) -> + Timestamp = wapi_utils:to_universal_time(ExpiresAt), + Call = {file_storage, 'GenerateDownloadUrl', [FileID, Timestamp]}, + case wapi_handler_utils:service_call(Call, HandlerContext) of + {exception, #file_storage_FileNotFound{}} -> + {error, notfound}; + Result-> + Result + end. + +%% Internal + +-spec get_contract_id_from_identity(id(), handler_context()) -> + {ok, id()} | {error, Error} + when Error :: + {identity, unauthorized} | + {identity, notfound}. + +get_contract_id_from_identity(IdentityID, HandlerContext) -> + case wapi_identity_backend:get_thrift_identity(IdentityID, HandlerContext) of + {ok, #idnt_IdentityState{contract_id = ContractID}} -> + {ok, ContractID}; + {error, _} = Error -> + Error + end. + +create_report_request(#{ + party_id := PartyID, + contract_id := ContractID, + from_time := FromTime, + to_time := ToTime +}) -> + #'ff_reports_ReportRequest'{ + party_id = PartyID, + contract_id = ContractID, + time_range = #'ff_reports_ReportTimeRange'{ + from_time = FromTime, + to_time = ToTime + } + }. + +get_time(Key, Req) -> + case genlib_map:get(Key, Req) of + Timestamp when is_binary(Timestamp) -> + wapi_utils:to_universal_time(Timestamp); + undefined -> + undefined + end. + +%% Marshaling + +unmarshal_reports(List) -> + lists:map(fun(Report) -> unmarshal_report(Report) end, List). + +unmarshal_report(#ff_reports_Report{ + report_id = ReportID, + time_range = TimeRange, + created_at = CreatedAt, + report_type = Type, + status = Status, + file_data_ids = Files +}) -> + genlib_map:compact(#{ + <<"id">> => ReportID, + <<"fromTime">> => TimeRange#ff_reports_ReportTimeRange.from_time, + <<"toTime">> => TimeRange#ff_reports_ReportTimeRange.to_time, + <<"createdAt">> => CreatedAt, + <<"status">> => unmarshal_report_status(Status), + <<"type">> => Type, + <<"files">> => unmarshal_report_files(Files) + }). + +unmarshal_report_status(pending) -> + <<"pending">>; +unmarshal_report_status(created) -> + <<"created">>; +unmarshal_report_status(canceled) -> + <<"canceled">>. + +unmarshal_report_files(undefined) -> + []; +unmarshal_report_files(Files) -> + lists:map(fun(File) -> unmarshal_report_file(File) end, Files). + +unmarshal_report_file(File) -> + #{<<"id">> => File}. diff --git a/apps/wapi/src/wapi_wallet_thrift_handler.erl b/apps/wapi/src/wapi_wallet_thrift_handler.erl index 5554075..831a10d 100644 --- a/apps/wapi/src/wapi_wallet_thrift_handler.erl +++ b/apps/wapi/src/wapi_wallet_thrift_handler.erl @@ -586,6 +586,83 @@ process_request('IssueP2PTransferTicket', #{ wapi_handler_utils:reply_error(404) end; +%% Reports + +process_request('CreateReport', Params, Context, _Opts) -> + case wapi_report_backend:create_report(Params, Context) of + {ok, Report} -> wapi_handler_utils:reply_ok(201, Report); + {error, {identity, notfound}} -> wapi_handler_utils:reply_ok(400, #{ + <<"errorType">> => <<"NotFound">>, + <<"name">> => <<"identity">>, + <<"description">> => <<"identity not found">> + }); + {error, {identity, unauthorized}} -> wapi_handler_utils:reply_ok(400, #{ + <<"errorType">> => <<"NotFound">>, + <<"name">> => <<"identity">>, + <<"description">> => <<"identity not found">> + }); + {error, invalid_request} -> wapi_handler_utils:reply_ok(400, #{ + <<"errorType">> => <<"NoMatch">>, + <<"name">> => <<"timestamps">>, + <<"description">> => <<"invalid time range">> + }); + {error, invalid_contract} -> wapi_handler_utils:reply_ok(400, #{ + <<"errorType">> => <<"NotFound">>, + <<"name">> => <<"contractID">>, + <<"description">> => <<"contract not found">> + }) + end; +process_request('GetReport', #{ + identityID := IdentityID, + reportID := ReportId +}, Context, _Opts) -> + case wapi_report_backend:get_report(ReportId, IdentityID, Context) of + {ok, Report} -> wapi_handler_utils:reply_ok(200, Report); + {error, {identity, notfound}} -> wapi_handler_utils:reply_ok(400, #{ + <<"errorType">> => <<"NotFound">>, + <<"name">> => <<"identity">>, + <<"description">> => <<"identity not found">> + }); + {error, {identity, unauthorized}} -> wapi_handler_utils:reply_ok(400, #{ + <<"errorType">> => <<"NotFound">>, + <<"name">> => <<"identity">>, + <<"description">> => <<"identity not found">> + }); + {error, notfound} -> wapi_handler_utils:reply_ok(404) + end; +process_request('GetReports', Params, Context, _Opts) -> + case wapi_report_backend:get_reports(Params, Context) of + {ok, ReportList} -> wapi_handler_utils:reply_ok(200, ReportList); + {error, {identity, notfound}} -> wapi_handler_utils:reply_ok(400, #{ + <<"errorType">> => <<"NotFound">>, + <<"name">> => <<"identity">>, + <<"description">> => <<"identity not found">> + }); + {error, {identity, unauthorized}} -> wapi_handler_utils:reply_ok(400, #{ + <<"errorType">> => <<"NotFound">>, + <<"name">> => <<"identity">>, + <<"description">> => <<"identity not found">> + }); + {error, invalid_request} -> wapi_handler_utils:reply_ok(400, #{ + <<"errorType">> => <<"NoMatch">>, + <<"name">> => <<"timestamps">>, + <<"description">> => <<"invalid time range">> + }); + {error, {dataset_too_big, Limit}} -> wapi_handler_utils:reply_ok(400, #{ + <<"errorType">> => <<"WrongLength">>, + <<"name">> => <<"limitExceeded">>, + <<"description">> => io_lib:format("Max limit: ~p", [Limit]) + }) + end; +process_request('DownloadFile', #{fileID := FileId}, Context, _Opts) -> + ExpiresAt = get_default_url_lifetime(), + case wapi_report_backend:download_file(FileId, ExpiresAt, Context) of + {ok, URL} -> + wapi_handler_utils:reply_ok(201, #{<<"url">> => URL, <<"expiresAt">> => ExpiresAt}); + {error, notfound} -> + wapi_handler_utils:reply_ok(404) + end; + %% Fallback to legacy handler process_request(OperationID, Params, Context, Opts) -> @@ -614,3 +691,10 @@ get_expiration_deadline(Expiration) -> false -> {error, expired} end. + +-define(DEFAULT_URL_LIFETIME, 60). % seconds + +get_default_url_lifetime() -> + Now = erlang:system_time(second), + Lifetime = application:get_env(wapi, file_storage_url_lifetime, ?DEFAULT_URL_LIFETIME), + genlib_rfc3339:format(Now + Lifetime, second). diff --git a/apps/wapi/src/wapi_withdrawal_backend.erl b/apps/wapi/src/wapi_withdrawal_backend.erl index 30732a1..93e637a 100644 --- a/apps/wapi/src/wapi_withdrawal_backend.erl +++ b/apps/wapi/src/wapi_withdrawal_backend.erl @@ -8,7 +8,6 @@ }). -define(statusChange(Status), {status_changed, #wthd_StatusChange{status = Status}}). - -type req_data() :: wapi_handler:req_data(). -type handler_context() :: wapi_handler:context(). -type response_data() :: wapi_handler:response_data(). diff --git a/apps/wapi/test/wapi_report_tests_SUITE.erl b/apps/wapi/test/wapi_report_tests_SUITE.erl index 34488c3..1621c0e 100644 --- a/apps/wapi/test/wapi_report_tests_SUITE.erl +++ b/apps/wapi/test/wapi_report_tests_SUITE.erl @@ -8,6 +8,7 @@ -include_lib("fistful_proto/include/ff_proto_base_thrift.hrl"). -include_lib("jose/include/jose_jwk.hrl"). -include_lib("wapi_wallet_dummy_data.hrl"). +-include_lib("fistful_proto/include/ff_proto_identity_thrift.hrl"). -export([all/0]). -export([groups/0]). @@ -130,17 +131,19 @@ end_per_testcase(_Name, C) -> -spec create_report_ok_test(config()) -> _. create_report_ok_test(C) -> - {ok, Identity} = create_identity(C), - IdentityID = maps:get(<<"id">>, Identity), - wapi_ct_helper:mock_services([{fistful_report, fun - ('GenerateReport', _) -> {ok, ?REPORT_ID}; - ('GetReport', _) -> {ok, ?REPORT} - end}], C), + PartyID = ?config(party, C), + wapi_ct_helper:mock_services([ + {fistful_report, fun + ('GenerateReport', _) -> {ok, ?REPORT_ID}; + ('GetReport', _) -> {ok, ?REPORT} + end}, + {fistful_identity, fun('Get', _) -> {ok, ?IDENTITY(PartyID)} end} + ], C), {ok, _} = call_api( fun swag_client_wallet_reports_api:create_report/3, #{ binding => #{ - <<"identityID">> => IdentityID + <<"identityID">> => ?STRING }, body => #{ <<"reportType">> => <<"withdrawalRegistry">>, @@ -154,16 +157,18 @@ create_report_ok_test(C) -> -spec get_report_ok_test(config()) -> _. get_report_ok_test(C) -> - {ok, Identity} = create_identity(C), - IdentityID = maps:get(<<"id">>, Identity), - wapi_ct_helper:mock_services([{fistful_report, fun - ('GetReport', _) -> {ok, ?REPORT} - end}], C), + PartyID = ?config(party, C), + wapi_ct_helper:mock_services([ + {fistful_report, fun + ('GetReport', _) -> {ok, ?REPORT} + end}, + {fistful_identity, fun('Get', _) -> {ok, ?IDENTITY(PartyID)} end} + ], C), {ok, _} = call_api( fun swag_client_wallet_reports_api:get_report/3, #{ binding => #{ - <<"identityID">> => IdentityID, + <<"identityID">> => ?STRING, <<"reportID">> => ?INTEGER } }, @@ -173,19 +178,21 @@ get_report_ok_test(C) -> -spec get_reports_ok_test(config()) -> _. get_reports_ok_test(C) -> - {ok, Identity} = create_identity(C), - IdentityID = maps:get(<<"id">>, Identity), - wapi_ct_helper:mock_services([{fistful_report, fun - ('GetReports', _) -> {ok, [ - ?REPORT_EXT(pending, []), - ?REPORT_EXT(created, undefined), - ?REPORT_WITH_STATUS(canceled)]} - end}], C), + PartyID = ?config(party, C), + wapi_ct_helper:mock_services([ + {fistful_report, fun + ('GetReports', _) -> {ok, [ + ?REPORT_EXT(pending, []), + ?REPORT_EXT(created, undefined), + ?REPORT_WITH_STATUS(canceled)]} + end}, + {fistful_identity, fun('Get', _) -> {ok, ?IDENTITY(PartyID)} end} + ], C), {ok, _} = call_api( fun swag_client_wallet_reports_api:get_reports/3, #{ binding => #{ - <<"identityID">> => IdentityID + <<"identityID">> => ?STRING }, qs_val => #{ <<"fromTime">> => ?TIMESTAMP, @@ -200,11 +207,14 @@ get_reports_ok_test(C) -> _. reports_with_wrong_identity_ok_test(C) -> IdentityID = <<"WrongIdentity">>, - wapi_ct_helper:mock_services([{fistful_report, fun - ('GenerateReport', _) -> {ok, ?REPORT_ID}; - ('GetReport', _) -> {ok, ?REPORT}; - ('GetReports', _) -> {ok, [?REPORT, ?REPORT, ?REPORT]} - end}], C), + wapi_ct_helper:mock_services([ + {fistful_report, fun + ('GenerateReport', _) -> {ok, ?REPORT_ID}; + ('GetReport', _) -> {ok, ?REPORT}; + ('GetReports', _) -> {ok, [?REPORT, ?REPORT, ?REPORT]} + end}, + {fistful_identity, fun('Get', _) -> throw(#fistful_IdentityNotFound{}) end} + ], C), ?emptyresp(400) = call_api( fun swag_client_wallet_reports_api:create_report/3, #{ @@ -276,21 +286,3 @@ create_party(_C) -> ID = genlib:bsuuid(), _ = ff_party:create(ID), ID. - -create_identity(C) -> - PartyID = ?config(party, C), - Params = #{ - <<"provider">> => <<"good-one">>, - <<"class">> => <<"person">>, - <<"name">> => <<"HAHA NO2">> - }, - wapi_wallet_ff_backend:create_identity(Params, create_context(PartyID, C)). - -create_context(PartyID, C) -> - maps:merge(wapi_ct_helper:create_auth_ctx(PartyID), create_woody_ctx(C)). - -create_woody_ctx(C) -> - #{ - woody_context => ct_helper:get_woody_ctx(C) - }. - diff --git a/apps/wapi/test/wapi_wallet_dummy_data.hrl b/apps/wapi/test/wapi_wallet_dummy_data.hrl index a2f05f0..9ccb8e4 100644 --- a/apps/wapi/test/wapi_wallet_dummy_data.hrl +++ b/apps/wapi/test/wapi_wallet_dummy_data.hrl @@ -177,6 +177,7 @@ name = ?STRING, party_id = ?STRING, provider_id = ?STRING, + contract_id = ?STRING, class_id = ?STRING, metadata = ?DEFAULT_METADATA(), context = Context