mirror of
https://github.com/valitydev/fistful-server.git
synced 2024-11-06 02:35:18 +00:00
Nail wapi app here for a while
This commit is contained in:
parent
db02893b11
commit
623197fba5
4
.gitignore
vendored
4
.gitignore
vendored
@ -6,9 +6,13 @@ log
|
||||
erl_crash.dump
|
||||
.tags*
|
||||
*.sublime-workspace
|
||||
.edts
|
||||
.DS_Store
|
||||
Dockerfile
|
||||
docker-compose.yml
|
||||
/.idea/
|
||||
*.beam
|
||||
/test/log/
|
||||
|
||||
# wapi
|
||||
apps/swag_*
|
||||
|
4
.gitmodules
vendored
4
.gitmodules
vendored
@ -1,3 +1,7 @@
|
||||
[submodule "build-utils"]
|
||||
path = build-utils
|
||||
url = git+ssh://github.com/rbkmoney/build_utils
|
||||
[submodule "schemes/swag"]
|
||||
path = schemes/swag
|
||||
url = git@github.com:rbkmoney/swag-wallets.git
|
||||
branch = release/v0
|
||||
|
@ -5,6 +5,10 @@ MAINTAINER Andrey Mayorov <a.mayorov@rbkmoney.com>
|
||||
COPY ./_build/prod/rel/${SERVICE_NAME} /opt/${SERVICE_NAME}
|
||||
CMD /opt/${SERVICE_NAME}/bin/${SERVICE_NAME} foreground
|
||||
EXPOSE 8022
|
||||
|
||||
# wapi
|
||||
EXPOSE 8080
|
||||
|
||||
# A bit of magic below to get a proper branch name
|
||||
# even when the HEAD is detached (Hey Jenkins!
|
||||
# BRANCH_NAME is available in Jenkins env).
|
||||
|
85
Makefile
85
Makefile
@ -1,5 +1,8 @@
|
||||
REBAR := $(or $(shell which rebar3), $(error "`rebar3' executable missing"))
|
||||
REBAR = $(or $(shell which rebar3), $(error "`rebar3' executable missing"))
|
||||
SUBMODULES = build-utils
|
||||
|
||||
# wapi
|
||||
SUBMODULES += schemes/swag
|
||||
SUBTARGETS = $(patsubst %,%/.git,$(SUBMODULES))
|
||||
|
||||
UTILS_PATH := build-utils
|
||||
@ -17,9 +20,10 @@ BASE_IMAGE_NAME := service_erlang
|
||||
BASE_IMAGE_TAG := 16e2b3ef17e5fdefac8554ced9c2c74e5c6e9e11
|
||||
|
||||
# Build image tag to be used
|
||||
BUILD_IMAGE_TAG := 562313697353c29d4b34fb081a8b70e8c2207134
|
||||
BUILD_IMAGE_TAG := 585ec439a97bade30cfcebc36cefdb45f13f3372
|
||||
|
||||
CALL_ANYWHERE := all submodules compile xref lint dialyze release clean distclean
|
||||
CALL_ANYWHERE += generate regenerate
|
||||
|
||||
CALL_W_CONTAINER := $(CALL_ANYWHERE) test
|
||||
|
||||
@ -36,7 +40,7 @@ $(SUBTARGETS): %/.git: %
|
||||
|
||||
submodules: $(SUBTARGETS)
|
||||
|
||||
compile: submodules
|
||||
compile: submodules generate
|
||||
$(REBAR) compile
|
||||
|
||||
xref: submodules
|
||||
@ -54,12 +58,12 @@ release: submodules
|
||||
clean:
|
||||
$(REBAR) clean
|
||||
|
||||
distclean:
|
||||
distclean: swag_server.distclean swag_client.distclean
|
||||
$(REBAR) clean -a
|
||||
rm -rf _build
|
||||
|
||||
test: submodules
|
||||
$(REBAR) ct
|
||||
$(REBAR) eunit ct
|
||||
|
||||
TESTSUITES = $(wildcard apps/*/test/*_SUITE.erl)
|
||||
|
||||
@ -71,3 +75,74 @@ test.$(patsubst ff_%_SUITE.erl,%,$(notdir $(1))): $(1) submodules
|
||||
endef
|
||||
|
||||
$(foreach suite,$(TESTSUITES),$(eval $(call testsuite,$(suite))))
|
||||
|
||||
|
||||
#
|
||||
# wapi
|
||||
#
|
||||
|
||||
.PHONY: generate regenerate swag_server.generate swag_server.regenerate swag_client.generate swag_client.regenerate
|
||||
|
||||
generate: swag_server.generate swag_client.generate
|
||||
|
||||
regenerate: swag_server.regenerate swag_client.regenerate
|
||||
|
||||
SWAGGER_CODEGEN = $(call which, swagger-codegen)
|
||||
SWAGGER_SCHEME_BASE_PATH := schemes/swag
|
||||
APP_PATH := apps
|
||||
SWAGGER_SCHEME_API_PATH := $(SWAGGER_SCHEME_BASE_PATH)/api
|
||||
SWAG_SPEC_FILE := swagger.yaml
|
||||
|
||||
# Swagger server
|
||||
|
||||
SWAG_SERVER_PREFIX := swag_server
|
||||
SWAG_SERVER_APP_TARGET := $(APP_PATH)/$(SWAG_SERVER_PREFIX)
|
||||
SWAG_SERVER_APP_PATH := $(APP_PATH)/$(SWAG_SERVER_PREFIX)
|
||||
|
||||
SWAG_SERVER_APP_TARGET_WALLET := $(SWAG_SERVER_APP_PATH)_wallet/rebar.config
|
||||
SWAG_SERVER_APP_TARGET_PAYRES := $(SWAG_SERVER_APP_PATH)_payres/rebar.config
|
||||
SWAG_SERVER_APP_TARGET_PRIVDOC := $(SWAG_SERVER_APP_PATH)_privdoc/rebar.config
|
||||
|
||||
$(SWAG_SERVER_APP_PATH)_%/rebar.config: $(SWAGGER_SCHEME_BASE_PATH)/.git
|
||||
$(SWAGGER_CODEGEN) generate \
|
||||
-i $(SWAGGER_SCHEME_API_PATH)/$*/$(SWAG_SPEC_FILE) \
|
||||
-l erlang-server \
|
||||
-o $(SWAG_SERVER_APP_PATH)_$* \
|
||||
--additional-properties \
|
||||
packageName=$(SWAG_SERVER_PREFIX)_$*
|
||||
|
||||
swag_server.generate: $(SWAG_SERVER_APP_TARGET_WALLET) $(SWAG_SERVER_APP_TARGET_PAYRES) $(SWAG_SERVER_APP_TARGET_PRIVDOC)
|
||||
|
||||
swag_server.distclean: swag_server.distclean_wallet swag_server.distclean_payres swag_server.distclean_privdoc
|
||||
|
||||
swag_server.distclean_%:
|
||||
rm -rf $(SWAG_SERVER_APP_PATH)_$*
|
||||
|
||||
swag_server.regenerate: swag_server.distclean swag_server.generate
|
||||
|
||||
# Swagger client
|
||||
|
||||
SWAG_CLIENT_PREFIX := swag_client
|
||||
SWAG_CLIENT_APP_TARGET := $(APP_PATH)/$(SWAG_CLIENT_PREFIX)
|
||||
SWAG_CLIENT_APP_PATH := $(APP_PATH)/$(SWAG_CLIENT_PREFIX)
|
||||
|
||||
SWAG_CLIENT_APP_TARGET_WALLET := $(SWAG_CLIENT_APP_PATH)_wallet/rebar.config
|
||||
SWAG_CLIENT_APP_TARGET_PAYRES := $(SWAG_CLIENT_APP_PATH)_payres/rebar.config
|
||||
SWAG_CLIENT_APP_TARGET_PRIVDOC := $(SWAG_CLIENT_APP_PATH)_privdoc/rebar.config
|
||||
|
||||
$(SWAG_CLIENT_APP_PATH)_%/rebar.config: $(SWAGGER_SCHEME_BASE_PATH)/.git
|
||||
$(SWAGGER_CODEGEN) generate \
|
||||
-i $(SWAGGER_SCHEME_API_PATH)/$*/$(SWAG_SPEC_FILE) \
|
||||
-l erlang-client \
|
||||
-o $(SWAG_CLIENT_APP_PATH)_$* \
|
||||
--additional-properties \
|
||||
packageName=$(SWAG_CLIENT_PREFIX)_$*
|
||||
|
||||
swag_client.generate: $(SWAG_CLIENT_APP_TARGET_WALLET) $(SWAG_CLIENT_APP_TARGET_PAYRES) $(SWAG_CLIENT_APP_TARGET_PRIVDOC)
|
||||
|
||||
swag_client.distclean: swag_client.distclean_wallet swag_client.distclean_payres swag_client.distclean_privdoc
|
||||
|
||||
swag_client.distclean_%:
|
||||
rm -rf $(SWAG_CLIENT_APP_PATH)_$*
|
||||
|
||||
swag_client.regenerate: swag_client.distclean swag_client.generate
|
||||
|
@ -10,7 +10,8 @@
|
||||
stdlib,
|
||||
woody,
|
||||
fistful,
|
||||
ff_withdraw
|
||||
ff_withdraw,
|
||||
wapi
|
||||
]},
|
||||
{env, []},
|
||||
{modules, []},
|
||||
|
@ -98,12 +98,12 @@ get(ID) ->
|
||||
-type event_cursor() :: non_neg_integer() | undefined.
|
||||
|
||||
-spec get_status_events(id(), event_cursor()) ->
|
||||
{ok, [ev()]} |
|
||||
{error, notfound} .
|
||||
{ok, [{integer(), machinery:timestamp(), ev()}]} |
|
||||
{error, notfound} .
|
||||
|
||||
get_status_events(ID, Cursor) ->
|
||||
do(fun () ->
|
||||
unwrap(machinery:get(?NS, ID, {Cursor, undefined, forward}, backend()))
|
||||
maps:get(history, unwrap(machinery:get(?NS, ID, {Cursor, undefined, forward}, backend())))
|
||||
end).
|
||||
|
||||
backend() ->
|
||||
|
@ -20,6 +20,7 @@
|
||||
-export_type([ctx/0]).
|
||||
|
||||
-export([new/0]).
|
||||
-export([get/2]).
|
||||
|
||||
%%
|
||||
|
||||
@ -28,3 +29,10 @@
|
||||
|
||||
new() ->
|
||||
#{}.
|
||||
|
||||
-spec get(namespace(), ctx()) ->
|
||||
{ok, md()} |
|
||||
{error, notfound}.
|
||||
|
||||
get(Ns, Ctx) ->
|
||||
ff_map:find(Ns, Ctx).
|
||||
|
@ -55,6 +55,7 @@
|
||||
-export([provider/1]).
|
||||
-export([party/1]).
|
||||
-export([class/1]).
|
||||
-export([level/1]).
|
||||
-export([contract/1]).
|
||||
-export([challenge/2]).
|
||||
-export([effective_challenge/1]).
|
||||
|
@ -102,7 +102,7 @@ get(ID) ->
|
||||
-type challenge_params() :: #{
|
||||
id := challenge_id(),
|
||||
class := ff_identity_class:challenge_class_id(),
|
||||
proofs := [ff_identity:proof()]
|
||||
proofs := [ff_identity_challenge:proof()]
|
||||
}.
|
||||
|
||||
-spec start_challenge(id(), challenge_params()) ->
|
||||
|
77
apps/wapi/elvis.config
Normal file
77
apps/wapi/elvis.config
Normal file
@ -0,0 +1,77 @@
|
||||
[
|
||||
{elvis, [
|
||||
{config, [
|
||||
#{
|
||||
dirs => [
|
||||
"apps/*/src",
|
||||
"apps/*/test"
|
||||
],
|
||||
filter => "*.erl",
|
||||
ignore => ["_thrift.erl$", "src/swag_server*", "src/swag_client*", "_SUITE.erl$"],
|
||||
rules => [
|
||||
{elvis_style, line_length, #{limit => 120, skip_comments => false}},
|
||||
{elvis_style, no_tabs},
|
||||
{elvis_style, no_trailing_whitespace},
|
||||
{elvis_style, macro_module_names},
|
||||
{elvis_style, operator_spaces, #{rules => [{right, ","}, {right, "++"}, {left, "++"}]}},
|
||||
{elvis_style, nesting_level, #{level => 4}},
|
||||
{elvis_style, god_modules, #{limit => 25}},
|
||||
{elvis_style, no_if_expression},
|
||||
{elvis_style, invalid_dynamic_call, #{ignore => [wapi_swagger_server]}},
|
||||
{elvis_style, used_ignored_variable},
|
||||
{elvis_style, no_behavior_info},
|
||||
{elvis_style, module_naming_convention, #{regex => "^[a-z]([a-z0-9]*_?)*(_SUITE)?$"}},
|
||||
{elvis_style, function_naming_convention, #{regex => "^[a-z]([a-z0-9]*_?)*$"}},
|
||||
{elvis_style, state_record_and_type},
|
||||
{elvis_style, no_spec_with_records},
|
||||
{elvis_style, dont_repeat_yourself, #{
|
||||
min_complexity => 30,
|
||||
ignore => [
|
||||
wapi_tests_SUITE
|
||||
]
|
||||
}},
|
||||
{elvis_style, no_debug_call, #{}}
|
||||
]
|
||||
},
|
||||
#{
|
||||
dirs => ["."],
|
||||
filter => "Makefile",
|
||||
ruleset => makefiles
|
||||
},
|
||||
#{
|
||||
dirs => ["."],
|
||||
filter => "elvis.config",
|
||||
ruleset => elvis_config
|
||||
},
|
||||
#{
|
||||
dirs => ["apps", "apps/*"],
|
||||
filter => "rebar.config",
|
||||
ignore => ["swag_server/*", "swag_client/*"],
|
||||
rules => [
|
||||
{elvis_style, line_length, #{limit => 120, skip_comments => false}},
|
||||
{elvis_style, no_tabs},
|
||||
{elvis_style, no_trailing_whitespace}
|
||||
]
|
||||
},
|
||||
#{
|
||||
dirs => ["."],
|
||||
filter => "rebar.config",
|
||||
rules => [
|
||||
{elvis_style, line_length, #{limit => 120, skip_comments => false}},
|
||||
{elvis_style, no_tabs},
|
||||
{elvis_style, no_trailing_whitespace}
|
||||
]
|
||||
},
|
||||
#{
|
||||
dirs => ["apps/*/src"],
|
||||
filter => "*.app.src",
|
||||
ignore => ["src/swag_server*", "src/swag_client*"],
|
||||
rules => [
|
||||
{elvis_style, line_length, #{limit => 120, skip_comments => false}},
|
||||
{elvis_style, no_tabs},
|
||||
{elvis_style, no_trailing_whitespace}
|
||||
]
|
||||
}
|
||||
]}
|
||||
]}
|
||||
].
|
129
apps/wapi/rebar.config
Normal file
129
apps/wapi/rebar.config
Normal file
@ -0,0 +1,129 @@
|
||||
%% Common project erlang options.
|
||||
{erl_opts, [
|
||||
|
||||
%% % mandatory
|
||||
%% debug_info,
|
||||
%% warnings_as_errors,
|
||||
%% warn_export_all,
|
||||
%% warn_missing_spec,
|
||||
%% warn_untyped_record,
|
||||
%% warn_export_vars,
|
||||
|
||||
%% % by default
|
||||
%% warn_unused_record,
|
||||
%% warn_bif_clash,
|
||||
%% warn_obsolete_guard,
|
||||
%% warn_unused_vars,
|
||||
%% warn_shadow_vars,
|
||||
%% warn_unused_import,
|
||||
%% warn_unused_function,
|
||||
%% warn_deprecated_function,
|
||||
|
||||
%% % at will
|
||||
%% % bin_opt_info
|
||||
%% % no_auto_import
|
||||
%% % warn_missing_spec_all
|
||||
{parse_transform, lager_transform}
|
||||
]}.
|
||||
|
||||
%% Common project dependencies.
|
||||
{deps, [
|
||||
{cowboy, "1.0.4"},
|
||||
%% {rfc3339, "0.2.2"},
|
||||
{jose, "1.7.9"},
|
||||
%% {lager, "3.6.1"},
|
||||
{base64url, "0.0.1"},
|
||||
%% {genlib,
|
||||
%% {git, "https://github.com/rbkmoney/genlib.git", {branch, "master"}}
|
||||
%% },
|
||||
%% {woody,
|
||||
%% {git, "git@github.com:rbkmoney/woody_erlang.git", {branch, "master"}}
|
||||
%% },
|
||||
%% {woody_user_identity,
|
||||
%% {git, "git@github.com:rbkmoney/woody_erlang_user_identity.git", {branch, "master"}}
|
||||
%% },
|
||||
%% {dmsl,
|
||||
%% {git, "git@github.com:rbkmoney/damsel.git", {branch, "release/erlang/master"}}
|
||||
%% },
|
||||
%% {lager_logstash_formatter,
|
||||
%% {git, "git@github.com:rbkmoney/lager_logstash_formatter.git", {branch, "master"}}
|
||||
%% },
|
||||
{cowboy_cors,
|
||||
{git, "https://github.com/danielwhite/cowboy_cors.git", {branch, "master"}}
|
||||
},
|
||||
{cowboy_access_log,
|
||||
{git, "git@github.com:rbkmoney/cowboy_access_log.git", {branch, "master"}}
|
||||
},
|
||||
{payproc_errors,
|
||||
{git, "git@github.com:rbkmoney/payproc-errors-erlang.git", {branch, "master"}}
|
||||
},
|
||||
{erl_health,
|
||||
{git, "https://github.com/rbkmoney/erlang-health.git", {branch, master}}
|
||||
}
|
||||
]}.
|
||||
|
||||
%% XRef checks
|
||||
%% {xref_checks, [
|
||||
%% undefined_function_calls,
|
||||
%% undefined_functions,
|
||||
%% deprecated_functions_calls,
|
||||
%% deprecated_functions
|
||||
%% ]}.
|
||||
% at will
|
||||
% {xref_warnings, true}.
|
||||
|
||||
%% Tests
|
||||
%% {cover_enabled, true}.
|
||||
|
||||
%% Relx configuration
|
||||
%% {relx, [
|
||||
%% {release, { capi , "0.1.0"}, [
|
||||
%% {recon , load }, % tools for introspection
|
||||
%% {runtime_tools, load }, % debugger
|
||||
%% {tools , load }, % profiler
|
||||
%% capi,
|
||||
%% sasl
|
||||
%% ]},
|
||||
%% {sys_config, "./config/sys.config"},
|
||||
%% {vm_args, "./config/vm.args"},
|
||||
%% {dev_mode, true},
|
||||
%% {include_erts, false},
|
||||
%% {extended_start_script, true}
|
||||
%% ]}.
|
||||
|
||||
%% Dialyzer static analyzing
|
||||
%% {dialyzer, [
|
||||
%% {warnings, [
|
||||
%% % mandatory
|
||||
%% unmatched_returns,
|
||||
%% error_handling,
|
||||
%% race_conditions,
|
||||
%% unknown
|
||||
%% ]},
|
||||
%% {plt_apps, all_deps}
|
||||
%% ]}.
|
||||
|
||||
%% {profiles, [
|
||||
%% {prod, [
|
||||
%% {deps, [
|
||||
%% % for introspection on production
|
||||
%% {recon, "2.3.2"}
|
||||
%% ]},
|
||||
%% {relx, [
|
||||
%% {dev_mode, false},
|
||||
%% {include_erts, true},
|
||||
%% {overlay, [
|
||||
%% {mkdir , "var/keys/capi" },
|
||||
%% {copy , "var/keys/capi/private.pem" , "var/keys/capi/private.pem" }
|
||||
%% ]}
|
||||
%% ]}
|
||||
%% ]},
|
||||
%% {test, [
|
||||
%% {cover_enabled, true},
|
||||
%% {deps, []}
|
||||
%% ]}
|
||||
%% ]}.
|
||||
|
||||
%% {pre_hooks, [
|
||||
%% {thrift, "git submodule update --init"}
|
||||
%% ]}.
|
31
apps/wapi/src/wapi.app.src
Normal file
31
apps/wapi/src/wapi.app.src
Normal file
@ -0,0 +1,31 @@
|
||||
{application, wapi , [
|
||||
{description, "Wallet API service adapter"},
|
||||
{vsn, "0.1.0"},
|
||||
{registered, []},
|
||||
{mod, {wapi , []}},
|
||||
{applications, [
|
||||
kernel,
|
||||
stdlib,
|
||||
public_key,
|
||||
lager,
|
||||
%% lager_logstash_formatter,
|
||||
genlib,
|
||||
woody,
|
||||
scoper,
|
||||
dmsl,
|
||||
swag_server_wallet,
|
||||
swag_server_payres,
|
||||
swag_server_privdoc,
|
||||
jose,
|
||||
cowboy_cors,
|
||||
cowboy_access_log,
|
||||
rfc3339,
|
||||
base64url,
|
||||
snowflake,
|
||||
woody_user_identity,
|
||||
payproc_errors,
|
||||
erl_health,
|
||||
identdocstore_proto
|
||||
]},
|
||||
{env, []}
|
||||
]}.
|
21
apps/wapi/src/wapi.erl
Normal file
21
apps/wapi/src/wapi.erl
Normal file
@ -0,0 +1,21 @@
|
||||
%% @doc Public API and application startup.
|
||||
%% @end
|
||||
|
||||
-module(wapi).
|
||||
-behaviour(application).
|
||||
|
||||
%% Application callbacks
|
||||
-export([start/2]).
|
||||
-export([stop/1]).
|
||||
|
||||
%%
|
||||
|
||||
-spec start(normal, any()) -> {ok, pid()} | {error, any()}.
|
||||
|
||||
start(_StartType, _StartArgs) ->
|
||||
wapi_sup:start_link().
|
||||
|
||||
-spec stop(any()) -> ok.
|
||||
|
||||
stop(_State) ->
|
||||
ok.
|
241
apps/wapi/src/wapi_acl.erl
Normal file
241
apps/wapi/src/wapi_acl.erl
Normal file
@ -0,0 +1,241 @@
|
||||
-module(wapi_acl).
|
||||
|
||||
%%
|
||||
|
||||
-opaque t() :: [{{priority(), scope()}, [permission()]}].
|
||||
|
||||
-type priority() :: integer().
|
||||
-type scope() :: [resource() | {resource(), resource_id()}, ...].
|
||||
-type resource() :: atom().
|
||||
-type resource_id() :: binary().
|
||||
-type permission() :: read | write.
|
||||
|
||||
-export_type([t/0]).
|
||||
-export_type([scope/0]).
|
||||
-export_type([permission/0]).
|
||||
|
||||
-export([new/0]).
|
||||
-export([to_list/1]).
|
||||
-export([from_list/1]).
|
||||
-export([insert_scope/3]).
|
||||
-export([remove_scope/3]).
|
||||
|
||||
-export([match/2]).
|
||||
|
||||
-export([decode/1]).
|
||||
-export([encode/1]).
|
||||
|
||||
%%
|
||||
|
||||
-spec new() ->
|
||||
t().
|
||||
|
||||
new() ->
|
||||
[].
|
||||
|
||||
-spec to_list(t()) ->
|
||||
[{scope(), permission()}].
|
||||
|
||||
to_list(ACL) ->
|
||||
[{S, P} || {{_, S}, P} <- ACL].
|
||||
|
||||
-spec from_list([{scope(), permission()}]) ->
|
||||
t().
|
||||
|
||||
from_list(L) ->
|
||||
lists:foldl(fun ({S, P}, ACL) -> insert_scope(S, P, ACL) end, new(), L).
|
||||
|
||||
-spec insert_scope(scope(), permission(), t()) ->
|
||||
t().
|
||||
|
||||
insert_scope(Scope, Permission, ACL) ->
|
||||
Priority = compute_priority(Scope, Permission),
|
||||
insert({{Priority, Scope}, [Permission]}, ACL).
|
||||
|
||||
insert({PS, _} = V, [{PS0, _} = V0 | Vs]) when PS < PS0 ->
|
||||
[V0 | insert(V, Vs)];
|
||||
insert({PS, Perms}, [{PS, Perms0} | Vs]) ->
|
||||
% NOTE squashing permissions of entries with the same scope
|
||||
[{PS, lists:usort(Perms ++ Perms0)} | Vs];
|
||||
insert({PS, _} = V, [{PS0, _} | _] = Vs) when PS > PS0 ->
|
||||
[V | Vs];
|
||||
insert(V, []) ->
|
||||
[V].
|
||||
|
||||
-spec remove_scope(scope(), permission(), t()) ->
|
||||
t().
|
||||
|
||||
remove_scope(Scope, Permission, ACL) ->
|
||||
Priority = compute_priority(Scope, Permission),
|
||||
remove({{Priority, Scope}, [Permission]}, ACL).
|
||||
|
||||
remove(V, [V | Vs]) ->
|
||||
Vs;
|
||||
remove({PS, Perms}, [{PS, Perms0} | Vs]) ->
|
||||
[{PS, Perms0 -- Perms} | Vs];
|
||||
remove(V, [V0 | Vs]) ->
|
||||
[V0 | remove(V, Vs)];
|
||||
remove(_, []) ->
|
||||
[].
|
||||
|
||||
compute_priority(Scope, Permission) ->
|
||||
% NOTE
|
||||
% Scope priority depends on the following attributes, in the order of decreasing
|
||||
% importance:
|
||||
% 1. Depth, deeper is more important
|
||||
% 2. Scope element specificity, element marked with an ID is more important
|
||||
compute_scope_priority(Scope) + compute_permission_priority(Permission).
|
||||
|
||||
compute_scope_priority(Scope) when length(Scope) > 0 ->
|
||||
compute_scope_priority(Scope, get_resource_hierarchy(), 0);
|
||||
compute_scope_priority(Scope) ->
|
||||
error({badarg, {scope, Scope}}).
|
||||
|
||||
compute_scope_priority([{Resource, _ID} | Rest], H, P) ->
|
||||
compute_scope_priority(Rest, delve(Resource, H), P * 10 + 2);
|
||||
compute_scope_priority([Resource | Rest], H, P) ->
|
||||
compute_scope_priority(Rest, delve(Resource, H), P * 10 + 1);
|
||||
compute_scope_priority([], _, P) ->
|
||||
P * 10.
|
||||
|
||||
compute_permission_priority(read) ->
|
||||
0;
|
||||
compute_permission_priority(write) ->
|
||||
0;
|
||||
compute_permission_priority(V) ->
|
||||
error({badarg, {permission, V}}).
|
||||
|
||||
%%
|
||||
|
||||
-spec match(scope(), t()) ->
|
||||
[permission()].
|
||||
|
||||
match(Scope, ACL) when length(Scope) > 0 ->
|
||||
match_rules(Scope, ACL);
|
||||
match(Scope, _) ->
|
||||
error({badarg, {scope, Scope}}).
|
||||
|
||||
match_rules(Scope, [{{_Priority, ScopePrefix}, Permissions} | Rest]) ->
|
||||
% NOTE
|
||||
% The `Scope` matches iff `ScopePrefix` is scope prefix of the `Scope`.
|
||||
% An element of a scope matches corresponding element of a scope prefix
|
||||
% according to the following rules:
|
||||
% 1. Scope prefix element marked with resource and ID matches exactly the same
|
||||
% scope element.
|
||||
% 2. Scope prefix element marked with only resource matches any scope element
|
||||
% marked with the same resource.
|
||||
case match_scope(Scope, ScopePrefix) of
|
||||
true ->
|
||||
Permissions;
|
||||
false ->
|
||||
match_rules(Scope, Rest)
|
||||
end;
|
||||
match_rules(_Scope, []) ->
|
||||
[].
|
||||
|
||||
match_scope([V | Ss], [V | Ss0]) ->
|
||||
match_scope(Ss, Ss0);
|
||||
match_scope([{V, _ID} | Ss], [V | Ss0]) ->
|
||||
match_scope(Ss, Ss0);
|
||||
match_scope(_, []) ->
|
||||
true;
|
||||
match_scope(_, _) ->
|
||||
false.
|
||||
|
||||
%%
|
||||
|
||||
-spec decode([binary()]) ->
|
||||
t().
|
||||
|
||||
decode(V) ->
|
||||
lists:foldl(fun decode_entry/2, new(), V).
|
||||
|
||||
decode_entry(V, ACL) ->
|
||||
case binary:split(V, <<":">>, [global]) of
|
||||
[V1, V2] ->
|
||||
Scope = decode_scope(V1),
|
||||
Permission = decode_permission(V2),
|
||||
insert_scope(Scope, Permission, ACL);
|
||||
_ ->
|
||||
error({badarg, {role, V}})
|
||||
end.
|
||||
|
||||
decode_scope(V) ->
|
||||
Hierarchy = get_resource_hierarchy(),
|
||||
decode_scope_frags(binary:split(V, <<".">>, [global]), Hierarchy).
|
||||
|
||||
decode_scope_frags([V1, V2 | Vs], H) ->
|
||||
{Resource, H1} = decode_scope_frag_resource(V1, V2, H),
|
||||
[Resource | decode_scope_frags(Vs, H1)];
|
||||
decode_scope_frags([V], H) ->
|
||||
decode_scope_frags([V, <<"*">>], H);
|
||||
decode_scope_frags([], _) ->
|
||||
[].
|
||||
|
||||
decode_scope_frag_resource(V, <<"*">>, H) ->
|
||||
R = decode_resource(V),
|
||||
{R, delve(R, H)};
|
||||
decode_scope_frag_resource(V, ID, H) ->
|
||||
R = decode_resource(V),
|
||||
{{R, ID}, delve(R, H)}.
|
||||
|
||||
decode_resource(V) ->
|
||||
try binary_to_existing_atom(V, utf8) catch
|
||||
error:badarg ->
|
||||
error({badarg, {resource, V}})
|
||||
end.
|
||||
|
||||
decode_permission(<<"read">>) ->
|
||||
read;
|
||||
decode_permission(<<"write">>) ->
|
||||
write;
|
||||
decode_permission(V) ->
|
||||
error({badarg, {permission, V}}).
|
||||
|
||||
%%
|
||||
|
||||
-spec encode(t()) ->
|
||||
[binary()].
|
||||
|
||||
encode(ACL) ->
|
||||
lists:flatmap(fun encode_entry/1, ACL).
|
||||
|
||||
encode_entry({{_Priority, Scope}, Permissions}) ->
|
||||
S = encode_scope(Scope),
|
||||
[begin P = encode_permission(Permission), <<S/binary, ":", P/binary>> end
|
||||
|| Permission <- Permissions].
|
||||
|
||||
encode_scope(Scope) ->
|
||||
Hierarchy = get_resource_hierarchy(),
|
||||
genlib_string:join($., encode_scope_frags(Scope, Hierarchy)).
|
||||
|
||||
encode_scope_frags([{Resource, ID} | Rest], H) ->
|
||||
[encode_resource(Resource), ID | encode_scope_frags(Rest, delve(Resource, H))];
|
||||
encode_scope_frags([Resource], H) ->
|
||||
_ = delve(Resource, H),
|
||||
[encode_resource(Resource)];
|
||||
encode_scope_frags([Resource | Rest], H) ->
|
||||
[encode_resource(Resource), <<"*">> | encode_scope_frags(Rest, delve(Resource, H))];
|
||||
encode_scope_frags([], _) ->
|
||||
[].
|
||||
|
||||
encode_resource(V) ->
|
||||
atom_to_binary(V, utf8).
|
||||
|
||||
encode_permission(read) ->
|
||||
<<"read">>;
|
||||
encode_permission(write) ->
|
||||
<<"write">>.
|
||||
|
||||
%%
|
||||
|
||||
get_resource_hierarchy() ->
|
||||
wapi_auth:get_resource_hierarchy().
|
||||
|
||||
delve(Resource, Hierarchy) ->
|
||||
case maps:find(Resource, Hierarchy) of
|
||||
{ok, Sub} ->
|
||||
Sub;
|
||||
error ->
|
||||
error({badarg, {resource, Resource}})
|
||||
end.
|
203
apps/wapi/src/wapi_auth.erl
Normal file
203
apps/wapi/src/wapi_auth.erl
Normal file
@ -0,0 +1,203 @@
|
||||
-module(wapi_auth).
|
||||
|
||||
-export([authorize_api_key/3]).
|
||||
-export([authorize_operation/3]).
|
||||
-export([issue_access_token/2]).
|
||||
-export([issue_access_token/3]).
|
||||
|
||||
-export([get_subject_id/1]).
|
||||
-export([get_claims/1]).
|
||||
-export([get_claim/2]).
|
||||
-export([get_claim/3]).
|
||||
-export([get_consumer/1]).
|
||||
|
||||
-export([get_resource_hierarchy/0]).
|
||||
|
||||
-type context () :: wapi_authorizer_jwt:t().
|
||||
-type claims () :: wapi_authorizer_jwt:claims().
|
||||
-type consumer() :: client | merchant | provider.
|
||||
|
||||
-export_type([context /0]).
|
||||
-export_type([claims /0]).
|
||||
-export_type([consumer/0]).
|
||||
|
||||
-type operation_id() :: wapi_handler:operation_id().
|
||||
|
||||
-type api_key() ::
|
||||
swag_wallet_server:api_key() |
|
||||
swag_payres_server:api_key() |
|
||||
swag_privdoc_server:api_key().
|
||||
|
||||
-type handler_opts() :: wapi_handler:handler_opts().
|
||||
|
||||
-spec authorize_api_key(operation_id(), api_key(), handler_opts()) ->
|
||||
{true, context()}. %% | false.
|
||||
|
||||
authorize_api_key(_OperationID, _ApiKey, _Opts) ->
|
||||
%% case parse_api_key(ApiKey) of
|
||||
%% {ok, {Type, Credentials}} ->
|
||||
%% case do_authorize_api_key(OperationID, Type, Credentials) of
|
||||
%% {ok, Context} ->
|
||||
%% {true, Context};
|
||||
%% {error, Error} ->
|
||||
%% _ = log_auth_error(OperationID, Error),
|
||||
%% false
|
||||
%% end;
|
||||
%% {error, Error} ->
|
||||
%% _ = log_auth_error(OperationID, Error),
|
||||
%% false
|
||||
%% end,
|
||||
Subject = {<<"notimplemented">>, wapi_acl:new()},
|
||||
Claims = #{},
|
||||
{true, {Subject, Claims}}.
|
||||
|
||||
%% log_auth_error(OperationID, Error) ->
|
||||
%% lager:info("API Key authorization failed for ~p due to ~p", [OperationID, Error]).
|
||||
|
||||
%% -spec parse_api_key(ApiKey :: api_key()) ->
|
||||
%% {ok, {bearer, Credentials :: binary()}} | {error, Reason :: atom()}.
|
||||
|
||||
%% parse_api_key(ApiKey) ->
|
||||
%% case ApiKey of
|
||||
%% <<"Bearer ", Credentials/binary>> ->
|
||||
%% {ok, {bearer, Credentials}};
|
||||
%% _ ->
|
||||
%% {error, unsupported_auth_scheme}
|
||||
%% end.
|
||||
|
||||
%% -spec do_authorize_api_key(
|
||||
%% OperationID :: operation_id(),
|
||||
%% Type :: atom(),
|
||||
%% Credentials :: binary()
|
||||
%% ) ->
|
||||
%% {ok, Context :: context()} | {error, Reason :: atom()}.
|
||||
|
||||
%% do_authorize_api_key(_OperationID, bearer, Token) ->
|
||||
%% % NOTE
|
||||
%% % We are knowingly delegating actual request authorization to the logic handler
|
||||
%% % so we could gather more data to perform fine-grained access control.
|
||||
%% wapi_authorizer_jwt:verify(Token).
|
||||
|
||||
%%
|
||||
|
||||
% TODO
|
||||
% We need shared type here, exported somewhere in swagger app
|
||||
-type request_data() :: #{atom() | binary() => term()}.
|
||||
|
||||
-spec authorize_operation(
|
||||
OperationID :: operation_id(),
|
||||
Req :: request_data(),
|
||||
Auth :: wapi_authorizer_jwt:t()
|
||||
) ->
|
||||
ok | {error, unauthorized}.
|
||||
|
||||
%% TODO
|
||||
authorize_operation(_OperationID, _Req, _) ->
|
||||
ok.
|
||||
%% authorize_operation(OperationID, Req, {{_SubjectID, ACL}, _}) ->
|
||||
%% Access = get_operation_access(OperationID, Req),
|
||||
%% _ = case lists:all(
|
||||
%% fun ({Scope, Permission}) ->
|
||||
%% lists:member(Permission, wapi_acl:match(Scope, ACL))
|
||||
%% end,
|
||||
%% Access
|
||||
%% ) of
|
||||
%% true ->
|
||||
%% ok;
|
||||
%% false ->
|
||||
%% {error, unauthorized}
|
||||
%% end.
|
||||
|
||||
%%
|
||||
|
||||
-type token_spec() ::
|
||||
{destinations, DestinationID :: binary()}.
|
||||
|
||||
-spec issue_access_token(wapi_handler_utils:party_id(), token_spec()) ->
|
||||
wapi_authorizer_jwt:token().
|
||||
issue_access_token(PartyID, TokenSpec) ->
|
||||
issue_access_token(PartyID, TokenSpec, unlimited).
|
||||
|
||||
-type expiration() ::
|
||||
{deadline, machinery:timestamp() | pos_integer()} |
|
||||
{lifetime, Seconds :: pos_integer()} |
|
||||
unlimited .
|
||||
|
||||
-spec issue_access_token(wapi_handler_utils:party_id(), token_spec(), expiration()) ->
|
||||
wapi_authorizer_jwt:token().
|
||||
issue_access_token(PartyID, TokenSpec, Expiration0) ->
|
||||
Expiration = get_expiration(Expiration0),
|
||||
{Claims, ACL} = resolve_token_spec(TokenSpec),
|
||||
wapi_utils:unwrap(wapi_authorizer_jwt:issue({{PartyID, wapi_acl:from_list(ACL)}, Claims}, Expiration)).
|
||||
|
||||
-spec get_expiration(expiration()) ->
|
||||
wapi_authorizer_jwt:expiration().
|
||||
get_expiration(Exp = unlimited) ->
|
||||
Exp;
|
||||
get_expiration({deadline, {DateTime, Usec}}) ->
|
||||
{deadline, genlib_time:to_unixtime(DateTime) + Usec div 1000000};
|
||||
get_expiration(Exp = {deadline, _Sec}) ->
|
||||
Exp;
|
||||
get_expiration(Exp = {lifetime, _Sec}) ->
|
||||
Exp.
|
||||
|
||||
-type acl() :: [{wapi_acl:scope(), wapi_acl:permission()}].
|
||||
|
||||
-spec resolve_token_spec(token_spec()) ->
|
||||
{claims(), acl()}.
|
||||
resolve_token_spec({destinations, DestinationId}) ->
|
||||
Claims = #{},
|
||||
ACL = [
|
||||
{[party, {destinations, DestinationId}], read},
|
||||
{[party, {destinations, DestinationId}], write}
|
||||
],
|
||||
{Claims, ACL}.
|
||||
|
||||
-spec get_subject_id(context()) -> binary().
|
||||
|
||||
get_subject_id({{SubjectID, _ACL}, _}) ->
|
||||
SubjectID.
|
||||
|
||||
-spec get_claims(context()) -> claims().
|
||||
|
||||
get_claims({_Subject, Claims}) ->
|
||||
Claims.
|
||||
|
||||
-spec get_claim(binary(), context()) -> term().
|
||||
|
||||
get_claim(ClaimName, {_Subject, Claims}) ->
|
||||
maps:get(ClaimName, Claims).
|
||||
|
||||
-spec get_claim(binary(), context(), term()) -> term().
|
||||
|
||||
get_claim(ClaimName, {_Subject, Claims}, Default) ->
|
||||
maps:get(ClaimName, Claims, Default).
|
||||
|
||||
%%
|
||||
|
||||
%% TODO update for the wallet swag
|
||||
%% -spec get_operation_access(operation_id(), request_data()) ->
|
||||
%% [{wapi_acl:scope(), wapi_acl:permission()}].
|
||||
|
||||
%% get_operation_access('StoreBankCard' , _) ->
|
||||
%% [{[payment_resources], write}].
|
||||
|
||||
-spec get_resource_hierarchy() -> #{atom() => map()}.
|
||||
|
||||
%% TODO add some sence in here
|
||||
get_resource_hierarchy() ->
|
||||
#{
|
||||
party => #{
|
||||
wallets => #{},
|
||||
destinations => #{}
|
||||
}
|
||||
}.
|
||||
|
||||
-spec get_consumer(claims()) ->
|
||||
consumer().
|
||||
get_consumer(Claims) ->
|
||||
case maps:get(<<"cons">>, Claims, <<"merchant">>) of
|
||||
<<"merchant">> -> merchant;
|
||||
<<"client" >> -> client;
|
||||
<<"provider">> -> provider
|
||||
end.
|
397
apps/wapi/src/wapi_authorizer_jwt.erl
Normal file
397
apps/wapi/src/wapi_authorizer_jwt.erl
Normal file
@ -0,0 +1,397 @@
|
||||
-module(wapi_authorizer_jwt).
|
||||
|
||||
%%
|
||||
|
||||
-export([get_child_spec/1]).
|
||||
-export([init/1]).
|
||||
|
||||
-export([store_key/2]).
|
||||
% TODO
|
||||
% Extend interface to support proper keystore manipulation
|
||||
|
||||
-export([issue/2]).
|
||||
-export([verify/1]).
|
||||
|
||||
%%
|
||||
|
||||
-include_lib("jose/include/jose_jwk.hrl").
|
||||
-include_lib("jose/include/jose_jwt.hrl").
|
||||
|
||||
-type keyname() :: term().
|
||||
-type kid() :: binary().
|
||||
-type key() :: #jose_jwk{}.
|
||||
-type token() :: binary().
|
||||
-type claims() :: #{binary() => term()}.
|
||||
-type subject() :: {subject_id(), wapi_acl:t()}.
|
||||
-type subject_id() :: binary().
|
||||
-type t() :: {subject(), claims()}.
|
||||
-type expiration() ::
|
||||
{lifetime, Seconds :: pos_integer()} |
|
||||
{deadline, UnixTs :: pos_integer()} |
|
||||
unlimited.
|
||||
|
||||
-export_type([t/0]).
|
||||
-export_type([subject/0]).
|
||||
-export_type([claims/0]).
|
||||
-export_type([token/0]).
|
||||
-export_type([expiration/0]).
|
||||
|
||||
%%
|
||||
|
||||
-type options() :: #{
|
||||
%% The set of keys used to sign issued tokens and verify signatures on such
|
||||
%% tokens.
|
||||
keyset => keyset(),
|
||||
%% The name of a key used exclusively to sign any issued token.
|
||||
%% If not set any token issue is destined to fail.
|
||||
signee => keyname()
|
||||
}.
|
||||
|
||||
-type keyset() :: #{
|
||||
keyname() => keysource()
|
||||
}.
|
||||
|
||||
-type keysource() ::
|
||||
{pem_file, file:filename()}.
|
||||
|
||||
-spec get_child_spec(options()) ->
|
||||
supervisor:child_spec() | no_return().
|
||||
|
||||
get_child_spec(Options) ->
|
||||
#{
|
||||
id => ?MODULE,
|
||||
start => {supervisor, start_link, [?MODULE, parse_options(Options)]},
|
||||
type => supervisor
|
||||
}.
|
||||
|
||||
parse_options(Options) ->
|
||||
Keyset = maps:get(keyset, Options, #{}),
|
||||
_ = is_map(Keyset) orelse exit({invalid_option, keyset, Keyset}),
|
||||
_ = genlib_map:foreach(
|
||||
fun (K, V) ->
|
||||
is_keysource(V) orelse exit({invalid_option, K, V})
|
||||
end,
|
||||
Keyset
|
||||
),
|
||||
Signee = maps:find(signee, Options),
|
||||
{Keyset, Signee}.
|
||||
|
||||
is_keysource({pem_file, Fn}) ->
|
||||
is_list(Fn) orelse is_binary(Fn);
|
||||
is_keysource(_) ->
|
||||
false.
|
||||
|
||||
%%
|
||||
|
||||
-spec init({keyset(), {ok, keyname()} | error}) ->
|
||||
{ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}.
|
||||
|
||||
init({Keyset, Signee}) ->
|
||||
ok = create_table(),
|
||||
KeyInfos = maps:map(fun ensure_store_key/2, Keyset),
|
||||
ok = select_signee(Signee, KeyInfos),
|
||||
{ok, {#{}, []}}.
|
||||
|
||||
ensure_store_key(Keyname, Source) ->
|
||||
case store_key(Keyname, Source) of
|
||||
{ok, KeyInfo} ->
|
||||
KeyInfo;
|
||||
{error, Reason} ->
|
||||
_ = lager:error("Error importing key ~p: ~p", [Keyname, Reason]),
|
||||
exit({import_error, Keyname, Source, Reason})
|
||||
end.
|
||||
|
||||
select_signee({ok, Keyname}, KeyInfos) ->
|
||||
case maps:find(Keyname, KeyInfos) of
|
||||
{ok, #{sign := true}} ->
|
||||
set_signee(Keyname);
|
||||
{ok, KeyInfo} ->
|
||||
_ = lager:error("Error setting signee: signing with ~p is not allowed", [Keyname]),
|
||||
exit({invalid_signee, Keyname, KeyInfo});
|
||||
error ->
|
||||
_ = lager:error("Error setting signee: no key named ~p", [Keyname]),
|
||||
exit({nonexstent_signee, Keyname})
|
||||
end;
|
||||
select_signee(error, _KeyInfos) ->
|
||||
ok.
|
||||
|
||||
%%
|
||||
|
||||
-type keyinfo() :: #{
|
||||
kid => kid(),
|
||||
sign => boolean(),
|
||||
verify => boolean()
|
||||
}.
|
||||
|
||||
-spec store_key(keyname(), {pem_file, file:filename()}) ->
|
||||
{ok, keyinfo()} | {error, file:posix() | {unknown_key, _}}.
|
||||
|
||||
store_key(Keyname, {pem_file, Filename}) ->
|
||||
store_key(Keyname, {pem_file, Filename}, #{
|
||||
kid => fun derive_kid_from_public_key_pem_entry/1
|
||||
}).
|
||||
|
||||
derive_kid_from_public_key_pem_entry(JWK) ->
|
||||
JWKPublic = jose_jwk:to_public(JWK),
|
||||
{_Module, PublicKey} = JWKPublic#jose_jwk.kty,
|
||||
{_PemEntry, Data, _} = public_key:pem_entry_encode('SubjectPublicKeyInfo', PublicKey),
|
||||
base64url:encode(crypto:hash(sha256, Data)).
|
||||
|
||||
-type store_opts() :: #{
|
||||
kid => fun ((key()) -> kid())
|
||||
}.
|
||||
|
||||
-spec store_key(keyname(), {pem_file, file:filename()}, store_opts()) ->
|
||||
ok | {error, file:posix() | {unknown_key, _}}.
|
||||
|
||||
store_key(Keyname, {pem_file, Filename}, Opts) ->
|
||||
case jose_jwk:from_pem_file(Filename) of
|
||||
JWK = #jose_jwk{} ->
|
||||
Key = construct_key(derive_kid(JWK, Opts), JWK),
|
||||
ok = insert_key(Keyname, Key),
|
||||
{ok, get_key_info(Key)};
|
||||
Error = {error, _} ->
|
||||
Error
|
||||
end.
|
||||
|
||||
get_key_info(#{kid := KID, signer := Signer, verifier := Verifier}) ->
|
||||
#{
|
||||
kid => KID,
|
||||
sign => Signer /= undefined,
|
||||
verify => Verifier /= undefined
|
||||
}.
|
||||
|
||||
derive_kid(JWK, #{kid := DeriveFun}) when is_function(DeriveFun, 1) ->
|
||||
DeriveFun(JWK).
|
||||
|
||||
construct_key(KID, JWK) ->
|
||||
#{
|
||||
jwk => JWK,
|
||||
kid => KID,
|
||||
signer => try jose_jwk:signer(JWK) catch error:_ -> undefined end,
|
||||
verifier => try jose_jwk:verifier(JWK) catch error:_ -> undefined end
|
||||
}.
|
||||
|
||||
%%
|
||||
|
||||
-spec issue(t(), expiration()) ->
|
||||
{ok, token()} |
|
||||
{error,
|
||||
nonexistent_signee
|
||||
}.
|
||||
|
||||
issue(Auth, Expiration) ->
|
||||
case get_signee_key() of
|
||||
Key = #{} ->
|
||||
Claims = construct_final_claims(Auth, Expiration),
|
||||
sign(Key, Claims);
|
||||
undefined ->
|
||||
{error, nonexistent_signee}
|
||||
end.
|
||||
|
||||
construct_final_claims({{Subject, ACL}, Claims}, Expiration) ->
|
||||
maps:merge(
|
||||
Claims#{
|
||||
<<"jti">> => unique_id(),
|
||||
<<"sub">> => Subject,
|
||||
<<"exp">> => get_expires_at(Expiration)
|
||||
},
|
||||
encode_roles(wapi_acl:encode(ACL))
|
||||
).
|
||||
|
||||
get_expires_at({lifetime, Lt}) ->
|
||||
genlib_time:unow() + Lt;
|
||||
get_expires_at({deadline, Dl}) ->
|
||||
Dl;
|
||||
get_expires_at(unlimited) ->
|
||||
0.
|
||||
|
||||
unique_id() ->
|
||||
<<ID:64>> = snowflake:new(),
|
||||
genlib_format:format_int_base(ID, 62).
|
||||
|
||||
sign(#{kid := KID, jwk := JWK, signer := #{} = JWS}, Claims) ->
|
||||
JWT = jose_jwt:sign(JWK, JWS#{<<"kid">> => KID}, Claims),
|
||||
{_Modules, Token} = jose_jws:compact(JWT),
|
||||
{ok, Token}.
|
||||
|
||||
%%
|
||||
|
||||
-spec verify(token()) ->
|
||||
{ok, t()} |
|
||||
{error,
|
||||
{invalid_token,
|
||||
badarg |
|
||||
{badarg, term()} |
|
||||
{missing, atom()} |
|
||||
expired |
|
||||
{malformed_acl, term()}
|
||||
} |
|
||||
{nonexistent_key, kid()} |
|
||||
invalid_operation |
|
||||
invalid_signature
|
||||
}.
|
||||
|
||||
verify(Token) ->
|
||||
try
|
||||
{_, ExpandedToken} = jose_jws:expand(Token),
|
||||
#{<<"protected">> := ProtectedHeader} = ExpandedToken,
|
||||
Header = wapi_utils:base64url_to_map(ProtectedHeader),
|
||||
Alg = get_alg(Header),
|
||||
KID = get_kid(Header),
|
||||
verify(KID, Alg, ExpandedToken)
|
||||
catch
|
||||
%% from get_alg and get_kid
|
||||
throw:Reason ->
|
||||
{error, Reason};
|
||||
%% TODO we're losing error information here, e.g. stacktrace
|
||||
error:badarg = Reason ->
|
||||
{error, {invalid_token, Reason}};
|
||||
error:{badarg, _} = Reason ->
|
||||
{error, {invalid_token, Reason}};
|
||||
error:Reason ->
|
||||
{error, {invalid_token, Reason}}
|
||||
end.
|
||||
|
||||
verify(KID, Alg, ExpandedToken) ->
|
||||
case get_key_by_kid(KID) of
|
||||
#{jwk := JWK, verifier := Algs} ->
|
||||
_ = lists:member(Alg, Algs) orelse throw(invalid_operation),
|
||||
verify(JWK, ExpandedToken);
|
||||
undefined ->
|
||||
{error, {nonexistent_key, KID}}
|
||||
end.
|
||||
|
||||
verify(JWK, ExpandedToken) ->
|
||||
case jose_jwt:verify(JWK, ExpandedToken) of
|
||||
{true, #jose_jwt{fields = Claims}, _JWS} ->
|
||||
{#{subject_id := SubjectID}, Claims1} = validate_claims(Claims),
|
||||
get_result(SubjectID, decode_roles(Claims1));
|
||||
{false, _JWT, _JWS} ->
|
||||
{error, invalid_signature}
|
||||
end.
|
||||
|
||||
validate_claims(Claims) ->
|
||||
validate_claims(Claims, get_validators(), #{}).
|
||||
|
||||
validate_claims(Claims, [{Name, Claim, Validator} | Rest], Acc) ->
|
||||
V = Validator(Name, maps:get(Claim, Claims, undefined)),
|
||||
validate_claims(maps:without([Claim], Claims), Rest, Acc#{Name => V});
|
||||
validate_claims(Claims, [], Acc) ->
|
||||
{Acc, Claims}.
|
||||
|
||||
get_result(SubjectID, {Roles, Claims}) ->
|
||||
try
|
||||
Subject = {SubjectID, wapi_acl:decode(Roles)},
|
||||
{ok, {Subject, Claims}}
|
||||
catch
|
||||
error:{badarg, _} = Reason ->
|
||||
throw({invalid_token, {malformed_acl, Reason}})
|
||||
end.
|
||||
|
||||
get_kid(#{<<"kid">> := KID}) when is_binary(KID) ->
|
||||
KID;
|
||||
get_kid(#{}) ->
|
||||
throw({invalid_token, {missing, kid}}).
|
||||
|
||||
get_alg(#{<<"alg">> := Alg}) when is_binary(Alg) ->
|
||||
Alg;
|
||||
get_alg(#{}) ->
|
||||
throw({invalid_token, {missing, alg}}).
|
||||
|
||||
%%
|
||||
|
||||
get_validators() ->
|
||||
[
|
||||
{token_id , <<"jti">> , fun check_presence/2},
|
||||
{subject_id , <<"sub">> , fun check_presence/2},
|
||||
{expires_at , <<"exp">> , fun check_expiration/2}
|
||||
].
|
||||
|
||||
check_presence(_, V) when is_binary(V) ->
|
||||
V;
|
||||
check_presence(C, undefined) ->
|
||||
throw({invalid_token, {missing, C}}).
|
||||
|
||||
check_expiration(_, Exp = 0) ->
|
||||
Exp;
|
||||
check_expiration(_, Exp) when is_integer(Exp) ->
|
||||
case genlib_time:unow() of
|
||||
Now when Exp > Now ->
|
||||
Exp;
|
||||
_ ->
|
||||
throw({invalid_token, expired})
|
||||
end;
|
||||
check_expiration(C, undefined) ->
|
||||
throw({invalid_token, {missing, C}});
|
||||
check_expiration(C, V) ->
|
||||
throw({invalid_token, {badarg, {C, V}}}).
|
||||
|
||||
%%
|
||||
|
||||
encode_roles(Roles) ->
|
||||
#{
|
||||
<<"resource_access">> => #{
|
||||
<<"wallet-api">> => #{
|
||||
<<"roles">> => Roles
|
||||
}
|
||||
}
|
||||
}.
|
||||
|
||||
decode_roles(Claims = #{
|
||||
<<"resource_access">> := #{
|
||||
<<"wallet-api">> := #{
|
||||
<<"roles">> := Roles
|
||||
}
|
||||
}
|
||||
}) when is_list(Roles) ->
|
||||
{Roles, maps:remove(<<"resource_access">>, Claims)};
|
||||
decode_roles(_) ->
|
||||
throw({invalid_token, {missing, acl}}).
|
||||
|
||||
%%
|
||||
|
||||
insert_key(Keyname, Key = #{kid := KID}) ->
|
||||
insert_values(#{
|
||||
{keyname, Keyname} => Key,
|
||||
{kid, KID} => Key
|
||||
}).
|
||||
|
||||
get_key_by_name(Keyname) ->
|
||||
lookup_value({keyname, Keyname}).
|
||||
|
||||
get_key_by_kid(KID) ->
|
||||
lookup_value({kid, KID}).
|
||||
|
||||
set_signee(Keyname) ->
|
||||
insert_values(#{
|
||||
signee => {keyname, Keyname}
|
||||
}).
|
||||
|
||||
get_signee_key() ->
|
||||
case lookup_value(signee) of
|
||||
{keyname, Keyname} ->
|
||||
get_key_by_name(Keyname);
|
||||
undefined ->
|
||||
undefined
|
||||
end.
|
||||
|
||||
%%
|
||||
|
||||
-define(TABLE, ?MODULE).
|
||||
|
||||
create_table() ->
|
||||
_ = ets:new(?TABLE, [set, public, named_table, {read_concurrency, true}]),
|
||||
ok.
|
||||
|
||||
insert_values(Values) ->
|
||||
true = ets:insert(?TABLE, maps:to_list(Values)),
|
||||
ok.
|
||||
|
||||
lookup_value(Key) ->
|
||||
case ets:lookup(?TABLE, Key) of
|
||||
[{Key, Value}] ->
|
||||
Value;
|
||||
[] ->
|
||||
undefined
|
||||
end.
|
35
apps/wapi/src/wapi_cors_policy.erl
Normal file
35
apps/wapi/src/wapi_cors_policy.erl
Normal file
@ -0,0 +1,35 @@
|
||||
-module(wapi_cors_policy).
|
||||
-behaviour(cowboy_cors_policy).
|
||||
|
||||
-export([policy_init/1]).
|
||||
-export([allowed_origins/2]).
|
||||
-export([allowed_headers/2]).
|
||||
-export([allowed_methods/2]).
|
||||
|
||||
-spec policy_init(cowboy_req:req()) -> {ok, cowboy_req:req(), any()}.
|
||||
|
||||
policy_init(Req) ->
|
||||
{ok, Req, undefined_state}.
|
||||
|
||||
-spec allowed_origins(cowboy_req:req(), any()) -> {'*', cowboy_req:req(), any()}.
|
||||
|
||||
allowed_origins(Req, State) ->
|
||||
{'*', Req, State}.
|
||||
|
||||
-spec allowed_headers(cowboy_req:req(), any()) -> {[binary()], cowboy_req:req(), any()}.
|
||||
|
||||
allowed_headers(Req, State) ->
|
||||
{[
|
||||
<<"access-control-allow-headers">>,
|
||||
<<"origin">>,
|
||||
<<"x-requested-with">>,
|
||||
<<"content-type">>,
|
||||
<<"accept">>,
|
||||
<<"authorization">>,
|
||||
<<"x-request-id">>
|
||||
], Req, State}.
|
||||
|
||||
-spec allowed_methods(cowboy_req:req(), any()) -> {[binary()], cowboy_req:req(), any()}.
|
||||
|
||||
allowed_methods(Req, State) ->
|
||||
{[<<"GET">>, <<"POST">>, <<"PUT">>, <<"DELETE">>, <<"OPTIONS">>], Req, State}.
|
108
apps/wapi/src/wapi_handler.erl
Normal file
108
apps/wapi/src/wapi_handler.erl
Normal file
@ -0,0 +1,108 @@
|
||||
-module(wapi_handler).
|
||||
|
||||
%% API
|
||||
-export([handle_request/5]).
|
||||
-export([throw_result/1]).
|
||||
|
||||
%% Behaviour definition
|
||||
|
||||
-type operation_id() ::
|
||||
swag_server_payres:operation_id() |
|
||||
swag_server_wallet:operation_id() |
|
||||
swag_server_privdoc:operation_id().
|
||||
|
||||
-type swagger_context() ::
|
||||
swag_server_payres:request_context() |
|
||||
swag_server_wallet:request_context() |
|
||||
swag_server_privdoc:request_context().
|
||||
|
||||
-type handler_context() :: #{
|
||||
woody_context := woody_context:ctx(),
|
||||
swagger_context := swagger_context()
|
||||
}.
|
||||
|
||||
-type handler_opts() ::
|
||||
swag_server_wallet:handler_opts() |
|
||||
swag_server_payres:handler_opts() |
|
||||
swag_server_privdoc:handler_opts().
|
||||
|
||||
-type req_data() :: #{atom() | binary() => term()}.
|
||||
-type status_code() :: 200..599.
|
||||
-type headers() :: cowboy:http_headers().
|
||||
-type response_data() :: map() | [map()] | undefined.
|
||||
-type request_result() :: {ok | error, {status_code(), headers(), response_data()}}.
|
||||
|
||||
-callback process_request(operation_id(), req_data(), handler_context(), handler_opts()) ->
|
||||
request_result() | no_return().
|
||||
|
||||
-export_type([operation_id/0]).
|
||||
-export_type([swagger_context/0]).
|
||||
-export_type([handler_context/0]).
|
||||
-export_type([handler_opts/0]).
|
||||
-export_type([req_data/0]).
|
||||
-export_type([status_code/0]).
|
||||
-export_type([response_data/0]).
|
||||
-export_type([headers/0]).
|
||||
-export_type([request_result/0]).
|
||||
|
||||
%% API
|
||||
|
||||
-define(request_result, wapi_req_result).
|
||||
|
||||
-spec handle_request(operation_id(), req_data(), swagger_context(), module(), handler_opts()) ->
|
||||
request_result().
|
||||
handle_request(OperationID, Req, SwagContext = #{auth_context := AuthContext}, Handler, Opts) ->
|
||||
_ = lager:info("Processing request ~p", [OperationID]),
|
||||
try
|
||||
case wapi_auth:authorize_operation(OperationID, Req, AuthContext) of
|
||||
ok ->
|
||||
WoodyContext = create_woody_context(Req, AuthContext, Opts),
|
||||
Context = create_handler_context(SwagContext, WoodyContext),
|
||||
Handler:process_request(OperationID, Req, Context, Opts)
|
||||
%% ToDo: return back as soon, as authorization is implemented
|
||||
%% {error, _} = Error ->
|
||||
%% _ = lager:info("Operation ~p authorization failed due to ~p", [OperationID, Error]),
|
||||
%% wapi_handler_utils:reply_error(401, wapi_handler_utils:get_error_msg(<<"Unauthorized operation">>))
|
||||
end
|
||||
catch
|
||||
throw:{?request_result, Result} ->
|
||||
Result;
|
||||
error:{woody_error, {Source, Class, Details}} ->
|
||||
process_woody_error(Source, Class, Details)
|
||||
end.
|
||||
|
||||
-spec throw_result(request_result()) ->
|
||||
no_return().
|
||||
throw_result(Res) ->
|
||||
erlang:throw({?request_result, Res}).
|
||||
|
||||
-spec create_woody_context(req_data(), wapi_auth:context(), handler_opts()) ->
|
||||
woody_context:ctx().
|
||||
create_woody_context(#{'X-Request-ID' := RequestID}, AuthContext, Opts) ->
|
||||
RpcID = #{trace_id := TraceID} = woody_context:new_rpc_id(genlib:to_binary(RequestID)),
|
||||
ok = scoper:add_meta(#{request_id => RequestID, trace_id => TraceID}),
|
||||
_ = lager:debug("Created TraceID for the request"),
|
||||
woody_user_identity:put(collect_user_identity(AuthContext, Opts), woody_context:new(RpcID)).
|
||||
|
||||
-define(APP, wapi).
|
||||
|
||||
collect_user_identity(AuthContext, _Opts) ->
|
||||
genlib_map:compact(#{
|
||||
id => wapi_auth:get_subject_id(AuthContext),
|
||||
%% TODO pass realm via Opts
|
||||
realm => genlib_app:env(?APP, realm),
|
||||
email => wapi_auth:get_claim(<<"email">>, AuthContext, undefined),
|
||||
username => wapi_auth:get_claim(<<"name">> , AuthContext, undefined)
|
||||
}).
|
||||
|
||||
-spec create_handler_context(swagger_context(), woody_context:ctx()) ->
|
||||
handler_context().
|
||||
create_handler_context(SwagContext, WoodyContext) ->
|
||||
#{
|
||||
woody_context => WoodyContext,
|
||||
swagger_context => SwagContext
|
||||
}.
|
||||
|
||||
process_woody_error(_Source, result_unexpected , _Details) -> wapi_handler_utils:reply_error(500);
|
||||
process_woody_error(_Source, resource_unavailable, _Details) -> wapi_handler_utils:reply_error(503);
|
||||
process_woody_error(_Source, result_unknown , _Details) -> wapi_handler_utils:reply_error(504).
|
87
apps/wapi/src/wapi_handler_utils.erl
Normal file
87
apps/wapi/src/wapi_handler_utils.erl
Normal file
@ -0,0 +1,87 @@
|
||||
-module(wapi_handler_utils).
|
||||
|
||||
-export([get_error_msg/1]).
|
||||
|
||||
-export([reply_ok/1]).
|
||||
-export([reply_ok/2]).
|
||||
-export([reply_ok/3]).
|
||||
|
||||
-export([reply_error/1]).
|
||||
-export([reply_error/2]).
|
||||
-export([reply_error/3]).
|
||||
|
||||
-export([get_party_id/1]).
|
||||
-export([get_auth_context/1]).
|
||||
|
||||
-export([get_location/3]).
|
||||
|
||||
-define(APP, wapi).
|
||||
|
||||
-type handler_context() :: wapi_handler:handler_context().
|
||||
-type handler_opts() :: wapi_handler:handler_opts().
|
||||
|
||||
-type error_message() :: binary() | io_lib:chars().
|
||||
|
||||
-type status_code() :: wapi_handler:status_code().
|
||||
-type headers() :: wapi_handler:headers().
|
||||
-type response_data() :: wapi_handler:response_data().
|
||||
|
||||
-type party_id() :: binary().
|
||||
-export_type([party_id/0]).
|
||||
|
||||
%% API
|
||||
|
||||
-spec get_party_id(handler_context()) ->
|
||||
party_id().
|
||||
get_party_id(Context) ->
|
||||
wapi_auth:get_subject_id(get_auth_context(Context)).
|
||||
|
||||
-spec get_auth_context(handler_context()) ->
|
||||
wapi_auth:context().
|
||||
get_auth_context(#{swagger_context := #{auth_context := AuthContext}}) ->
|
||||
AuthContext.
|
||||
|
||||
-spec get_error_msg(error_message()) ->
|
||||
response_data().
|
||||
get_error_msg(Message) ->
|
||||
#{<<"message">> => genlib:to_binary(Message)}.
|
||||
|
||||
-spec reply_ok(status_code()) ->
|
||||
{ok, {status_code(), [], undefined}}.
|
||||
reply_ok(Code) ->
|
||||
reply_ok(Code, undefined).
|
||||
|
||||
-spec reply_ok(status_code(), response_data()) ->
|
||||
{ok, {status_code(), [], response_data()}}.
|
||||
reply_ok(Code, Data) ->
|
||||
reply_ok(Code, Data, []).
|
||||
|
||||
-spec reply_ok(status_code(), response_data(), headers()) ->
|
||||
{ok, {status_code(), [], response_data()}}.
|
||||
reply_ok(Code, Data, Headers) ->
|
||||
reply(ok, Code, Data, Headers).
|
||||
|
||||
-spec reply_error(status_code()) ->
|
||||
{error, {status_code(), [], undefined}}.
|
||||
reply_error(Code) ->
|
||||
reply_error(Code, undefined).
|
||||
|
||||
-spec reply_error(status_code(), response_data()) ->
|
||||
{error, {status_code(), [], response_data()}}.
|
||||
reply_error(Code, Data) ->
|
||||
reply_error(Code, Data, []).
|
||||
|
||||
-spec reply_error(status_code(), response_data(), headers()) ->
|
||||
{error, {status_code(), [], response_data()}}.
|
||||
reply_error(Code, Data, Headers) ->
|
||||
reply(error, Code, Data, Headers).
|
||||
|
||||
reply(Status, Code, Data, Headers) ->
|
||||
{Status, {Code, Headers, Data}}.
|
||||
|
||||
-spec get_location(cowboy_router:route_match(), [binary()], handler_opts()) ->
|
||||
headers().
|
||||
get_location(PathSpec, Params, _Opts) ->
|
||||
%% TODO pass base URL via Opts
|
||||
BaseUrl = genlib_app:env(?APP, public_endpoint),
|
||||
[{<<"Location">>, wapi_utils:get_url(BaseUrl, PathSpec, Params)}].
|
142
apps/wapi/src/wapi_payres_handler.erl
Normal file
142
apps/wapi/src/wapi_payres_handler.erl
Normal file
@ -0,0 +1,142 @@
|
||||
-module(wapi_payres_handler).
|
||||
|
||||
-include_lib("dmsl/include/dmsl_cds_thrift.hrl").
|
||||
|
||||
-behaviour(swag_server_payres_logic_handler).
|
||||
-behaviour(wapi_handler).
|
||||
|
||||
%% swag_server_payres_logic_handler callbacks
|
||||
-export([authorize_api_key/3]).
|
||||
-export([handle_request/4]).
|
||||
|
||||
%% wapi_handler callbacks
|
||||
-export([process_request/4]).
|
||||
|
||||
%% Types
|
||||
|
||||
-type req_data() :: wapi_handler:req_data().
|
||||
-type handler_context() :: wapi_handler:handler_context().
|
||||
-type request_result() :: wapi_handler:request_result().
|
||||
-type operation_id() :: swag_server_payres:operation_id().
|
||||
-type api_key() :: swag_server_payres:api_key().
|
||||
-type request_context() :: swag_server_payres:request_context().
|
||||
-type handler_opts() :: swag_server_payres:handler_opts().
|
||||
|
||||
%% API
|
||||
|
||||
-spec authorize_api_key(operation_id(), api_key(), handler_opts()) ->
|
||||
false | {true, wapi_auth:context()}.
|
||||
authorize_api_key(OperationID, ApiKey, Opts) ->
|
||||
ok = scoper:add_meta(#{api => payres, operation_id => OperationID}),
|
||||
wapi_auth:authorize_api_key(OperationID, ApiKey, Opts).
|
||||
|
||||
-spec handle_request(operation_id(), req_data(), request_context(), handler_opts()) ->
|
||||
request_result().
|
||||
handle_request(OperationID, Req, SwagContext, Opts) ->
|
||||
wapi_handler:handle_request(OperationID, Req, SwagContext, ?MODULE, Opts).
|
||||
|
||||
-spec process_request(operation_id(), req_data(), handler_context(), handler_opts()) ->
|
||||
request_result().
|
||||
process_request('StoreBankCard', Req, Context, _Opts) ->
|
||||
{CardData, AuthData} = process_card_data(Req, Context),
|
||||
wapi_handler_utils:reply_ok(201, maps:merge(to_swag(CardData), to_swag(AuthData)));
|
||||
process_request('GetBankCard', #{'token' := Token}, _Context, _Opts) ->
|
||||
case decode_token(Token) of
|
||||
{ok, Data} ->
|
||||
wapi_handler_utils:reply_ok(200, Data);
|
||||
{error, badarg} ->
|
||||
wapi_handler_utils:reply_ok(404)
|
||||
end.
|
||||
|
||||
%% Internal functions
|
||||
|
||||
process_card_data(#{'BankCard' := Data}, Context) ->
|
||||
put_card_data_to_cds(to_thrift(card_data, Data), to_thrift(session_data, Data), Context).
|
||||
|
||||
put_card_data_to_cds(CardData, SessionData, Context) ->
|
||||
Call = {cds_storage, 'PutCardData', [CardData, SessionData]},
|
||||
case service_call(Call, Context) of
|
||||
{ok, #'PutCardDataResult'{session_id = SessionID, bank_card = BankCard}} ->
|
||||
{{bank_card, BankCard}, {auth_data, SessionID}};
|
||||
{exception, Exception} ->
|
||||
case Exception of
|
||||
#'InvalidCardData'{} ->
|
||||
wapi_handler:throw_result(wapi_handler_utils:reply_ok(400,
|
||||
wapi_handler_utils:get_error_msg(<<"Card data is invalid">>)
|
||||
));
|
||||
#'KeyringLocked'{} ->
|
||||
% TODO
|
||||
% It's better for the cds to signal woody-level unavailability when the
|
||||
% keyring is locked, isn't it? It could always mention keyring lock as a
|
||||
% reason in a woody error definition.
|
||||
wapi_handler:throw_result(wapi_handler_utils:reply_error(503))
|
||||
end
|
||||
end.
|
||||
|
||||
to_thrift(card_data, Data) ->
|
||||
{Month, Year} = parse_exp_date(genlib_map:get(<<"expDate">>, Data)),
|
||||
CardNumber = genlib:to_binary(genlib_map:get(<<"cardNumber">>, Data)),
|
||||
#'CardData'{
|
||||
pan = CardNumber,
|
||||
exp_date = #'ExpDate'{
|
||||
month = Month,
|
||||
year = Year
|
||||
},
|
||||
cardholder_name = genlib_map:get(<<"cardHolder">>, Data, undefined),
|
||||
cvv = genlib_map:get(<<"cvv">>, Data, undefined)
|
||||
};
|
||||
to_thrift(session_data, Data) ->
|
||||
#'SessionData'{
|
||||
auth_data = {card_security_code, #'CardSecurityCode'{
|
||||
value = maps:get(<<"cvv">>, Data, <<>>)
|
||||
}}
|
||||
}.
|
||||
|
||||
to_swag({Spec, Data}) when is_atom(Spec) ->
|
||||
to_swag(Spec, Data).
|
||||
|
||||
to_swag(bank_card, #domain_BankCard{
|
||||
'token' = Token,
|
||||
'payment_system' = PaymentSystem,
|
||||
'bin' = Bin,
|
||||
'masked_pan' = MaskedPan
|
||||
}) ->
|
||||
BankCard = genlib_map:compact(#{
|
||||
<<"token">> => Token,
|
||||
<<"paymentSystem">> => genlib:to_binary(PaymentSystem),
|
||||
<<"bin">> => Bin,
|
||||
<<"lastDigits">> => wapi_utils:get_last_pan_digits(MaskedPan)
|
||||
}),
|
||||
BankCard#{<<"token">> => encode_token(BankCard)};
|
||||
|
||||
to_swag(auth_data, PaymentSessionID) ->
|
||||
#{<<"authData">> => genlib:to_binary(PaymentSessionID)}.
|
||||
|
||||
encode_token(TokenData) ->
|
||||
wapi_utils:map_to_base64url(TokenData).
|
||||
|
||||
decode_token(Token) ->
|
||||
try wapi_utils:base64url_to_map(Token) of
|
||||
Data = #{<<"token">> := _} ->
|
||||
{ok, maps:with([<<"token">>, <<"paymentSystem">>, <<"bin">>, <<"lastDigits">>],
|
||||
Data#{<<"token">> => Token})
|
||||
};
|
||||
_ ->
|
||||
{error, badarg}
|
||||
catch
|
||||
error:badarg ->
|
||||
{error, badarg}
|
||||
end.
|
||||
|
||||
parse_exp_date(ExpDate) when is_binary(ExpDate) ->
|
||||
[Month, Year0] = binary:split(ExpDate, <<"/">>),
|
||||
Year = case genlib:to_int(Year0) of
|
||||
Y when Y < 100 ->
|
||||
2000 + Y;
|
||||
Y ->
|
||||
Y
|
||||
end,
|
||||
{genlib:to_int(Month), Year}.
|
||||
|
||||
service_call({ServiceName, Function, Args}, #{woody_context := WoodyContext}) ->
|
||||
wapi_woody_client:call_service(ServiceName, Function, Args, WoodyContext).
|
151
apps/wapi/src/wapi_privdoc_handler.erl
Normal file
151
apps/wapi/src/wapi_privdoc_handler.erl
Normal file
@ -0,0 +1,151 @@
|
||||
-module(wapi_privdoc_handler).
|
||||
|
||||
-include_lib("identdocstore_proto/include/identdocstore_identity_document_storage_thrift.hrl").
|
||||
|
||||
-behaviour(swag_server_privdoc_logic_handler).
|
||||
-behaviour(wapi_handler).
|
||||
|
||||
%% swag_server_privdoc_logic_handler callbacks
|
||||
-export([authorize_api_key/3]).
|
||||
-export([handle_request/4]).
|
||||
|
||||
%% wapi_handler callbacks
|
||||
-export([process_request/4]).
|
||||
|
||||
%% helper
|
||||
%% TODO move it somewhere else
|
||||
-export([get_proof/2]).
|
||||
|
||||
%% Types
|
||||
|
||||
-type req_data() :: wapi_handler:req_data().
|
||||
-type handler_context() :: wapi_handler:handler_context().
|
||||
-type request_result() :: wapi_handler:request_result().
|
||||
-type operation_id() :: swag_server_privdoc:operation_id().
|
||||
-type api_key() :: swag_server_privdoc:api_key().
|
||||
-type request_context() :: swag_server_privdoc:request_context().
|
||||
-type handler_opts() :: swag_server_privdoc:handler_opts().
|
||||
|
||||
|
||||
%% API
|
||||
|
||||
-spec authorize_api_key(operation_id(), api_key(), handler_opts()) ->
|
||||
false | {true, wapi_auth:context()}.
|
||||
authorize_api_key(OperationID, ApiKey, Opts) ->
|
||||
ok = scoper:add_meta(#{api => privdoc, operation_id => OperationID}),
|
||||
wapi_auth:authorize_api_key(OperationID, ApiKey, Opts).
|
||||
|
||||
-spec handle_request(operation_id(), req_data(), request_context(), handler_opts()) ->
|
||||
request_result().
|
||||
handle_request(OperationID, Params, SwagContext, Opts) ->
|
||||
wapi_handler:handle_request(OperationID, Params, SwagContext, ?MODULE, Opts).
|
||||
|
||||
-spec process_request(operation_id(), req_data(), handler_context(), handler_opts()) ->
|
||||
request_result().
|
||||
process_request('StorePrivateDocument', #{'PrivateDocument' := Params}, Context, _Opts) ->
|
||||
wapi_handler_utils:reply_ok(201, process_doc_data(Params, Context)).
|
||||
|
||||
process_doc_data(Params, Context) ->
|
||||
{ok, Token} = put_doc_data_to_cds(to_thrift(doc_data, Params), Context),
|
||||
to_swag(doc, {Params, Token}).
|
||||
|
||||
-spec get_proof(binary(), handler_context()) -> map().
|
||||
get_proof(Token, Context) ->
|
||||
{ok, DocData} = service_call({identdoc_storage, 'Get', [Token]}, Context),
|
||||
to_swag(doc_data, {DocData, Token}).
|
||||
|
||||
to_thrift(doc_data, Params = #{<<"type">> := <<"RUSDomesticPassportData">>}) ->
|
||||
{russian_domestic_passport, #'identdocstore_RussianDomesticPassport'{
|
||||
series = maps:get(<<"series">>, Params),
|
||||
number = maps:get(<<"number">>, Params),
|
||||
issuer = maps:get(<<"issuer">>, Params),
|
||||
issuer_code = maps:get(<<"issuerCode">>, Params),
|
||||
issued_at = maps:get(<<"issuedAt">>, Params),
|
||||
family_name = maps:get(<<"familyName">>, Params),
|
||||
first_name = maps:get(<<"firstName">>, Params),
|
||||
patronymic = maps:get(<<"patronymic">>, Params, undefined),
|
||||
birth_date = maps:get(<<"birthDate">>, Params),
|
||||
birth_place = maps:get(<<"birthPlace">>, Params)
|
||||
}};
|
||||
to_thrift(doc_data, Params = #{<<"type">> := <<"RUSRetireeInsuranceCertificateData">>}) ->
|
||||
{russian_retiree_insurance_certificate, #'identdocstore_RussianRetireeInsuranceCertificate'{
|
||||
number = maps:get(<<"number">>, Params)
|
||||
}}.
|
||||
|
||||
to_swag(doc, {Params, Token}) ->
|
||||
Doc = to_swag(raw_doc, {Params, Token}),
|
||||
Doc#{<<"token">> => wapi_utils:map_to_base64url(Doc)};
|
||||
to_swag(raw_doc, {Params = #{<<"type">> := <<"RUSDomesticPassportData">>}, Token}) ->
|
||||
#{
|
||||
<<"type">> => <<"RUSDomesticPassport">>,
|
||||
<<"token">> => Token,
|
||||
<<"seriesMasked">> => mask(pass_series, Params),
|
||||
<<"numberMasked">> => mask(pass_number, Params),
|
||||
<<"fullnameMasked">> => mask(pass_fullname, Params)
|
||||
};
|
||||
to_swag(raw_doc, {Params = #{<<"type">> := <<"RUSRetireeInsuranceCertificateData">>}, Token}) ->
|
||||
#{
|
||||
<<"type">> => <<"RUSRetireeInsuranceCertificate">>,
|
||||
<<"token">> => Token,
|
||||
<<"numberMasked">> => mask(retiree_insurance_cert_number, Params)
|
||||
};
|
||||
to_swag(doc_data, {{russian_domestic_passport, D}, Token}) ->
|
||||
to_swag(doc, {
|
||||
#{
|
||||
<<"type">> => <<"RUSDomesticPassportData">>,
|
||||
<<"series">> => D#'identdocstore_RussianDomesticPassport'.series,
|
||||
<<"number">> => D#'identdocstore_RussianDomesticPassport'.number,
|
||||
<<"firstName">> => D#'identdocstore_RussianDomesticPassport'.first_name,
|
||||
<<"familyName">> => D#'identdocstore_RussianDomesticPassport'.family_name,
|
||||
<<"patronymic">> => D#'identdocstore_RussianDomesticPassport'.patronymic
|
||||
},
|
||||
Token
|
||||
});
|
||||
to_swag(doc_data, {{russian_retiree_insurance_certificate, D}, Token}) ->
|
||||
to_swag(doc, {
|
||||
#{
|
||||
<<"type">> => <<"RUSRetireeInsuranceCertificateData">>,
|
||||
<<"number">> => D#'identdocstore_RussianRetireeInsuranceCertificate'.number
|
||||
},
|
||||
Token
|
||||
}).
|
||||
|
||||
put_doc_data_to_cds(IdentityDoc, Context) ->
|
||||
service_call({identdoc_storage, 'Put', [IdentityDoc]}, Context).
|
||||
|
||||
service_call({ServiceName, Function, Args}, #{woody_context := WoodyContext}) ->
|
||||
wapi_woody_client:call_service(ServiceName, Function, Args, WoodyContext).
|
||||
|
||||
|
||||
|
||||
-define(PATTERN_DIGIT, [<<"0">>, <<"1">>, <<"2">>, <<"3">>, <<"4">>, <<"5">>, <<"6">>, <<"7">>, <<"8">>, <<"9">>]).
|
||||
|
||||
mask(pass_series, #{<<"series">> := V}) ->
|
||||
wapi_utils:mask_and_keep(leading, 2, $*, V);
|
||||
mask(pass_number, #{<<"number">> := V}) ->
|
||||
wapi_utils:mask_and_keep(trailing, 1, $*, V);
|
||||
mask(pass_fullname, Params) ->
|
||||
MaskedFamilyName = mask(family_name, Params),
|
||||
MaskedFirstName = mask(first_name, Params),
|
||||
MaskedPatronymic = mask(patronymic, Params),
|
||||
<<MaskedFamilyName/binary, " ", MaskedFirstName/binary, MaskedPatronymic/binary>>;
|
||||
mask(family_name, #{<<"familyName">> := V}) ->
|
||||
wapi_utils:mask_and_keep(leading, 1, $*, V);
|
||||
mask(first_name, #{<<"firstName">> := V}) ->
|
||||
<<(unicode:characters_to_binary(string:left(unicode:characters_to_list(V), 1)))/binary, "."/utf8>>;
|
||||
mask(patronymic, #{<<"patronymic">> := V}) ->
|
||||
<<(unicode:characters_to_binary(string:left(unicode:characters_to_list(V), 1)))/binary, "."/utf8>>;
|
||||
mask(patronymic, _) ->
|
||||
<<>>;
|
||||
%% TODO rewrite this ugly shit
|
||||
mask(retiree_insurance_cert_number, #{<<"number">> := Number}) ->
|
||||
FirstPublicSymbols = 2,
|
||||
LastPublicSymbols = 1,
|
||||
V1 = binary:part(Number, {0 , FirstPublicSymbols}),
|
||||
Rest1 = binary:part(Number, {0 + FirstPublicSymbols, size(Number) - (0 + FirstPublicSymbols)}),
|
||||
|
||||
V2 = binary:part(Rest1, {size(Rest1) , -LastPublicSymbols}),
|
||||
Rest2 = binary:part(Rest1, {0, size(Rest1) - LastPublicSymbols}),
|
||||
|
||||
Mask = binary:replace(Rest2, ?PATTERN_DIGIT, <<"*">>, [global]),
|
||||
<<V1/binary, Mask/binary, V2/binary>>.
|
54
apps/wapi/src/wapi_sup.erl
Normal file
54
apps/wapi/src/wapi_sup.erl
Normal file
@ -0,0 +1,54 @@
|
||||
%% @doc Top level supervisor.
|
||||
%% @end
|
||||
|
||||
-module(wapi_sup).
|
||||
-behaviour(supervisor).
|
||||
|
||||
%% API
|
||||
-export([start_link/0]).
|
||||
|
||||
%% Supervisor callbacks
|
||||
-export([init/1]).
|
||||
|
||||
%%
|
||||
|
||||
-spec start_link() -> {ok, pid()} | {error, {already_started, pid()}}.
|
||||
|
||||
start_link() ->
|
||||
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||
|
||||
%%
|
||||
|
||||
-spec init([]) -> {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}.
|
||||
|
||||
init([]) ->
|
||||
AuthorizerSpecs = get_authorizer_child_specs(),
|
||||
{LogicHandlers, LogicHandlerSpecs} = get_logic_handler_info(),
|
||||
HealthRoutes = [{'_', [erl_health_handle:get_route(genlib_app:env(wapi, health_checkers, []))]}],
|
||||
SwaggerSpec = wapi_swagger_server:child_spec({HealthRoutes, LogicHandlers}),
|
||||
{ok, {
|
||||
{one_for_all, 0, 1},
|
||||
AuthorizerSpecs ++ LogicHandlerSpecs ++ [SwaggerSpec]
|
||||
}}.
|
||||
|
||||
-spec get_authorizer_child_specs() -> [supervisor:child_spec()].
|
||||
|
||||
get_authorizer_child_specs() ->
|
||||
Authorizers = genlib_app:env(wapi, authorizers, #{}),
|
||||
[
|
||||
get_authorizer_child_spec(jwt, maps:get(jwt, Authorizers))
|
||||
].
|
||||
|
||||
-spec get_authorizer_child_spec(Name :: atom(), Options :: #{}) -> supervisor:child_spec().
|
||||
|
||||
get_authorizer_child_spec(jwt, Options) ->
|
||||
wapi_authorizer_jwt:get_child_spec(Options).
|
||||
|
||||
-spec get_logic_handler_info() -> {Handlers :: #{atom() => module()}, [Spec :: supervisor:child_spec()] | []} .
|
||||
|
||||
get_logic_handler_info() ->
|
||||
{#{
|
||||
wallet => wapi_wallet_handler,
|
||||
payres => wapi_payres_handler,
|
||||
privdoc => wapi_privdoc_handler
|
||||
}, []}.
|
140
apps/wapi/src/wapi_swagger_server.erl
Normal file
140
apps/wapi/src/wapi_swagger_server.erl
Normal file
@ -0,0 +1,140 @@
|
||||
-module(wapi_swagger_server).
|
||||
|
||||
-export([child_spec /1]).
|
||||
-export([request_hook /1]).
|
||||
-export([response_hook/4]).
|
||||
|
||||
-define(APP, wapi).
|
||||
-define(DEFAULT_ACCEPTORS_POOLSIZE, 100).
|
||||
-define(DEFAULT_IP_ADDR, "::").
|
||||
-define(DEFAULT_PORT, 8080).
|
||||
|
||||
-define(SWAG_HANDLER_SCOPE, swag_handler).
|
||||
|
||||
-type params() :: {cowboy_router:routes(), #{atom() => module()}}.
|
||||
|
||||
-spec child_spec(params()) ->
|
||||
supervisor:child_spec().
|
||||
child_spec({HealthRoutes, LogicHandlers}) ->
|
||||
{Transport, TransportOpts} = get_socket_transport(),
|
||||
CowboyOpts = get_cowboy_config(HealthRoutes, LogicHandlers),
|
||||
AcceptorsPool = genlib_app:env(?APP, acceptors_poolsize, ?DEFAULT_ACCEPTORS_POOLSIZE),
|
||||
ranch:child_spec(?MODULE, AcceptorsPool,
|
||||
Transport, TransportOpts, cowboy_protocol, CowboyOpts).
|
||||
|
||||
get_socket_transport() ->
|
||||
{ok, IP} = inet:parse_address(genlib_app:env(?APP, ip, ?DEFAULT_IP_ADDR)),
|
||||
Port = genlib_app:env(?APP, port, ?DEFAULT_PORT),
|
||||
{ranch_tcp, [{ip, IP}, {port, Port}]}.
|
||||
|
||||
get_cowboy_config(HealthRoutes, LogicHandlers) ->
|
||||
Dispatch =
|
||||
cowboy_router:compile(squash_routes(
|
||||
HealthRoutes ++
|
||||
swag_server_wallet_router:get_paths(maps:get(wallet, LogicHandlers)) ++
|
||||
swag_server_payres_router:get_paths(maps:get(payres, LogicHandlers)) ++
|
||||
swag_server_privdoc_router:get_paths(maps:get(privdoc, LogicHandlers))
|
||||
)),
|
||||
[
|
||||
{env, [
|
||||
{dispatch, Dispatch},
|
||||
{cors_policy, wapi_cors_policy}
|
||||
]},
|
||||
{middlewares, [
|
||||
cowboy_router,
|
||||
cowboy_cors,
|
||||
cowboy_handler
|
||||
]},
|
||||
{onrequest, fun ?MODULE:request_hook/1},
|
||||
{onresponse, fun ?MODULE:response_hook/4}
|
||||
].
|
||||
|
||||
squash_routes(Routes) ->
|
||||
orddict:to_list(lists:foldl(
|
||||
fun ({K, V}, D) -> orddict:update(K, fun (V0) -> V0 ++ V end, V, D) end,
|
||||
orddict:new(),
|
||||
Routes
|
||||
)).
|
||||
|
||||
-spec request_hook(cowboy_req:req()) ->
|
||||
cowboy_req:req().
|
||||
|
||||
request_hook(Req) ->
|
||||
ok = scoper:add_scope(?SWAG_HANDLER_SCOPE),
|
||||
HookFun = cowboy_access_log:get_request_hook(),
|
||||
HookFun(Req).
|
||||
|
||||
-spec response_hook(cowboy:http_status(), cowboy:http_headers(), iodata(), cowboy_req:req()) ->
|
||||
cowboy_req:req().
|
||||
|
||||
response_hook(Code, Headers, Body, Req) ->
|
||||
try
|
||||
{Code1, Headers1, Req1} = handle_response(Code, Headers, Req),
|
||||
_ = log_access(Code1, Headers1, Body, Req1),
|
||||
ok = cleanup_scoper(),
|
||||
Req1
|
||||
catch
|
||||
Class:Reason ->
|
||||
Stack = genlib_format:format_stacktrace(erlang:get_stacktrace(), [newlines]),
|
||||
_ = lager:warning(
|
||||
"Response hook failed for: [~p, ~p, ~p]~nwith: ~p:~p~nstacktrace: ~ts",
|
||||
[Code, Headers, Req, Class, Reason, Stack]
|
||||
),
|
||||
ok = cleanup_scoper(),
|
||||
Req
|
||||
end.
|
||||
|
||||
handle_response(Code, Headers, Req) when Code >= 500 ->
|
||||
send_oops_resp(Code, Headers, get_oops_body_safe(Code), Req);
|
||||
handle_response(Code, Headers, Req) ->
|
||||
{Code, Headers, Req}.
|
||||
|
||||
%% cowboy_req:reply/4 has a faulty spec in case of response body fun.
|
||||
-dialyzer({[no_contracts, no_fail_call], send_oops_resp/4}).
|
||||
|
||||
send_oops_resp(Code, Headers, undefined, Req) ->
|
||||
{Code, Headers, Req};
|
||||
send_oops_resp(Code, Headers, File, Req) ->
|
||||
FileSize = filelib:file_size(File),
|
||||
F = fun(Socket, Transport) ->
|
||||
case Transport:sendfile(Socket, File) of
|
||||
{ok, _} ->
|
||||
ok;
|
||||
{error, Error} ->
|
||||
_ = lager:warning("Failed to send oops body: ~p", [Error]),
|
||||
ok
|
||||
end
|
||||
end,
|
||||
Headers1 = lists:foldl(
|
||||
fun({K, V}, Acc) -> lists:keystore(K, 1, Acc, {K, V}) end,
|
||||
Headers,
|
||||
[
|
||||
{<<"content-type">>, <<"text/plain; charset=utf-8">>},
|
||||
{<<"content-length">>, integer_to_list(FileSize)}
|
||||
]
|
||||
),
|
||||
{ok, Req1} = cowboy_req:reply(Code, Headers1, {FileSize, F}, Req),
|
||||
{Code, Headers1, Req1}.
|
||||
|
||||
get_oops_body_safe(Code) ->
|
||||
try get_oops_body(Code)
|
||||
catch
|
||||
Error:Reason ->
|
||||
_ = lager:warning("Invalid oops body config for code: ~p. Error: ~p:~p", [Code, Error, Reason]),
|
||||
undefined
|
||||
end.
|
||||
|
||||
get_oops_body(Code) ->
|
||||
genlib_map:get(Code, genlib_app:env(?APP, oops_bodies, #{}), undefined).
|
||||
|
||||
log_access(Code, Headers, Body, Req) ->
|
||||
LogFun = cowboy_access_log:get_response_hook(wapi_access_lager_event),
|
||||
LogFun(Code, Headers, Body, Req).
|
||||
|
||||
cleanup_scoper() ->
|
||||
try scoper:get_current_scope() of
|
||||
?SWAG_HANDLER_SCOPE -> scoper:remove_scope();
|
||||
_ -> ok
|
||||
catch
|
||||
error:no_scopes -> ok
|
||||
end.
|
222
apps/wapi/src/wapi_utils.erl
Normal file
222
apps/wapi/src/wapi_utils.erl
Normal file
@ -0,0 +1,222 @@
|
||||
-module(wapi_utils).
|
||||
|
||||
-export([base64url_to_map/1]).
|
||||
-export([map_to_base64url/1]).
|
||||
|
||||
-export([to_universal_time/1]).
|
||||
|
||||
-export([redact/2]).
|
||||
-export([mask_and_keep/4]).
|
||||
-export([mask/4]).
|
||||
|
||||
-export([unwrap/1]).
|
||||
-export([define/2]).
|
||||
|
||||
-export([get_path/2]).
|
||||
-export([get_url/2]).
|
||||
-export([get_url/3]).
|
||||
|
||||
-export([get_last_pan_digits/1]).
|
||||
|
||||
-type binding_value() :: binary().
|
||||
-type url() :: binary().
|
||||
-type path() :: binary().
|
||||
|
||||
%% API
|
||||
|
||||
-spec base64url_to_map(binary()) -> map() | no_return().
|
||||
base64url_to_map(Base64) when is_binary(Base64) ->
|
||||
try jsx:decode(base64url:decode(Base64), [return_maps])
|
||||
catch
|
||||
Class:Reason ->
|
||||
_ = lager:debug("decoding base64 ~p to map failed with ~p:~p", [Base64, Class, Reason]),
|
||||
erlang:error(badarg)
|
||||
end.
|
||||
|
||||
-spec map_to_base64url(map()) -> binary() | no_return().
|
||||
map_to_base64url(Map) when is_map(Map) ->
|
||||
try base64url:encode(jsx:encode(Map))
|
||||
catch
|
||||
Class:Reason ->
|
||||
_ = lager:debug("encoding map ~p to base64 failed with ~p:~p", [Map, Class, Reason]),
|
||||
erlang:error(badarg)
|
||||
end.
|
||||
|
||||
-spec redact(Subject :: binary(), Pattern :: binary()) -> Redacted :: binary().
|
||||
redact(Subject, Pattern) ->
|
||||
case re:run(Subject, Pattern, [global, {capture, all_but_first, index}]) of
|
||||
{match, Captures} ->
|
||||
lists:foldl(fun redact_match/2, Subject, Captures);
|
||||
nomatch ->
|
||||
Subject
|
||||
end.
|
||||
|
||||
redact_match({S, Len}, Subject) ->
|
||||
<<Pre:S/binary, _:Len/binary, Rest/binary>> = Subject,
|
||||
<<Pre/binary, (binary:copy(<<"*">>, Len))/binary, Rest/binary>>;
|
||||
redact_match([Capture], Message) ->
|
||||
redact_match(Capture, Message).
|
||||
|
||||
%% TODO Switch to this sexy code after the upgrade to Erlang 20+
|
||||
%%
|
||||
%% -spec mask(leading|trailing, non_neg_integer(), char(), binary()) ->
|
||||
%% binary().
|
||||
%% mask(Dir = trailing, MaskLen, MaskChar, Str) ->
|
||||
%% mask(Dir, 0, string:length(Str) - MaskLen, MaskChar, Str);
|
||||
%% mask(Dir = leading, MaskLen, MaskChar, Str) ->
|
||||
%% mask(Dir, MaskLen, string:length(Str), MaskChar, Str).
|
||||
|
||||
%% mask(Dir, KeepStart, KeepLen, MaskChar, Str) ->
|
||||
%% unicode:characters_to_binary(string:pad(string:slice(Str, KeepStart, KeepLen), string:length(Str), Dir, MaskChar)).
|
||||
|
||||
-spec mask_and_keep(leading|trailing, non_neg_integer(), char(), binary()) ->
|
||||
binary().
|
||||
mask_and_keep(trailing, KeepLen, MaskChar, Chardata) ->
|
||||
StrLen = erlang:length(unicode:characters_to_list(Chardata)),
|
||||
mask(leading, StrLen - KeepLen, MaskChar, Chardata);
|
||||
mask_and_keep(leading, KeepLen, MaskChar, Chardata) ->
|
||||
StrLen = erlang:length(unicode:characters_to_list(Chardata)),
|
||||
mask(trailing, StrLen - KeepLen, MaskChar, Chardata).
|
||||
|
||||
-spec mask(leading|trailing, non_neg_integer(), char(), binary()) ->
|
||||
binary().
|
||||
mask(trailing, MaskLen, MaskChar, Chardata) ->
|
||||
Str = unicode:characters_to_list(Chardata),
|
||||
unicode:characters_to_binary(
|
||||
string:left(string:substr(Str, 1, erlang:length(Str) - MaskLen), erlang:length(Str), MaskChar)
|
||||
);
|
||||
mask(leading, MaskLen, MaskChar, Chardata) ->
|
||||
Str = unicode:characters_to_list(Chardata),
|
||||
unicode:characters_to_binary(
|
||||
string:right(string:substr(Str, MaskLen + 1), erlang:length(Str), MaskChar)
|
||||
).
|
||||
|
||||
-spec to_universal_time(Timestamp :: binary()) -> TimestampUTC :: binary().
|
||||
to_universal_time(Timestamp) ->
|
||||
{ok, {Date, Time, Usec, TZOffset}} = rfc3339:parse(Timestamp),
|
||||
Seconds = calendar:datetime_to_gregorian_seconds({Date, Time}),
|
||||
%% The following crappy code is a dialyzer workaround
|
||||
%% for the wrong rfc3339:parse/1 spec.
|
||||
{DateUTC, TimeUTC} = calendar:gregorian_seconds_to_datetime(
|
||||
case TZOffset of
|
||||
_ when is_integer(TZOffset) ->
|
||||
Seconds - (60 * TZOffset);
|
||||
_ ->
|
||||
Seconds
|
||||
end
|
||||
),
|
||||
{ok, TimestampUTC} = rfc3339:format({DateUTC, TimeUTC, Usec, 0}),
|
||||
TimestampUTC.
|
||||
|
||||
-spec unwrap(ok | {ok, Value} | {error, _Error}) ->
|
||||
Value | no_return().
|
||||
unwrap(ok) ->
|
||||
ok;
|
||||
unwrap({ok, Value}) ->
|
||||
Value;
|
||||
unwrap({error, Error}) ->
|
||||
erlang:error({unwrap_error, Error}).
|
||||
|
||||
-spec define(undefined | T, T) -> T.
|
||||
define(undefined, V) ->
|
||||
V;
|
||||
define(V, _Default) ->
|
||||
V.
|
||||
|
||||
-spec get_path(cowboy_router:route_match(), [binding_value()]) ->
|
||||
path().
|
||||
get_path(PathSpec, Params) when is_list(PathSpec) ->
|
||||
get_path(genlib:to_binary(PathSpec), Params);
|
||||
get_path(Path, []) ->
|
||||
Path;
|
||||
get_path(PathSpec, [Value | Rest]) ->
|
||||
[P1, P2] = split(PathSpec),
|
||||
P3 = get_next(P2),
|
||||
get_path(<<P1/binary, Value/binary, P3/binary>>, Rest).
|
||||
|
||||
split(PathSpec) ->
|
||||
case binary:split(PathSpec, <<":">>) of
|
||||
Res = [_, _] -> Res;
|
||||
[_] -> erlang:error(param_mismatch)
|
||||
end.
|
||||
|
||||
get_next(PathSpec) ->
|
||||
case binary:split(PathSpec, <<"/">>) of
|
||||
[_, Next] -> <<"/", Next/binary>>;
|
||||
[_] -> <<>>
|
||||
end.
|
||||
|
||||
-spec get_url(url(), path()) ->
|
||||
url().
|
||||
get_url(BaseUrl, Path) ->
|
||||
<<BaseUrl/binary, Path/binary>>.
|
||||
|
||||
-spec get_url(url(), cowboy_router:route_match(), [binding_value()]) ->
|
||||
url().
|
||||
get_url(BaseUrl, PathSpec, Params) ->
|
||||
get_url(BaseUrl, get_path(PathSpec, Params)).
|
||||
|
||||
-define(MASKED_PAN_MAX_LENGTH, 4).
|
||||
|
||||
-spec get_last_pan_digits(binary()) ->
|
||||
binary().
|
||||
get_last_pan_digits(MaskedPan) when byte_size(MaskedPan) > ?MASKED_PAN_MAX_LENGTH ->
|
||||
binary:part(MaskedPan, {byte_size(MaskedPan), -?MASKED_PAN_MAX_LENGTH});
|
||||
get_last_pan_digits(MaskedPan) ->
|
||||
MaskedPan.
|
||||
|
||||
%%
|
||||
|
||||
-ifdef(TEST).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
-spec test() -> _.
|
||||
|
||||
-spec to_universal_time_test() -> _.
|
||||
to_universal_time_test() ->
|
||||
?assertEqual(<<"2017-04-19T13:56:07Z">>, to_universal_time(<<"2017-04-19T13:56:07Z">>)),
|
||||
?assertEqual(<<"2017-04-19T13:56:07.530000Z">>, to_universal_time(<<"2017-04-19T13:56:07.53Z">>)),
|
||||
?assertEqual(<<"2017-04-19T10:36:07.530000Z">>, to_universal_time(<<"2017-04-19T13:56:07.53+03:20">>)),
|
||||
?assertEqual(<<"2017-04-19T17:16:07.530000Z">>, to_universal_time(<<"2017-04-19T13:56:07.53-03:20">>)).
|
||||
|
||||
-spec redact_test() -> _.
|
||||
redact_test() ->
|
||||
P1 = <<"^\\+\\d(\\d{1,10}?)\\d{2,4}$">>,
|
||||
?assertEqual(<<"+7******3210">>, redact(<<"+79876543210">>, P1)),
|
||||
?assertEqual( <<"+1*11">>, redact(<<"+1111">>, P1)).
|
||||
|
||||
-spec get_path_test() -> _.
|
||||
get_path_test() ->
|
||||
?assertEqual(<<"/wallet/v0/deposits/11/events/42">>, get_path(
|
||||
<<"/wallet/v0/deposits/:depositID/events/:eventID">>, [<<"11">>, <<"42">>]
|
||||
)),
|
||||
?assertEqual(<<"/wallet/v0/deposits/11/events/42">>, get_path(
|
||||
"/wallet/v0/deposits/:depositID/events/:eventID", [<<"11">>, <<"42">>]
|
||||
)),
|
||||
?assertError(param_mismatch, get_path(
|
||||
"/wallet/v0/deposits/:depositID/events/:eventID", [<<"11">>, <<"42">>, <<"0">>]
|
||||
)).
|
||||
|
||||
-spec mask_test() -> _.
|
||||
mask_test() ->
|
||||
?assertEqual(<<"Хуй">>, mask(leading, 0, $*, <<"Хуй">>)),
|
||||
?assertEqual(<<"*уй">>, mask(leading, 1, $*, <<"Хуй">>)),
|
||||
?assertEqual(<<"**й">>, mask(leading, 2, $*, <<"Хуй">>)),
|
||||
?assertEqual(<<"***">>, mask(leading, 3, $*, <<"Хуй">>)),
|
||||
?assertEqual(<<"Хуй">>, mask(trailing, 0, $*, <<"Хуй">>)),
|
||||
?assertEqual(<<"Ху*">>, mask(trailing, 1, $*, <<"Хуй">>)),
|
||||
?assertEqual(<<"Х**">>, mask(trailing, 2, $*, <<"Хуй">>)),
|
||||
?assertEqual(<<"***">>, mask(trailing, 3, $*, <<"Хуй">>)).
|
||||
|
||||
-spec mask_and_keep_test() -> _.
|
||||
mask_and_keep_test() ->
|
||||
?assertEqual(<<"***">>, mask_and_keep(leading, 0, $*, <<"Хуй">>)),
|
||||
?assertEqual(<<"Х**">>, mask_and_keep(leading, 1, $*, <<"Хуй">>)),
|
||||
?assertEqual(<<"Ху*">>, mask_and_keep(leading, 2, $*, <<"Хуй">>)),
|
||||
?assertEqual(<<"Хуй">>, mask_and_keep(leading, 3, $*, <<"Хуй">>)),
|
||||
?assertEqual(<<"***">>, mask_and_keep(trailing, 0, $*, <<"Хуй">>)),
|
||||
?assertEqual(<<"**й">>, mask_and_keep(trailing, 1, $*, <<"Хуй">>)),
|
||||
?assertEqual(<<"*уй">>, mask_and_keep(trailing, 2, $*, <<"Хуй">>)),
|
||||
?assertEqual(<<"Хуй">>, mask_and_keep(trailing, 3, $*, <<"Хуй">>)).
|
||||
|
||||
-endif.
|
466
apps/wapi/src/wapi_wallet_ff_backend.erl
Normal file
466
apps/wapi/src/wapi_wallet_ff_backend.erl
Normal file
@ -0,0 +1,466 @@
|
||||
%% Temporary stab for wallet handler
|
||||
|
||||
-module(wapi_wallet_ff_backend).
|
||||
|
||||
-include_lib("dmsl/include/dmsl_domain_thrift.hrl").
|
||||
|
||||
%% API
|
||||
-export([get_providers/2]).
|
||||
-export([get_provider/2]).
|
||||
-export([get_provider_identity_classes/2]).
|
||||
-export([get_provider_identity_class/3]).
|
||||
-export([get_provider_identity_class_levels/3]).
|
||||
-export([get_provider_identity_class_level/4]).
|
||||
|
||||
-export([get_identities/2]).
|
||||
-export([get_identity/2]).
|
||||
-export([create_identity/2]).
|
||||
-export([get_identity_challengies/2]).
|
||||
-export([create_identity_challenge/3]).
|
||||
-export([get_identity_challenge/3]).
|
||||
-export([cancel_identity_challenge/3]).
|
||||
-export([get_identity_challenge_events/2]).
|
||||
-export([get_identity_challenge_event/4]).
|
||||
|
||||
-export([get_destinations/2]).
|
||||
-export([get_destination/2]).
|
||||
-export([create_destination/2]).
|
||||
-export([create_withdrawal/2]).
|
||||
-export([get_withdrawal/2]).
|
||||
-export([get_withdrawal_events/2]).
|
||||
-export([get_withdrawal_event/3]).
|
||||
|
||||
%% Helper API
|
||||
-export([not_implemented/0]).
|
||||
|
||||
%% API
|
||||
|
||||
%% Providers
|
||||
|
||||
-spec get_providers(_, _) -> no_return().
|
||||
get_providers(_Params, _Context) ->
|
||||
not_implemented().
|
||||
|
||||
-spec get_provider(_, _) -> _.
|
||||
get_provider(ProviderId, _Context) ->
|
||||
case ff_provider:get(ProviderId) of
|
||||
{ok, Provider} -> {ok, to_swag(provider, {ProviderId, ff_provider:payinst(Provider)})};
|
||||
Error = {error, _} -> Error
|
||||
end.
|
||||
|
||||
-spec get_provider_identity_classes(_, _) -> _.
|
||||
get_provider_identity_classes(Id, _Context) ->
|
||||
case ff_provider:get(Id) of
|
||||
{ok, Provider} ->
|
||||
{ok, lists:map(
|
||||
fun(ClassId) ->
|
||||
{ok, Class} = do_get_provider_identity_class(ClassId, Provider),
|
||||
Class
|
||||
end,
|
||||
ff_provider:list_identity_classes(Provider)
|
||||
)};
|
||||
Error = {error, _} ->
|
||||
Error
|
||||
end.
|
||||
|
||||
-spec get_provider_identity_class(_, _, _) -> _.
|
||||
get_provider_identity_class(ProviderId, ClassId, _Context) ->
|
||||
case ff_provider:get(ProviderId) of
|
||||
{ok, Provider} -> do_get_provider_identity_class(ClassId, Provider);
|
||||
Error = {error, _} -> Error
|
||||
end.
|
||||
|
||||
do_get_provider_identity_class(ClassId, Provider) ->
|
||||
case ff_provider:get_identity_class(ClassId, Provider) of
|
||||
{ok, Class} -> {ok, to_swag(identity_class, Class)};
|
||||
Error = {error, _} -> Error
|
||||
end.
|
||||
|
||||
-spec get_provider_identity_class_levels(_, _, _) -> no_return().
|
||||
get_provider_identity_class_levels(_ProviderId, _ClassId, _Context) ->
|
||||
not_implemented().
|
||||
|
||||
-spec get_provider_identity_class_level(_, _, _, _) -> no_return().
|
||||
get_provider_identity_class_level(_ProviderId, _ClassId, _LevelId, _Context) ->
|
||||
not_implemented().
|
||||
|
||||
%% Identities
|
||||
|
||||
-spec get_identities(_, _) -> no_return().
|
||||
get_identities(_Params, _Context) ->
|
||||
not_implemented().
|
||||
|
||||
-define(NS, <<"com.rbkmoney.wapi">>).
|
||||
|
||||
-spec get_identity(_, _) -> _.
|
||||
get_identity(IdentityId, _Context) ->
|
||||
case ff_identity_machine:get(IdentityId) of
|
||||
{ok, IdentityState} ->
|
||||
{ok, to_swag(identity, IdentityState)};
|
||||
Error = {error, _} ->
|
||||
Error
|
||||
end.
|
||||
|
||||
-spec create_identity(_, _) -> _.
|
||||
create_identity(Params, Context) ->
|
||||
IdentityId = genlib:unique(),
|
||||
case ff_identity_machine:create(IdentityId, from_swag(identity_params, Params), make_ctx(Params, [<<"name">>])) of
|
||||
ok -> get_identity(IdentityId, Context);
|
||||
{error, exists} -> create_identity(Params, Context);
|
||||
Error = {error, _} -> Error
|
||||
end.
|
||||
|
||||
-spec get_identity_challengies(_, _) -> no_return().
|
||||
get_identity_challengies(_Params, _Context) ->
|
||||
not_implemented().
|
||||
|
||||
-spec create_identity_challenge(_, _, _) -> _.
|
||||
create_identity_challenge(IdentityId, Params, Context) ->
|
||||
ChallengeId = genlib:unique(),
|
||||
case ff_identity_machine:start_challenge(
|
||||
IdentityId,
|
||||
maps:merge(#{id => ChallengeId}, from_swag(identity_challenge_params, Params))
|
||||
) of
|
||||
ok ->
|
||||
get_identity_challenge(IdentityId, ChallengeId, Context);
|
||||
{error, notfound} ->
|
||||
{error, {identity, notfound}};
|
||||
Error = {error, _} ->
|
||||
Error
|
||||
end.
|
||||
|
||||
-spec get_identity_challenge(_, _, _) -> _.
|
||||
get_identity_challenge(IdentityId, ChallengeId, Context) ->
|
||||
case get_identity(IdentityId, Context) of
|
||||
{ok, IdentityState} ->
|
||||
case ff_identity:challenge(ChallengeId, ff_identity_machine:identity(IdentityState)) of
|
||||
{ok, Challenge} ->
|
||||
Proofs = [
|
||||
wapi_privdoc_handler:get_proof(Token, Context) ||
|
||||
{_, Token} <- ff_identity_challenge:proofs(Challenge)
|
||||
],
|
||||
{ok, to_swag(identity_challenge, {ChallengeId, Challenge, Proofs})};
|
||||
Error = {error, notfound} ->
|
||||
Error
|
||||
end;
|
||||
Error = {error, notfound} ->
|
||||
Error
|
||||
end.
|
||||
|
||||
-spec cancel_identity_challenge(_, _, _) -> no_return().
|
||||
cancel_identity_challenge(_IdentityId, _ChallengeId, _Context) ->
|
||||
not_implemented().
|
||||
|
||||
-spec get_identity_challenge_events(_, _) -> no_return().
|
||||
get_identity_challenge_events(Params = #{'identityID' := _IdentityId, 'challengeID' := _ChallengeId, 'limit' := _Limit}, _Context) ->
|
||||
_ = genlib_map:get('eventCursor', Params),
|
||||
not_implemented().
|
||||
|
||||
-spec get_identity_challenge_event(_, _, _, _) -> no_return().
|
||||
get_identity_challenge_event(_IdentityId, _ChallengeId, _EventId, _Context) ->
|
||||
not_implemented().
|
||||
|
||||
%% Withdrawals
|
||||
|
||||
-spec get_destinations(_, _) -> no_return().
|
||||
get_destinations(_Params, _Context) ->
|
||||
not_implemented().
|
||||
|
||||
-spec get_destination(_, _) -> _.
|
||||
get_destination(DestinationId, _Context) ->
|
||||
case ff_destination_machine:get(DestinationId) of
|
||||
{ok, DestinationState} -> {ok, to_swag(destination, DestinationState)};
|
||||
Error = {error, _} -> Error
|
||||
end.
|
||||
|
||||
-spec create_destination(_, _) -> _.
|
||||
create_destination(Params, Context) ->
|
||||
DestinationId = genlib:unique(),
|
||||
case ff_destination_machine:create(
|
||||
DestinationId, from_swag(destination_params, Params), make_ctx(Params, [<<"name">>])
|
||||
) of
|
||||
ok -> get_destination(DestinationId, Context);
|
||||
{error, exists} -> create_destination(Params, Context);
|
||||
Error = {error, _} -> Error
|
||||
end.
|
||||
|
||||
-spec create_withdrawal(_, _) -> _.
|
||||
create_withdrawal(Params, Context) ->
|
||||
WithdrawalId = genlib:unique(),
|
||||
case ff_withdrawal_machine:create(WithdrawalId, from_swag(withdrawal_params, Params), make_ctx(Params, [])) of
|
||||
ok -> get_withdrawal(WithdrawalId, Context);
|
||||
{error, exists} -> create_withdrawal(Params, Context);
|
||||
Error = {error, _} -> Error
|
||||
end.
|
||||
|
||||
-spec get_withdrawal(_, _) -> _.
|
||||
get_withdrawal(WithdrawalId, _Context) ->
|
||||
case ff_withdrawal_machine:get(WithdrawalId) of
|
||||
{ok, State} -> {ok, to_swag(withdrawal, State)};
|
||||
Error = {error, _} -> Error
|
||||
end.
|
||||
|
||||
-spec get_withdrawal_events(_, _) -> _.
|
||||
get_withdrawal_events(Params = #{'withdrawalID' := WithdrawalId, 'limit' := Limit}, _Context) ->
|
||||
case ff_withdrawal_machine:get_status_events(WithdrawalId, genlib_map:get('eventCursor', Params)) of
|
||||
{ok, Events} -> {ok, to_swag(withdrawal_events, filter_status_events(Events, Limit))};
|
||||
Error = {error, _} -> Error
|
||||
end.
|
||||
|
||||
-spec get_withdrawal_event(_, _, _) -> _.
|
||||
get_withdrawal_event(WithdrawalId, EventId, _Context) ->
|
||||
case ff_withdrawal_machine:get_status_events(WithdrawalId, undefined) of
|
||||
{ok, Events} ->
|
||||
case lists:keyfind(EventId, 1, filter_status_events(Events)) of
|
||||
false -> {error, {event, notfound}};
|
||||
Event -> {ok, to_swag(withdrawal_event, Event)}
|
||||
end;
|
||||
Error = {error, _} -> Error
|
||||
end.
|
||||
|
||||
%% Helper API
|
||||
|
||||
-spec not_implemented() -> no_return().
|
||||
not_implemented() ->
|
||||
wapi_handler:throw_result(wapi_handler_utils:reply_error(501)).
|
||||
|
||||
%% Internal functions
|
||||
|
||||
make_ctx(Params, WapiKeys) ->
|
||||
Ctx0 = maps:with(WapiKeys, Params),
|
||||
Ctx1 = case maps:get(<<"metadata">>, Params, undefined) of
|
||||
undefined -> Ctx0;
|
||||
MD -> Ctx0#{<<"md">> => MD}
|
||||
end,
|
||||
#{?NS => Ctx1}.
|
||||
|
||||
filter_status_events(Events) ->
|
||||
filter_status_events(Events, undefined).
|
||||
|
||||
filter_status_events(Events, Limit) ->
|
||||
filter_status_events(Events, [], Limit).
|
||||
|
||||
filter_status_events(_, Acc, Limit) when is_integer(Limit) andalso length(Acc) >= Limit ->
|
||||
Acc;
|
||||
filter_status_events([], Acc, _) ->
|
||||
Acc;
|
||||
filter_status_events([{ID, Ts, {created, _}} | Rest], Acc, Limit) ->
|
||||
filter_status_events(Rest, [{ID, Ts, undefined} | Acc], Limit);
|
||||
filter_status_events([{ID, Ts, {status_changed, Status}} | Rest], Acc, Limit) ->
|
||||
filter_status_events(Rest, [{ID, Ts, Status} | Acc], Limit);
|
||||
filter_status_events([_ | Rest], Acc, Limit) ->
|
||||
filter_status_events(Rest, Acc, Limit).
|
||||
|
||||
%% Marshalling
|
||||
from_swag(identity_params, Params) ->
|
||||
#{
|
||||
party => maps:get(<<"party">> , Params),
|
||||
provider => maps:get(<<"provider">>, Params),
|
||||
class => maps:get(<<"class">> , Params)
|
||||
};
|
||||
from_swag(identity_challenge_params, Params) ->
|
||||
#{
|
||||
class => maps:get(<<"type">>, Params),
|
||||
proofs => from_swag(proofs, maps:get(<<"proofs">>, Params))
|
||||
};
|
||||
from_swag(proofs, Proofs) ->
|
||||
from_swag(list, {proof, Proofs});
|
||||
from_swag(proof, #{<<"token">> := WapiToken}) ->
|
||||
try
|
||||
#{<<"type">> := Type, <<"token">> := Token} = wapi_utils:base64url_to_map(WapiToken),
|
||||
{from_swag(proof_type, Type), Token}
|
||||
catch
|
||||
error:badarg ->
|
||||
wapi_handler:throw_result(wapi_handler_utils:reply_error(
|
||||
422,
|
||||
wapi_handler_utils:get_error_msg(io_lib:format("Invalid proof token: ~p", [WapiToken]))
|
||||
))
|
||||
end;
|
||||
from_swag(proof_type, <<"RUSDomesticPassport">>) ->
|
||||
rus_domestic_passport;
|
||||
from_swag(proof_type, <<"RUSRetireeInsuranceCertificateData">>) ->
|
||||
rus_retiree_insurance_cert;
|
||||
from_swag(destination_params, Params) ->
|
||||
#{
|
||||
identity => maps:get(<<"identity">>, Params),
|
||||
currency => maps:get(<<"currency">>, Params),
|
||||
name => maps:get(<<"name">> , Params),
|
||||
resource => from_swag(destination_resource, maps:get(<<"resource">>, Params))
|
||||
};
|
||||
from_swag(destination_resource, #{
|
||||
<<"type">> := <<"BankCardDestinationResource">>,
|
||||
<<"token">> := WapiToken
|
||||
}) ->
|
||||
#{<<"token">> := CdsToken} = wapi_utils:base64url_to_map(WapiToken),
|
||||
{bank_card, #{token => CdsToken}};
|
||||
from_swag(withdrawal_params, Params) ->
|
||||
#{
|
||||
source => maps:get(<<"wallet">> , Params),
|
||||
destination => maps:get(<<"destination">>, Params),
|
||||
body => from_swag(withdrawal_body , maps:get(<<"body">>, Params))
|
||||
};
|
||||
from_swag(withdrawal_body, Body) ->
|
||||
{maps:get(<<"amount">>, Body), maps:get(<<"currency">>, Body)};
|
||||
from_swag(list, {Type, List}) ->
|
||||
lists:map(fun(V) -> from_swag(Type, V) end, List).
|
||||
|
||||
|
||||
to_swag(_, undefined) ->
|
||||
undefined;
|
||||
to_swag(providers, Providers) ->
|
||||
to_swag(list, {provider, Providers});
|
||||
to_swag(provider, {Id, Provider}) ->
|
||||
to_swag(map, #{
|
||||
<<"id">> => Id,
|
||||
<<"name">> => Provider#'domain_PaymentInstitution'.name,
|
||||
<<"residences">> => to_swag(list, {residence,
|
||||
ordsets:to_list(Provider#'domain_PaymentInstitution'.residences)
|
||||
})
|
||||
});
|
||||
to_swag(residence, Residence) ->
|
||||
genlib_string:to_upper(genlib:to_binary(Residence));
|
||||
to_swag(identity_class, Class) ->
|
||||
to_swag(map, maps:with([id, name], Class));
|
||||
to_swag(identity, #{identity := Identity, times := {CreatedAt, _}, ctx := Ctx}) ->
|
||||
{ok, WapiCtx} = ff_ctx:get(?NS, Ctx),
|
||||
ProviderId = ff_provider:id(ff_identity:provider(Identity)),
|
||||
#{id := ClassId} = ff_identity:class(Identity),
|
||||
to_swag(map, #{
|
||||
<<"id">> => ff_identity:id(Identity),
|
||||
<<"name">> => maps:get(<<"name">>, WapiCtx),
|
||||
<<"metadata">> => maps:get(<<"md">>, WapiCtx, undefined),
|
||||
<<"createdAt">> => to_swag(timestamp, CreatedAt),
|
||||
<<"provider">> => ProviderId,
|
||||
<<"class">> => ClassId,
|
||||
<<"level">> => ff_identity_class:level_id(ff_identity:level(Identity)),
|
||||
<<"effectiveChallenge">> => to_swag(identity_effective_challenge, ff_identity:effective_challenge(Identity)),
|
||||
<<"isBlocked">> => to_swag(is_blocked, ff_identity:is_accessible(Identity))
|
||||
});
|
||||
to_swag(identity_effective_challenge, {ok, ChallegeId}) ->
|
||||
ChallegeId;
|
||||
to_swag(identity_effective_challenge, {error, notfound}) ->
|
||||
undefined;
|
||||
to_swag(identity_challenge, {ChallengeId, Challenge, Proofs}) ->
|
||||
ChallengeClass = ff_identity_challenge:class(Challenge),
|
||||
to_swag(map,#{
|
||||
<<"id">> => ChallengeId,
|
||||
<<"createdAt">> => <<"TODO">>,
|
||||
<<"level">> => ff_identity_class:level_id(ff_identity_class:target_level(ChallengeClass)),
|
||||
<<"type">> => ff_identity_class:challenge_class_id(ChallengeClass),
|
||||
<<"proofs">> => Proofs,
|
||||
<<"status">> => to_swag(challenge_status,
|
||||
{ff_identity_challenge:status(Challenge), ff_identity_challenge:resolution(Challenge)}
|
||||
),
|
||||
<<"validUntil">> => to_swag(idenification_expiration, ff_identity_challenge:status(Challenge)),
|
||||
<<"failureReason">> => to_swag(identity_challenge_failure_reason, ff_identity_challenge:status(Challenge))
|
||||
});
|
||||
to_swag(challenge_status, {pending, _}) ->
|
||||
<<"Pending">>;
|
||||
to_swag(challenge_status, {completed, {ok, approved}}) ->
|
||||
<<"Completed">>;
|
||||
to_swag(challenge_status, {completed, {ok, denied}}) ->
|
||||
<<"Failed">>;
|
||||
to_swag(challenge_status, {failed, _}) ->
|
||||
<<"Failed">>;
|
||||
to_swag(challenge_status, cancelled) ->
|
||||
<<"Cancelled">>;
|
||||
to_swag(idenification_expiration, {completed, #{resolution := approved, valid_until := Timestamp}}) ->
|
||||
to_swag(timestamp, Timestamp);
|
||||
to_swag(idenification_expiration, _) ->
|
||||
undefined;
|
||||
to_swag(identity_challenge_failure_reason, {completed, #{resolution := denied}}) ->
|
||||
<<"Denied">>;
|
||||
to_swag(identity_challenge_failure_reason, {failed, Reason}) ->
|
||||
genlib:to_binary(Reason);
|
||||
to_swag(identity_challenge_failure_reason, _) ->
|
||||
undefined;
|
||||
to_swag(destination, #{destination := Destination, times := {CreatedAt, _}, ctx := Ctx}) ->
|
||||
{ok, WapiCtx} = ff_ctx:get(?NS, Ctx),
|
||||
Wallet = ff_destination:wallet(Destination),
|
||||
to_swag(map, #{
|
||||
<<"id">> => ff_destination:id(Destination),
|
||||
<<"name">> => maps:get(<<"name">>, WapiCtx),
|
||||
<<"metadata">> => maps:get(<<"md">>, WapiCtx, undefined),
|
||||
<<"createdAt">> => to_swag(timestamp, CreatedAt),
|
||||
%% TODO
|
||||
<<"isBlocked">> => to_swag(is_blocked, {ok, accessible}), %% ff_destination:is_accessible(Destination)),
|
||||
<<"identity">> => ff_identity:id(ff_wallet:identity(Wallet)),
|
||||
<<"currency">> => to_swag(currency, ff_wallet:currency(Wallet)),
|
||||
<<"resource">> => to_swag(destination_resource, ff_destination:resource(Destination)),
|
||||
<<"status">> => to_swag(destination_status, ff_destination:status(Destination)),
|
||||
<<"validUntil">> => to_swag(destination_expiration, Destination)
|
||||
});
|
||||
to_swag(destination_status, authorized) ->
|
||||
<<"Authorized">>;
|
||||
to_swag(destination_status, unauthorized) ->
|
||||
<<"Unauthorized">>;
|
||||
to_swag(destination_expiration, #{status := authorized, timeout := Timeout}) ->
|
||||
Timeout;
|
||||
to_swag(destination_expiration, _) ->
|
||||
undefined;
|
||||
to_swag(destination_resource, {bank_card, BankCard}) ->
|
||||
to_swag(map, #{
|
||||
<<"type">> => <<"BankCardDestinationResource">>,
|
||||
<<"token">> => maps:get(token, BankCard),
|
||||
<<"paymentSystem">> => genlib_map:get(payment_system, BankCard),
|
||||
<<"bin">> => genlib_map:get(bin, BankCard),
|
||||
<<"lastDigits">> => to_swag(pan_last_digits, genlib_map:get(masked_pan, BankCard))
|
||||
});
|
||||
to_swag(pan_last_digits, MaskedPan) ->
|
||||
wapi_utils:get_last_pan_digits(MaskedPan);
|
||||
to_swag(withdrawal, St = #{withdrawal := W, times := {CreatedAt, _}, ctx := Ctx}) ->
|
||||
{ok, WapiCtx} = ff_ctx:get(?NS, Ctx),
|
||||
Status = genlib_map:get(status, St),
|
||||
to_swag(map, #{
|
||||
<<"id">> => ff_withdrawal:id(W),
|
||||
<<"createdAt">> => to_swag(timestamp, CreatedAt),
|
||||
<<"metadata">> => maps:get(<<"md">>, WapiCtx, undefined),
|
||||
<<"wallet">> => ff_wallet:id(ff_withdrawal:source(W)),
|
||||
<<"destination">> => ff_destination:id(ff_withdrawal:destination(W)),
|
||||
<<"body">> => to_swag(withdrawal_body, ff_withdrawal:body(W)),
|
||||
<<"status">> => to_swag(withdrawal_status, Status),
|
||||
<<"failure">> => to_swag(withdrawal_failure, Status)
|
||||
});
|
||||
to_swag(withdrawal_body, Body) ->
|
||||
to_swag(map, #{
|
||||
<<"amount">> => maps:get(amount, Body),
|
||||
<<"currency">> => to_swag(currency, maps:get(currency, Body))
|
||||
});
|
||||
to_swag(withdrawal_status, succeeded) ->
|
||||
<<"Succeeded">>;
|
||||
to_swag(withdrawal_status, failed) ->
|
||||
<<"Failed">>;
|
||||
to_swag(withdrawal_status, {failed, _Reason}) ->
|
||||
<<"Failed">>;
|
||||
to_swag(withdrawal_status, _) ->
|
||||
<<"Pending">>;
|
||||
to_swag(withdrawal_failure, {failed, Reason}) ->
|
||||
genlib:to_binary(Reason);
|
||||
to_swag(withdrawal_failure, _) ->
|
||||
undefined;
|
||||
to_swag(withdrawal_events, Events) ->
|
||||
to_swag(list, {withdrawal_event, Events});
|
||||
to_swag(withdrawal_event, {EventId, Ts, Status}) ->
|
||||
to_swag(map, #{
|
||||
<<"eventID">> => EventId,
|
||||
<<"occuredAt">> => to_swag(timestamp, Ts),
|
||||
<<"changes">> => [#{
|
||||
<<"type">> => <<"WithdrawalStatusChanged">>,
|
||||
<<"status">> => to_swag(withdrawal_status, Status),
|
||||
<<"failure">> => to_swag(withdrawal_failure, Status)
|
||||
}]
|
||||
});
|
||||
to_swag(timestamp, {{Date, Time}, Usec}) ->
|
||||
rfc3339:format({Date, Time, Usec, undefined});
|
||||
to_swag(currency, Currency) ->
|
||||
genlib_string:to_upper(genlib:to_binary(Currency));
|
||||
to_swag(is_blocked, {ok, accessible}) ->
|
||||
false;
|
||||
to_swag(is_blocked, _) ->
|
||||
true;
|
||||
to_swag(_Type, V) when is_map(V) ->
|
||||
to_swag(map, V);
|
||||
to_swag(list, {Type, List}) ->
|
||||
lists:map(fun(V) -> to_swag(Type, V) end, List);
|
||||
to_swag(map, Map) ->
|
||||
genlib_map:compact(Map).
|
267
apps/wapi/src/wapi_wallet_handler.erl
Normal file
267
apps/wapi/src/wapi_wallet_handler.erl
Normal file
@ -0,0 +1,267 @@
|
||||
-module(wapi_wallet_handler).
|
||||
|
||||
-behaviour(swag_server_wallet_logic_handler).
|
||||
-behaviour(wapi_handler).
|
||||
|
||||
%% swag_server_wallet_logic_handler callbacks
|
||||
-export([authorize_api_key/3]).
|
||||
-export([handle_request/4]).
|
||||
|
||||
%% wapi_handler callbacks
|
||||
-export([process_request/4]).
|
||||
|
||||
%% Types
|
||||
|
||||
-type req_data() :: wapi_handler:req_data().
|
||||
-type handler_context() :: wapi_handler:handler_context().
|
||||
-type request_result() :: wapi_handler:request_result().
|
||||
-type operation_id() :: swag_server_wallet:operation_id().
|
||||
-type api_key() :: swag_server_wallet:api_key().
|
||||
-type request_context() :: swag_server_wallet:request_context().
|
||||
-type handler_opts() :: swag_server_wallet:handler_opts().
|
||||
|
||||
%% API
|
||||
|
||||
-spec authorize_api_key(operation_id(), api_key(), handler_opts()) ->
|
||||
false | {true, wapi_auth:context()}.
|
||||
authorize_api_key(OperationID, ApiKey, Opts) ->
|
||||
ok = scoper:add_meta(#{api => wallet, operation_id => OperationID}),
|
||||
wapi_auth:authorize_api_key(OperationID, ApiKey, Opts).
|
||||
|
||||
-spec handle_request(swag_server_wallet:operation_id(), req_data(), request_context(), handler_opts()) ->
|
||||
request_result().
|
||||
handle_request(OperationID, Req, SwagContext, Opts) ->
|
||||
wapi_handler:handle_request(OperationID, Req, SwagContext, ?MODULE, Opts).
|
||||
|
||||
|
||||
%% Providers
|
||||
-spec process_request(operation_id(), req_data(), handler_context(), handler_opts()) ->
|
||||
request_result().
|
||||
process_request('ListProviders', Req, #{woody_context := Context}, _Opts) ->
|
||||
case wapi_wallet_ff_backend:get_providers(maps:with(['residence'], Req), Context) of
|
||||
{ok, Providers} -> wapi_handler_utils:reply_ok(200, Providers);
|
||||
{error, notfound} -> wapi_handler_utils:reply_ok(200, [])
|
||||
end;
|
||||
process_request('GetProvider', #{'providerID' := Id}, #{woody_context := Context}, _Opts) ->
|
||||
case wapi_wallet_ff_backend:get_provider(Id, Context) of
|
||||
{ok, Provider} -> wapi_handler_utils:reply_ok(200, Provider);
|
||||
{error, notfound} -> wapi_handler_utils:reply_ok(404)
|
||||
end;
|
||||
process_request('ListProviderIdentityClasses', #{'providerID' := Id}, #{woody_context := Context}, _Opts) ->
|
||||
case wapi_wallet_ff_backend:get_provider_identity_classes(Id, Context) of
|
||||
{ok, Classes} -> wapi_handler_utils:reply_ok(200, Classes);
|
||||
{error, notfound} -> wapi_handler_utils:reply_ok(404)
|
||||
end;
|
||||
process_request('GetProviderIdentityClass', #{
|
||||
'providerID' := ProviderId,
|
||||
'identityClassID' := ClassId
|
||||
}, #{woody_context := Context}, _Opts) ->
|
||||
case wapi_wallet_ff_backend:get_provider_identity_class(ProviderId, ClassId, Context) of
|
||||
{ok, Class} -> wapi_handler_utils:reply_ok(200, Class);
|
||||
{error, notfound} -> wapi_handler_utils:reply_ok(404)
|
||||
end;
|
||||
process_request('ListProviderIdentityLevels', #{
|
||||
'providerID' := ProviderId,
|
||||
'identityClassID' := ClassId
|
||||
}, #{woody_context := Context}, _Opts) ->
|
||||
case wapi_wallet_ff_backend:get_provider_identity_class_levels(ProviderId, ClassId, Context) of
|
||||
{ok, Levels} -> wapi_handler_utils:reply_ok(200, Levels);
|
||||
{error, notfound} -> wapi_handler_utils:reply_ok(404)
|
||||
end;
|
||||
process_request('GetProviderIdentityLevel', #{
|
||||
'providerID' := ProviderId,
|
||||
'identityClassID' := ClassId,
|
||||
'identityLevelID' := LevelId
|
||||
}, #{woody_context := Context}, _Opts) ->
|
||||
case wapi_wallet_ff_backend:get_provider_identity_class_level(ProviderId, ClassId, LevelId, Context) of
|
||||
{ok, Level} -> wapi_handler_utils:reply_ok(200, Level);
|
||||
{error, notfound} -> wapi_handler_utils:reply_ok(404)
|
||||
end;
|
||||
|
||||
%% Identities
|
||||
process_request('ListIdentities', Req, #{woody_context := Context}, _Opts) ->
|
||||
case wapi_wallet_ff_backend:get_identities(maps:with(['provider', 'class', 'level'], Req), Context) of
|
||||
{ok, Identities} -> wapi_handler_utils:reply_ok(200, Identities);
|
||||
{error, notfound} -> wapi_handler_utils:reply_ok(404)
|
||||
end;
|
||||
process_request('GetIdentity', #{'identityID' := IdentityId}, #{woody_context := Context}, _Opts) ->
|
||||
case wapi_wallet_ff_backend:get_identity(IdentityId, Context) of
|
||||
{ok, Identity} -> wapi_handler_utils:reply_ok(200, Identity);
|
||||
{error, notfound} -> wapi_handler_utils:reply_ok(404)
|
||||
end;
|
||||
process_request(O = 'CreateIdentity', #{'Identity' := Params}, C = #{woody_context := Context}, Opts) ->
|
||||
case wapi_wallet_ff_backend:create_identity(Params#{<<"party">> => wapi_handler_utils:get_party_id(C)}, Context) of
|
||||
{ok, Identity = #{<<"id">> := IdentityId}} ->
|
||||
wapi_handler_utils:reply_ok(201, Identity, get_location(O, [IdentityId], Opts));
|
||||
{error, {provider, notfound}} ->
|
||||
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"No such provider">>));
|
||||
{error, {identity_class, notfound}} ->
|
||||
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"No such identity class">>))
|
||||
end;
|
||||
process_request('ListIdentityChallenges', Req, #{woody_context := Context}, _Opts) ->
|
||||
case wapi_wallet_ff_backend:get_identity_challengies(maps:with(['status', 'identityID'], Req), Context) of
|
||||
{ok, Challengies} -> wapi_handler_utils:reply_ok(200, Challengies);
|
||||
{error, notfound} -> wapi_handler_utils:reply_ok(404)
|
||||
end;
|
||||
process_request(O = 'StartIdentityChallenge', #{
|
||||
'identityID' := IdentityId,
|
||||
'IdentityChallenge' := Params
|
||||
}, #{woody_context := Context}, Opts) ->
|
||||
case wapi_wallet_ff_backend:create_identity_challenge(IdentityId, Params, Context) of
|
||||
{ok, Challenge = #{<<"id">> := ChallengeId}} ->
|
||||
wapi_handler_utils:reply_ok(202, Challenge, get_location(O, [ChallengeId], Opts));
|
||||
{error, {identity, notfound}} ->
|
||||
wapi_handler_utils:reply_ok(404);
|
||||
{error, {challenge, {pending, _}}} ->
|
||||
wapi_handler_utils:reply_ok(409);
|
||||
{error, {challenge, {class, notfound}}} ->
|
||||
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"No such identity class">>));
|
||||
{error, {challenge, {proof, notfound}}} ->
|
||||
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"Proof not found">>));
|
||||
{error, {challenge, {proof, insufficient}}} ->
|
||||
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"Insufficient proof">>))
|
||||
%% TODO any other possible errors here?
|
||||
end;
|
||||
process_request('GetIdentityChallenge', #{
|
||||
'identityID' := IdentityId,
|
||||
'challengeID' := ChallengeId
|
||||
}, #{woody_context := Context}, _Opts) ->
|
||||
case wapi_wallet_ff_backend:get_identity_challenge(IdentityId, ChallengeId, Context) of
|
||||
{ok, Challenge} -> wapi_handler_utils:reply_ok(200, Challenge);
|
||||
{error, notfound} -> wapi_handler_utils:reply_ok(404)
|
||||
end;
|
||||
process_request('CancelIdentityChallenge', #{
|
||||
'identityID' := IdentityId,
|
||||
'challengeID' := ChallengeId
|
||||
}, #{woody_context := Context}, _Opts) ->
|
||||
wapi_wallet_ff_backend:cancel_identity_challenge(IdentityId, ChallengeId, Context);
|
||||
process_request('PollIdentityChallengeEvents', Params, #{woody_context := Context}, _Opts) ->
|
||||
case wapi_wallet_ff_backend:get_identity_challenge_events(Params, Context) of
|
||||
{ok, Events} -> wapi_handler_utils:reply_ok(200, Events);
|
||||
{error, notfound} -> wapi_handler_utils:reply_ok(404)
|
||||
end;
|
||||
process_request('GetIdentityChallengeEvent', #{
|
||||
'identityID' := IdentityId,
|
||||
'challengeID' := ChallengeId,
|
||||
'eventID' := EventId
|
||||
}, #{woody_context := Context}, _Opts) ->
|
||||
case wapi_wallet_ff_backend:get_identity_challenge_event(IdentityId, ChallengeId, EventId, Context) of
|
||||
{ok, Event} -> wapi_handler_utils:reply_ok(200, Event);
|
||||
{error, notfound} -> wapi_handler_utils:reply_ok(404);
|
||||
{error, {event, notfound}} -> wapi_handler_utils:reply_ok(404)
|
||||
end;
|
||||
|
||||
%% Wallets
|
||||
process_request(O, _Req, _Context, _Opts) when
|
||||
O =:= 'ListWallets' orelse
|
||||
O =:= 'CreateWallet' orelse
|
||||
O =:= 'GetWallet' orelse
|
||||
O =:= 'GetWalletAccount' orelse
|
||||
O =:= 'IssueWalletGrant'
|
||||
->
|
||||
wapi_wallet_ff_backend:not_implemented();
|
||||
|
||||
%% Deposits
|
||||
process_request(O, _Req, _Context, _Opts) when
|
||||
O =:= 'CreateDeposit' orelse
|
||||
O =:= 'GetDeposit' orelse
|
||||
O =:= 'PollDepositEvents' orelse
|
||||
O =:= 'GetDepositEvents'
|
||||
->
|
||||
wapi_wallet_ff_backend:not_implemented();
|
||||
|
||||
%% Withdrawals
|
||||
process_request('ListDestinations', Req, #{woody_context := Context}, _Opts) ->
|
||||
case wapi_wallet_ff_backend:get_destinations(maps:with(['identity', 'currency'], Req), Context) of
|
||||
{ok, Destinations} -> wapi_handler_utils:reply_ok(200, Destinations);
|
||||
{error, notfound} -> wapi_handler_utils:reply_ok(200, [])
|
||||
end;
|
||||
process_request('GetDestination', #{'destinationID' := DestinationId}, #{woody_context := Context}, _Opts) ->
|
||||
case wapi_wallet_ff_backend:get_destination(DestinationId, Context) of
|
||||
{ok, Destination} -> wapi_handler_utils:reply_ok(200, Destination);
|
||||
{error, notfound} -> wapi_handler_utils:reply_ok(404)
|
||||
end;
|
||||
process_request(O = 'CreateDestination', #{'Destination' := Params}, C = #{woody_context := Context}, Opts) ->
|
||||
case wapi_wallet_ff_backend:create_destination(Params#{party => wapi_handler_utils:get_party_id(C)}, Context) of
|
||||
{ok, Destination = #{<<"id">> := DestinationId}} ->
|
||||
wapi_handler_utils:reply_ok(201, Destination, get_location(O, [DestinationId], Opts));
|
||||
{error, {identity, notfound}} ->
|
||||
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"No such identity">>));
|
||||
{error, {currency, notfound}} ->
|
||||
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"Currency not supported">>))
|
||||
end;
|
||||
process_request('IssueDestinationGrant', #{
|
||||
'destinationID' := DestinationId,
|
||||
'DestinationGrantRequest' := #{<<"validUntil">> := Expiration}
|
||||
}, #{woody_context := Context}, _Opts) ->
|
||||
ExpirationUTC = wapi_utils:to_universal_time(Expiration),
|
||||
ok = check_expiration(ExpirationUTC),
|
||||
case wapi_wallet_ff_backend:get_destination(DestinationId, Context) of
|
||||
{ok, _Destination} ->
|
||||
{ok, {Date, Time, Usec, _Tz}} = rfc3339:parse(ExpirationUTC),
|
||||
wapi_handler_utils:reply_ok(201, #{
|
||||
<<"token">> => wapi_auth:issue_access_token(
|
||||
wapi_handler_utils:get_party_id(Context),
|
||||
{destinations, DestinationId},
|
||||
{deadline, {{Date, Time}, Usec}}
|
||||
),
|
||||
<<"validUntil">> => Expiration
|
||||
});
|
||||
{error, notfound} ->
|
||||
wapi_handler_utils:reply_ok(404)
|
||||
end;
|
||||
process_request(O = 'CreateWithdrawal', #{'WithdrawalParameters' := Params}, #{woody_context := Context}, Opts) ->
|
||||
%% TODO: check authorization crap here (or on the backend)
|
||||
case wapi_wallet_ff_backend:create_withdrawal(Params, Context) of
|
||||
{ok, Withdrawal = #{<<"id">> := WithdrawalId}} ->
|
||||
wapi_handler_utils:reply_ok(201, Withdrawal, get_location(O, [WithdrawalId], Opts));
|
||||
{error, {source, notfound}} ->
|
||||
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"No such wallet">>));
|
||||
{error, {destination, notfound}} ->
|
||||
wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"No such wallet">>))
|
||||
end;
|
||||
process_request('GetWithdrawal', #{'withdrawalID' := WithdrawalId}, #{woody_context := Context}, _Opts) ->
|
||||
case wapi_wallet_ff_backend:get_withdrawal(WithdrawalId, Context) of
|
||||
{ok, Withdrawal} -> wapi_handler_utils:reply_ok(200, Withdrawal);
|
||||
{error, notfound} -> wapi_handler_utils:reply_ok(422, wapi_handler_utils:get_error_msg(<<"No such withdrawal">>))
|
||||
end;
|
||||
process_request('PollWithdrawalEvents', Params, #{woody_context := Context}, _Opts) ->
|
||||
case wapi_wallet_ff_backend:get_withdrawal_events(Params, Context) of
|
||||
{ok, Events} -> wapi_handler_utils:reply_ok(200, Events);
|
||||
{error, notfound} -> wapi_handler_utils:reply_ok(404)
|
||||
end;
|
||||
process_request('GetWithdrawalEvents', #{
|
||||
'withdrawalID' := WithdrawalId,
|
||||
'eventID' := EventId
|
||||
}, #{woody_context := Context}, _Opts) ->
|
||||
case wapi_wallet_ff_backend:get_withdrawal_event(WithdrawalId, EventId, Context) of
|
||||
{ok, Event} -> wapi_handler_utils:reply_ok(200, Event);
|
||||
{error, notfound} -> wapi_handler_utils:reply_ok(404);
|
||||
{error, {event, notfound}} -> wapi_handler_utils:reply_ok(404)
|
||||
end;
|
||||
|
||||
%% Residences
|
||||
process_request('GetResidence', _Req, _Context, _Opts) ->
|
||||
wapi_wallet_ff_backend:not_implemented();
|
||||
|
||||
%% Currencies
|
||||
process_request('GetCurrency', _Req, _Context, _Opts) ->
|
||||
wapi_wallet_ff_backend:not_implemented().
|
||||
|
||||
%% Internal functions
|
||||
|
||||
get_location(OperationId, Params, Opts) ->
|
||||
#{path := PathSpec} = swag_server_wallet_router:get_operation(OperationId),
|
||||
wapi_handler_utils:get_location(PathSpec, Params, Opts).
|
||||
|
||||
check_expiration(Expiration) ->
|
||||
{ok, ExpirationSec} = rfc3339:to_time(Expiration, second),
|
||||
case (genlib_time:unow() - ExpirationSec) >= 0 of
|
||||
true ->
|
||||
wapi_handler:throw_result(wapi_handler_utils:reply_ok(
|
||||
422,
|
||||
wapi_handler_utils:get_error_msg(<<"Already expired">>)
|
||||
));
|
||||
false ->
|
||||
ok
|
||||
end.
|
227
apps/wapi/src/wapi_wallet_mock_backend.erl
Normal file
227
apps/wapi/src/wapi_wallet_mock_backend.erl
Normal file
@ -0,0 +1,227 @@
|
||||
%% Temporary stab for wallet handler
|
||||
|
||||
-module(wapi_wallet_mock_backend).
|
||||
|
||||
-include_lib("dmsl/include/dmsl_domain_thrift.hrl").
|
||||
|
||||
-export([get_providers/2]).
|
||||
-export([get_provider/2]).
|
||||
-export([get_provider_identity_classes/2]).
|
||||
-export([get_provider_identity_class/3]).
|
||||
-export([get_provider_identity_class_levels/3]).
|
||||
-export([get_provider_identity_class_level/4]).
|
||||
|
||||
-export([get_identities/2]).
|
||||
-export([get_identity/2]).
|
||||
-export([create_identity/2]).
|
||||
|
||||
-export([get_destinations/2]).
|
||||
-export([get_destination/2]).
|
||||
-export([create_destination/2]).
|
||||
-export([create_withdrawal/2]).
|
||||
-export([get_withdrawal/2]).
|
||||
-export([get_withdrawal_events/2]).
|
||||
-export([get_withdrawal_event/3]).
|
||||
|
||||
%% API
|
||||
|
||||
-spec get_providers(_, _) -> _.
|
||||
get_providers(_Params, _Context) ->
|
||||
{ok, [#{
|
||||
<<"id">> => <<"1">>,
|
||||
<<"name">> => <<"НКО «ЭПС»">>,
|
||||
<<"residences">> => [<<"RUS">>]
|
||||
}]}.
|
||||
|
||||
-spec get_provider(_, _) -> _.
|
||||
get_provider(_Id, _Context) ->
|
||||
{ok, #{
|
||||
<<"id">> => <<"1">>,
|
||||
<<"name">> => <<"НКО «ЭПС»">>,
|
||||
<<"residences">> => [<<"RUS">>]
|
||||
}}.
|
||||
|
||||
-spec get_provider_identity_classes(_, _) -> _.
|
||||
get_provider_identity_classes(_Id, _Context) ->
|
||||
{ok, [#{
|
||||
<<"id">> => <<"person">>,
|
||||
<<"name">> => <<"Частная харя">>
|
||||
}]}.
|
||||
|
||||
-spec get_provider_identity_class(_, _, _) -> _.
|
||||
get_provider_identity_class(_ProviderId, _ClassId, _Context) ->
|
||||
{ok, #{id => <<"person">>, name => <<"Частная харя">>}}.
|
||||
|
||||
-spec get_provider_identity_class_levels(_, _, _) -> _.
|
||||
get_provider_identity_class_levels(_ProviderId, _ClassId, _Context) ->
|
||||
{ok, [
|
||||
#{
|
||||
<<"id">> => <<"partial">>,
|
||||
<<"name">> => <<"Частично идентифицирован(а/о)">>,
|
||||
<<"challenges">> => #{
|
||||
<<"id">> => <<"esia">>,
|
||||
<<"name">> => <<"Упрощённая идентификация">>,
|
||||
<<"requiredProofs">> => [
|
||||
<<"RUSDomesticPassport">>,
|
||||
<<"RUSRetireeInsuranceCertificate">>
|
||||
]
|
||||
}
|
||||
},
|
||||
#{
|
||||
<<"id">> => <<"full">>,
|
||||
<<"name">> => <<"Полностью идентифицирован(а/о)">>,
|
||||
<<"challenges">> => #{
|
||||
<<"id">> => <<"svyaznoi bpa">>,
|
||||
<<"name">> => <<"Полная идентификацияв Связном">>,
|
||||
<<"requiredProofs">> => [
|
||||
<<"RUSDomesticPassport">>,
|
||||
<<"RUSRetireeInsuranceCertificate">>
|
||||
]
|
||||
}
|
||||
}
|
||||
]}.
|
||||
|
||||
-spec get_provider_identity_class_level(_, _, _, _) -> _.
|
||||
get_provider_identity_class_level(_ProviderId, _ClassId, _LevelId, _Context) ->
|
||||
{ok, #{
|
||||
<<"id">> => <<"partial">>,
|
||||
<<"name">> => <<"Частично идентифицирован(а/о)">>,
|
||||
<<"challenges">> => #{
|
||||
<<"id">> => <<"esia">>,
|
||||
<<"name">> => <<"Упрощённая идентификация">>,
|
||||
<<"requiredProofs">> => [
|
||||
<<"RUSDomesticPassport">>,
|
||||
<<"RUSRetireeInsuranceCertificate">>
|
||||
]
|
||||
}
|
||||
}}.
|
||||
|
||||
-spec get_identities(_, _) -> _.
|
||||
get_identities(_Params, _Context) ->
|
||||
{ok, [#{
|
||||
<<"id">> => <<"douknowdawae">>,
|
||||
<<"name">> => <<"Keyn Fawkes aka Slug">>,
|
||||
<<"metadata">> => #{<<"is real">> => false},
|
||||
<<"createdAt">> => {{{1989, 01, 17}, {12, 01, 45}}, 0},
|
||||
<<"provider">> => <<"1">>,
|
||||
<<"class">> => <<"person">>,
|
||||
<<"level">> => <<"partial">>,
|
||||
<<"effectiveChallenge">> => <<"25">>,
|
||||
<<"isBlocked">> => false
|
||||
}]}.
|
||||
|
||||
-spec get_identity(_, _) -> _.
|
||||
get_identity(IdentityId, _Context) ->
|
||||
{ok, #{
|
||||
<<"id">> => IdentityId,
|
||||
<<"name">> => <<"Keyn Fawkes aka Slug">>,
|
||||
<<"metadata">> => #{<<"is real">> => false},
|
||||
<<"createdAt">> => {{{1989, 01, 17}, {12, 01, 45}}, 0},
|
||||
<<"provider">> => <<"1">>,
|
||||
<<"class">> => <<"person">>,
|
||||
<<"level">> => <<"partial">>,
|
||||
<<"effectiveChallenge">> => <<"25">>,
|
||||
<<"isBlocked">> => false
|
||||
}}.
|
||||
|
||||
-spec create_identity(_, _) -> _.
|
||||
create_identity(_Params, Context) ->
|
||||
get_identity(woody_context:new_req_id(), Context).
|
||||
|
||||
-spec get_destinations(_, _) -> _.
|
||||
get_destinations(_Params, _Context) ->
|
||||
{ok, [#{
|
||||
<<"id">> => <<"107498">>,
|
||||
<<"name">> => <<"Squarey plastic thingy">>,
|
||||
<<"metadata">> => #{<<"display_name">> => <<"Картофан СБЕР">>},
|
||||
<<"createdAt">> => <<"2018-06-20T08:56:02Z">>,
|
||||
<<"isBlocked">> => false,
|
||||
<<"identity">> => <<"douknowdawae">>,
|
||||
<<"currency">> => <<"RUB">>,
|
||||
<<"resource">> => get_destination_resource(what, ever),
|
||||
<<"status">> => <<"Authorized">>,
|
||||
<<"validUntil">> => <<"2018-06-20T08:56:02Z">>
|
||||
}]}.
|
||||
|
||||
-spec get_destination(_, _) -> _.
|
||||
get_destination(_DestinationId, _Context) ->
|
||||
{ok, #{
|
||||
<<"id">> => <<"107498">>,
|
||||
<<"name">> => <<"Squarey plastic thingy">>,
|
||||
<<"metadata">> => #{<<"display_name">> => <<"Картофан СБЕР">>},
|
||||
<<"createdAt">> => <<"2018-06-20T08:56:02Z">>,
|
||||
<<"isBlocked">> => false,
|
||||
<<"identity">> => <<"douknowdawae">>,
|
||||
<<"currency">> => <<"RUB">>,
|
||||
<<"resource">> => get_destination_resource(what, ever),
|
||||
<<"status">> => <<"Authorized">>,
|
||||
<<"validUntil">> => <<"2018-06-20T08:56:02Z">>
|
||||
}}.
|
||||
|
||||
-spec create_destination(_, _) -> _.
|
||||
create_destination(_Params, Context) ->
|
||||
get_destination(woody_context:new_req_id(), Context).
|
||||
|
||||
-spec get_withdrawal(_, _) -> _.
|
||||
get_withdrawal(WithdrawalId, _Context) ->
|
||||
{ok, #{
|
||||
<<"id">> => WithdrawalId,
|
||||
<<"createdAt">> => {{{2018, 06, 17}, {12, 01, 45}}, 0},
|
||||
<<"wallet">> => woody_context:new_req_id(),
|
||||
<<"destination">> => woody_context:new_req_id(),
|
||||
<<"body">> => #{
|
||||
<<"amount">> => 1430000,
|
||||
<<"currency">> => <<"RUB">>
|
||||
},
|
||||
<<"status">> => <<"Pending">>,
|
||||
<<"metadata">> => #{<<"who'sthedaddy">> => <<"me">>}
|
||||
}}.
|
||||
|
||||
-spec create_withdrawal(_, _) -> _.
|
||||
create_withdrawal(_Params, Context) ->
|
||||
get_withdrawal(woody_context:new_req_id(), Context).
|
||||
|
||||
-spec get_withdrawal_events(_, _) -> _.
|
||||
get_withdrawal_events(_, _) ->
|
||||
[#{
|
||||
<<"eventID">> => 1,
|
||||
<<"occuredAt">> => "2018-06-28T12:49:12Z",
|
||||
<<"changes">> => [#{
|
||||
<<"type">> => <<"WithdrawalStatusChanged">>,
|
||||
<<"status">> => <<"Pending">>
|
||||
}]
|
||||
},
|
||||
#{
|
||||
<<"eventID">> => 5,
|
||||
<<"occuredAt">> => "2018-06-28T12:49:13Z",
|
||||
<<"changes">> => [#{
|
||||
<<"type">> => <<"WithdrawalStatusChanged">>,
|
||||
<<"status">> => <<"Failed">>,
|
||||
<<"failure">> => <<"tolkonepiu is not a function">>
|
||||
}]
|
||||
}].
|
||||
|
||||
-spec get_withdrawal_event(_, _, _) -> _.
|
||||
get_withdrawal_event(_WithdrawalId, EventId, _) ->
|
||||
#{
|
||||
<<"eventID">> => EventId,
|
||||
<<"occuredAt">> => "2018-07-24T04:37:45Z",
|
||||
<<"changes">> => [#{
|
||||
<<"type">> => <<"WithdrawalStatusChanged">>,
|
||||
<<"status">> => <<"Succeeded">>
|
||||
}]
|
||||
}.
|
||||
|
||||
%% Internals
|
||||
|
||||
get_destination_resource(_, _) ->
|
||||
#{
|
||||
<<"type">> => <<"BankCardDestinationResource">>,
|
||||
<<"bin">> => <<"424242">>,
|
||||
<<"lastDigits">> => <<"4242">>,
|
||||
<<"paymentSystem">> => <<"visa">>,
|
||||
<<"token">> => <<
|
||||
"eyJiaW4iOiI0MjQyNDIiLCJsYXN0RGlnaXRzIjoiNDI0MiIsInBheW1lbnRTeXN0ZW"
|
||||
"0iOiJ2aXNhIiwidG9rZW4iOiI3NXlQSkZac1lCOEFvdEFUS0dFa3p6In0"
|
||||
>>
|
||||
}.
|
41
apps/wapi/src/wapi_woody_client.erl
Normal file
41
apps/wapi/src/wapi_woody_client.erl
Normal file
@ -0,0 +1,41 @@
|
||||
-module(wapi_woody_client).
|
||||
|
||||
-export([call_service/4]).
|
||||
-export([call_service/5]).
|
||||
|
||||
-export([get_service_modname/1]).
|
||||
|
||||
%%
|
||||
-define(APP, wapi).
|
||||
|
||||
-type service_name() :: atom().
|
||||
|
||||
-spec call_service(service_name(), woody:func(), [term()], woody_context:ctx()) ->
|
||||
woody:result().
|
||||
|
||||
call_service(ServiceName, Function, Args, Context) ->
|
||||
call_service(ServiceName, Function, Args, Context, scoper_woody_event_handler).
|
||||
|
||||
-spec call_service(service_name(), woody:func(), [term()], woody_context:ctx(), woody:ev_handler()) ->
|
||||
woody:result().
|
||||
|
||||
call_service(ServiceName, Function, Args, Context, EventHandler) ->
|
||||
{Url, Service} = get_service_spec(ServiceName),
|
||||
Request = {Service, Function, Args},
|
||||
woody_client:call(Request, #{url => Url, event_handler => EventHandler}, Context).
|
||||
|
||||
get_service_spec(ServiceName) ->
|
||||
{get_service_url(ServiceName), get_service_modname(ServiceName)}.
|
||||
|
||||
get_service_url(ServiceName) ->
|
||||
maps:get(ServiceName, genlib_app:env(?APP, service_urls)).
|
||||
|
||||
-spec get_service_modname(service_name()) -> woody:service().
|
||||
|
||||
get_service_modname(cds_storage) ->
|
||||
{dmsl_cds_thrift, 'Storage'};
|
||||
get_service_modname(identdoc_storage) ->
|
||||
{identdocstore_identity_document_storage_thrift, 'IdentityDocumentStorage'}.
|
||||
|
||||
%% get_service_modname(webhook_manager) ->
|
||||
%% {dmsl_webhooker_thrift, 'WebhookManager'}.
|
27
apps/wapi/var/keys/wapi/private.pem
Normal file
27
apps/wapi/var/keys/wapi/private.pem
Normal file
@ -0,0 +1,27 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpAIBAAKCAQEA4MUtYkvoIAHNgvYtHSydanyY1qD8nJ+D/A1FFp5LF4SmM9nn
|
||||
vSfTFC2T3D53sCR/DtUzCFIQwZIXXHob22ndFydZqhahrYLLJkpH5IXMy593Sho/
|
||||
oXzxgwkbXaOMevcLFZcj5AneG+q2vFjaDGeQAJaAAPGinMo6UN94DYguNH2s6zqo
|
||||
yRc8ng6KWD5UgEFTIEWni1RIZvp2NAnSkh/SeI1zs9uY6AR7bf6oFSChTd9m+li5
|
||||
d20L5tc0aX7LG842SJEM2dJKckI4ZDZHvU6nDitH3TGrxkMa0CqLe7nUOfvSff2c
|
||||
H9m0CzSbPy/SnyTQLklWoFsi9z2cqqtY6SvR7QIDAQABAoIBADAoz1KSZQgGmtwG
|
||||
lx/7ITdhvvWtxLJiU0s8JKN2Ayzk1R+i/s4+rDFUmqvEDq0FBNxOvgJ4YvK2tJ6x
|
||||
4yoeAqslWUbiVn3w2ko3/DNwn7K5VjvgZ+XX+X9UAjMMCduG9y8HFT+VBawBnGm6
|
||||
t+2UevxFQuPw4iCqC9isKPLtTMkeBXfaCA+tzBqVytlBeW5nJG1Bh9GSV6OeeNoc
|
||||
x6lh1X+7kxk/qLQZsogNwZXxPLuIK0qJCfsGzMYodSi43nv2mFtl5vBt0M+iU42i
|
||||
KrL32SlQmkBI4st/HIie9YpSjj55llOU6L0KBPhH58wc8LDEc2Kwcxeow4/McO0E
|
||||
fSwf9pkCgYEA+4v+371szXbUTfOBOBO7+bGbTo0gzJ8JnMaSdVDLhBaqyp5dbztS
|
||||
TPiaCqfEYk4AYnl2dR7nLYRca/WRDle7hvDqB7K2RWWS58RDifiQ4gfJM9lW4Ocu
|
||||
SIhnxVmr4iVdo4eOs6pxe8yRtF1U+uK8WuoV06+lgL/esEJB2JPyeQsCgYEA5L/U
|
||||
osQFOogSk1Ycjl66UEXm0Y2HzFONTKMSellUnkdSSscx6+mLOn7eL5voSNSJrnCw
|
||||
Tfh3uZ0NOh63Yw3aPGCwtn+EIflW1hzx+DJMvCS5TaU3BZF954rljklJL6VpaIPP
|
||||
fXrc0z1FcsAT2s3aQNmEK2SWp7Y44V6mpQn7a+cCgYEA0Tf+dD+MOFRmfrNSvb6E
|
||||
MUkMwMfXCPoaN6BdfmAF9cYYpdAULIjtigGXtdcWGyF/ZmhaI03hv9UAPfcQgBpu
|
||||
ae0E6gQ1YAD8r/Jorl/kuWr6aTqS7Rq7Py7dCKLtuHmVqYb9JOhV3T8nzRl3rfhZ
|
||||
61AZeWj1QeHUKUvikm1zVkMCgYEAyan42xn3BhgKUEw9VqJanQRTLnEYxGDwlBy7
|
||||
4JM6j2OPQA+GilXVgddxKAXJ7dM6IkiElei0HDZB//gucqw2tr4DbJDUu2LnVFIm
|
||||
XEpz7fZuSu6ZqFYQ6n1ATYV8eP3aBOMXnKchYTWGMVj26BJNFJju9ZZzXx293aol
|
||||
PiCjwAcCgYAmOtRZRtf/p1eXPz1JN1OwEVSrnghJP5KBA8XGsnBmQUTeMmHo3Wl7
|
||||
rELKg0O2bsPtTTAvm5bfLsRgvee+EY28mAY6MA8xJNHB6OabOHuRHqX7ow/LOagK
|
||||
15mUtZ9f8AaKamZ3Bmg/XWWJxNmeCt5LJDr1OnmCDyItbfF9DxnXXg==
|
||||
-----END RSA PRIVATE KEY-----
|
@ -62,8 +62,33 @@
|
||||
}
|
||||
}},
|
||||
{services, #{
|
||||
'partymgmt' => "http://hellgate:8022/v1/processing/partymgmt",
|
||||
'accounter' => "http://shumway:8022/accounter"
|
||||
'partymgmt' => "http://hellgate:8022/v1/processing/partymgmt",
|
||||
'accounter' => "http://shumway:8022/accounter",
|
||||
'identification' => "http://identification:8022/v1/identification"
|
||||
}}
|
||||
]},
|
||||
|
||||
%% wapi
|
||||
{wapi, [
|
||||
{ip, "::"},
|
||||
{port, 8080},
|
||||
%% To send ASCII text in 5xx replies
|
||||
%% {oops_bodies, #{
|
||||
%% 500 => "oops_bodies/500_body"
|
||||
%% }},
|
||||
{realm, <<"external">>},
|
||||
{authorizers, #{
|
||||
jwt => #{
|
||||
signee => wapi,
|
||||
keyset => #{
|
||||
wapi => {pem_file, "var/keys/wapi/private.pem"}
|
||||
}
|
||||
}
|
||||
}},
|
||||
{service_urls, #{
|
||||
webhook_manager => "http://hooker:8022/hook",
|
||||
cds_storage => "http://cds:8022/v1/storage",
|
||||
identdoc_storage => "http://cds:8022/v1/identity_document_storage"
|
||||
}}
|
||||
]},
|
||||
|
||||
@ -78,7 +103,16 @@
|
||||
}},
|
||||
{services, #{
|
||||
'automaton' => "http://machinegun:8022/v1/automaton"
|
||||
}}
|
||||
}},
|
||||
{net_opts, [
|
||||
% Bump keepalive timeout up to a minute
|
||||
{timeout, 60000}
|
||||
]},
|
||||
{health_checkers, [
|
||||
{erl_health, disk , ["/", 99] },
|
||||
{erl_health, cg_memory, [99] },
|
||||
{erl_health, service , [<<"wapi">>]}
|
||||
]}
|
||||
]}
|
||||
|
||||
].
|
||||
|
@ -116,7 +116,13 @@
|
||||
{vm_args , "./config/vm.args"},
|
||||
{dev_mode , false},
|
||||
{include_erts , true},
|
||||
{extended_start_script , true}
|
||||
{extended_start_script , true},
|
||||
%% wapi
|
||||
{overlay, [
|
||||
{mkdir , "var/keys/wapi" },
|
||||
{copy , "apps/wapi/var/keys/wapi/private.pem", "var/keys/wapi/private.pem" }
|
||||
]}
|
||||
|
||||
]}
|
||||
|
||||
]},
|
||||
|
56
rebar.lock
56
rebar.lock
@ -1,7 +1,20 @@
|
||||
{"1.1.0",
|
||||
[{<<"certifi">>,{pkg,<<"certifi">>,<<"0.7.0">>},2},
|
||||
{<<"cowboy">>,{pkg,<<"cowboy">>,<<"1.0.4">>},1},
|
||||
{<<"cowlib">>,{pkg,<<"cowlib">>,<<"1.0.2">>},2},
|
||||
[{<<"base64url">>,{pkg,<<"base64url">>,<<"0.0.1">>},0},
|
||||
{<<"certifi">>,{pkg,<<"certifi">>,<<"0.7.0">>},1},
|
||||
{<<"cg_mon">>,
|
||||
{git,"https://github.com/rbkmoney/cg_mon.git",
|
||||
{ref,"5a87a37694e42b6592d3b4164ae54e0e87e24e18"}},
|
||||
1},
|
||||
{<<"cowboy">>,{pkg,<<"cowboy">>,<<"1.0.4">>},0},
|
||||
{<<"cowboy_access_log">>,
|
||||
{git,"git@github.com:rbkmoney/cowboy_access_log.git",
|
||||
{ref,"99df4e8069d3d581a55d287e6bc0cb575c67d5ec"}},
|
||||
0},
|
||||
{<<"cowboy_cors">>,
|
||||
{git,"https://github.com/danielwhite/cowboy_cors.git",
|
||||
{ref,"392f5804b63fff2bd0fda67671d5b2fbe0badd37"}},
|
||||
0},
|
||||
{<<"cowlib">>,{pkg,<<"cowlib">>,<<"1.0.2">>},1},
|
||||
{<<"dmsl">>,
|
||||
{git,"git@github.com:rbkmoney/damsel.git",
|
||||
{ref,"62d932bc3984301a6394aa8addade6f2892ac79f"}},
|
||||
@ -14,6 +27,10 @@
|
||||
{git,"git@github.com:rbkmoney/dmt_core.git",
|
||||
{ref,"045c78132ecce5a8ec4a2e6ccd2c6b0b65bade1f"}},
|
||||
1},
|
||||
{<<"erl_health">>,
|
||||
{git,"https://github.com/rbkmoney/erlang-health.git",
|
||||
{ref,"ab3ca1ccab6e77905810aa270eb936dbe70e02f8"}},
|
||||
0},
|
||||
{<<"erlang_localtime">>,
|
||||
{git,"https://github.com/kpy3/erlang_localtime",
|
||||
{ref,"c79fa7dd454343e7cbbdcce0c7a95ad86af1485d"}},
|
||||
@ -24,25 +41,43 @@
|
||||
0},
|
||||
{<<"goldrush">>,{pkg,<<"goldrush">>,<<"0.1.9">>},1},
|
||||
{<<"gproc">>,{pkg,<<"gproc">>,<<"0.6.1">>},0},
|
||||
{<<"hackney">>,{pkg,<<"hackney">>,<<"1.6.2">>},1},
|
||||
{<<"hackney">>,{pkg,<<"hackney">>,<<"1.6.2">>},0},
|
||||
{<<"id_proto">>,
|
||||
{git,"git@github.com:rbkmoney/identification-proto.git",
|
||||
{ref,"2da2c4717afd7fd7ef7caba3ef9013a9eb557250"}},
|
||||
0},
|
||||
{<<"idna">>,{pkg,<<"idna">>,<<"1.2.0">>},2},
|
||||
{<<"identdocstore_proto">>,
|
||||
{git,"git@github.com:rbkmoney/identdocstore-proto.git",
|
||||
{ref,"ccd301e37c128810c9f68d7a64dd8183af91b2bf"}},
|
||||
0},
|
||||
{<<"idna">>,{pkg,<<"idna">>,<<"1.2.0">>},1},
|
||||
{<<"jesse">>,
|
||||
{git,"https://github.com/rbkmoney/jesse.git",
|
||||
{ref,"39105922d1ce5834383d8e8aa877c60319b9834a"}},
|
||||
0},
|
||||
{<<"jose">>,{pkg,<<"jose">>,<<"1.7.9">>},0},
|
||||
{<<"jsx">>,{pkg,<<"jsx">>,<<"2.8.2">>},0},
|
||||
{<<"lager">>,{pkg,<<"lager">>,<<"3.6.1">>},0},
|
||||
{<<"machinery">>,
|
||||
{git,"git@github.com:rbkmoney/machinery.git",
|
||||
{ref,"eb1beed9a287d8b6ab8c68b782b2143ef574c99d"}},
|
||||
0},
|
||||
{<<"metrics">>,{pkg,<<"metrics">>,<<"1.0.1">>},2},
|
||||
{<<"metrics">>,{pkg,<<"metrics">>,<<"1.0.1">>},1},
|
||||
{<<"mg_proto">>,
|
||||
{git,"git@github.com:rbkmoney/machinegun_proto.git",
|
||||
{ref,"5c07c579014f9900357f7a72f9d10a03008b9da1"}},
|
||||
0},
|
||||
{<<"mimerl">>,{pkg,<<"mimerl">>,<<"1.0.2">>},2},
|
||||
{<<"mimerl">>,{pkg,<<"mimerl">>,<<"1.0.2">>},1},
|
||||
{<<"parse_trans">>,
|
||||
{git,"https://github.com/rbkmoney/parse_trans.git",
|
||||
{ref,"5ee45f5bfa6c04329bea3281977b774f04c89f11"}},
|
||||
0},
|
||||
{<<"payproc_errors">>,
|
||||
{git,"git@github.com:rbkmoney/payproc-errors-erlang.git",
|
||||
{ref,"9c720534eb88edc6ba47af084939efabceb9b2d6"}},
|
||||
0},
|
||||
{<<"quickrand">>,{pkg,<<"quickrand">>,<<"1.7.3">>},1},
|
||||
{<<"ranch">>,{pkg,<<"ranch">>,<<"1.5.0">>},2},
|
||||
{<<"ranch">>,{pkg,<<"ranch">>,<<"1.5.0">>},1},
|
||||
{<<"rfc3339">>,{pkg,<<"rfc3339">>,<<"0.2.2">>},0},
|
||||
{<<"scoper">>,
|
||||
{git,"git@github.com:rbkmoney/scoper.git",
|
||||
@ -52,7 +87,7 @@
|
||||
{git,"https://github.com/rbkmoney/snowflake.git",
|
||||
{ref,"0a598108f6582affe3b4ae550fc5b9f2062e318a"}},
|
||||
1},
|
||||
{<<"ssl_verify_fun">>,{pkg,<<"ssl_verify_fun">>,<<"1.1.1">>},2},
|
||||
{<<"ssl_verify_fun">>,{pkg,<<"ssl_verify_fun">>,<<"1.1.1">>},1},
|
||||
{<<"thrift">>,
|
||||
{git,"https://github.com/rbkmoney/thrift_erlang.git",
|
||||
{ref,"240bbc842f6e9b90d01bd07838778cf48752b510"}},
|
||||
@ -68,6 +103,7 @@
|
||||
0}]}.
|
||||
[
|
||||
{pkg_hash,[
|
||||
{<<"base64url">>, <<"36A90125F5948E3AFD7BE97662A1504B934DD5DAC78451CA6E9ABF85A10286BE">>},
|
||||
{<<"certifi">>, <<"861A57F3808F7EB0C2D1802AFEAAE0FA5DE813B0DF0979153CBAFCD853ABABAF">>},
|
||||
{<<"cowboy">>, <<"A324A8DF9F2316C833A470D918AAF73AE894278B8AA6226CE7A9BF699388F878">>},
|
||||
{<<"cowlib">>, <<"9D769A1D062C9C3AC753096F868CA121E2730B9A377DE23DEC0F7E08B1DF84EE">>},
|
||||
@ -75,6 +111,8 @@
|
||||
{<<"gproc">>, <<"4579663E5677970758A05D8F65D13C3E9814EC707AD51D8DCEF7294EDA1A730C">>},
|
||||
{<<"hackney">>, <<"96A0A5E7E65B7ACAD8031D231965718CC70A9B4131A8B033B7543BBD673B8210">>},
|
||||
{<<"idna">>, <<"AC62EE99DA068F43C50DC69ACF700E03A62A348360126260E87F2B54ECED86B2">>},
|
||||
{<<"jose">>, <<"9DC5A14AB62DB4E41677FCC97993752562FB57AD0B8BA062589682EDD3ACB91F">>},
|
||||
{<<"jsx">>, <<"7ACC7D785B5ABE8A6E9ADBDE926A24E481F29956DD8B4DF49E3E4E7BCC92A018">>},
|
||||
{<<"lager">>, <<"9D29C5FF7F926D25ECD9899990867C9152DCF34EEE65BAC8EC0DFC0D16A26E0C">>},
|
||||
{<<"metrics">>, <<"25F094DEA2CDA98213CECC3AEFF09E940299D950904393B2A29D191C346A8486">>},
|
||||
{<<"mimerl">>, <<"993F9B0E084083405ED8252B99460C4F0563E41729AB42D9074FD5E52439BE88">>},
|
||||
|
1
schemes/swag
Submodule
1
schemes/swag
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit ab892644583d093e78137feb5a65a3fdbd170c7d
|
Loading…
Reference in New Issue
Block a user