-module(livery_stripe_webhook_handler).
-moduledoc """
A mountable livery handler for the Stripe webhook endpoint.
Add it to a router with `routes/0,1`:
```erlang
livery_router:compile(livery_stripe_webhook_handler:routes()).
```
`handle/1` reads the raw body and `Stripe-Signature` header, verifies the
event with `livery_stripe_webhook`, and dispatches it to the configured
callback (the `webhook_callback` config key). It always answers JSON: 200
when accepted, 400 on a bad payload or signature.
The callback may be a `fun/1` (`Event`), a `fun/2` (`Type, Event`), a
module implementing `handle_event/2`, or a `{Module, Function}` pair.
Persistence (updating a user's subscription, etc.) belongs in the
callback; this module stays storage-agnostic.
""".
-export([handle/1, handle/2, routes/0, routes/1]).
-callback handle_event(Type :: binary(), Event :: map()) -> ok | {error, term()}.
-doc "livery route handler. Reads secret and callback from app config.".
-spec handle(livery_req:req()) -> livery_resp:resp().
handle(Req) ->
handle(Req, handler_opts()).
-doc "Like `handle/1` but with explicit `#{secret, callback}` opts.".
-spec handle(livery_req:req(), map()) -> livery_resp:resp().
handle(Req, Opts) ->
Secret = maps:get(secret, Opts, undefined),
Callback = maps:get(callback, Opts, undefined),
Raw = read_body(Req),
Sig = livery_req:header(<<"stripe-signature">>, Req),
case livery_stripe_webhook:construct_event(Raw, Sig, Secret) of
{ok, Event} ->
_ = dispatch(Callback, Event),
livery_resp:json(200, <<"{\"received\":true}">>);
{error, invalid_payload} ->
livery_resp:json(400, <<"{\"error\":\"invalid payload\"}">>);
{error, _} ->
livery_resp:json(400, <<"{\"error\":\"invalid signature\"}">>)
end.
-doc "Routes for the default path `/stripe/webhook`.".
-spec routes() -> [tuple()].
routes() ->
routes(<<"/stripe/webhook">>).
-spec routes(binary()) -> [tuple()].
routes(Path) ->
[{<<"POST">>, Path, {?MODULE, handle}}].
%%====================================================================
%% Internals
%%====================================================================
dispatch(undefined, _Event) ->
ok;
dispatch(Fun, Event) when is_function(Fun, 1) ->
Fun(Event);
dispatch(Fun, Event) when is_function(Fun, 2) ->
Fun(event_type(Event), Event);
dispatch({Mod, Fun}, Event) ->
Mod:Fun(event_type(Event), Event);
dispatch(Mod, Event) when is_atom(Mod) ->
Mod:handle_event(event_type(Event), Event).
event_type(Event) ->
maps:get(<<"type">>, Event, <<>>).
handler_opts() ->
Cfg = livery_stripe:config(),
#{
secret => maps:get(webhook_secret, Cfg, undefined),
callback => maps:get(webhook_callback, Cfg, undefined)
}.
read_body(Req) ->
case livery_req:body(Req) of
empty ->
<<>>;
{buffered, IoData} ->
iolist_to_binary(IoData);
{stream, Reader} ->
case livery_body:read_all(Reader) of
{ok, Bin, _Reader} -> Bin;
{error, _Reason, _Reader} -> <<>>
end
end.