./ct_report/coverage/mongoose_graphql_cowboy_handler.COVER.html

1 %% @doc A cowboy handler for graphql listeners. It supports both admin and user
2 %% schemas. The `schema_endpoint' config option must be set to decide which
3 %% schema to use.
4 %%
5 %% The graphql request is authorized, processed and then passed for execution.
6 %% @end
7 -module(mongoose_graphql_cowboy_handler).
8
9 -behavior(cowboy_rest).
10
11 %% Cowboy Handler Interface
12 -export([init/2]).
13
14 %% REST callbacks
15 -export([
16 allowed_methods/2,
17 resource_exists/2,
18 content_types_provided/2,
19 content_types_accepted/2,
20 charsets_provided/2,
21 is_authorized/2
22 ]).
23
24 %% Data input/output callbacks
25 -export([
26 from_json/2,
27 to_json/2,
28 to_html/2
29 ]).
30
31 -ignore_xref([cowboy_router_paths/2, from_json/2, to_html/2, to_json/2]).
32
33 %% API
34
35 init(Req, Opts) ->
36 211 IndexLocation = {priv_file, mongooseim, "graphql/wsite/index.html"},
37 211 OptsMap = maps:from_list(Opts),
38 211 {cowboy_rest,
39 Req,
40 OptsMap#{index_location => IndexLocation}
41 }.
42
43 allowed_methods(Req, State) ->
44 211 {[<<"GET">>, <<"POST">>], Req, State}.
45
46 content_types_accepted(Req, State) ->
47 209 {[
48 {{<<"application">>, <<"json">>, []}, from_json}
49 ], Req, State}.
50
51 content_types_provided(Req, State) ->
52 211 {[
53 {{<<"application">>, <<"json">>, []}, to_json},
54 {{<<"text">>, <<"html">>, []}, to_html}
55 ], Req, State}.
56
57 charsets_provided(Req, State) ->
58 211 {[<<"utf-8">>], Req, State}.
59
60 is_authorized(Req, State) ->
61 211 try cowboy_req:parse_header(<<"authorization">>, Req) of
62 Auth ->
63 211 case check_auth(Auth, State) of
64 {ok, State2} ->
65 211 {true, Req, State2};
66 error ->
67
:-(
Msg = make_error(authorize, wrong_credentials),
68
:-(
reply_error(Msg, Req, State)
69 end
70 catch
71 exit:Err ->
72
:-(
reply_error(make_error(authorize, Err), Req, State)
73 end.
74
75 resource_exists(#{method := <<"GET">>} = Req, State) ->
76 2 {true, Req, State};
77 resource_exists(#{method := <<"POST">>} = Req, State) ->
78 209 {false, Req, State}.
79
80 to_html(Req, #{index_location := {priv_file, App, FileLocation}} = State) ->
81 2 Filename = filename:join(code:priv_dir(App), FileLocation),
82 2 {ok, Data} = file:read_file(Filename),
83 2 {Data, Req, State}.
84
85 json_request(Req, State) ->
86 209 case gather(Req) of
87 {error, Reason} ->
88
:-(
reply_error(Reason, Req, State);
89 {ok, Req2, Decoded} ->
90 209 run_request(Decoded, Req2, State)
91 end.
92
93 209 from_json(Req, State) -> json_request(Req, State).
94
:-(
to_json(Req, State) -> json_request(Req, State).
95
96 %% Internal
97
98 check_auth(Auth, #{schema_endpoint := <<"admin">>} = State) ->
99 131 auth_admin(Auth, State);
100 check_auth(Auth, #{schema_endpoint := <<"user">>} = State) ->
101 80 auth_user(Auth, State).
102
103 auth_user({basic, User, Password}, State) ->
104 77 JID = jid:from_binary(User),
105 77 case mongoose_api_common:check_password(JID, Password) of
106 77 {true, _} -> {ok, State#{authorized => true, schema_ctx => #{user => JID}}};
107
:-(
_ -> error
108 end;
109 auth_user(_, State) ->
110 3 {ok, State#{authorized => false}}.
111
112 auth_admin({basic, Username, Password}, #{username := Username, password := Password} = State) ->
113 128 {ok, State#{authorized => true}};
114 auth_admin({basic, _, _}, _) ->
115
:-(
error;
116 auth_admin(_, #{username := _, password := _} = State) ->
117 3 {ok, State#{authorized => false}};
118 auth_admin(_, State) ->
119 % auth credentials not provided in config
120
:-(
{ok, State#{authorized => true}}.
121
122 run_request(#{document := undefined}, Req, State) ->
123 2 reply_error(make_error(decode, no_query_supplied), Req, State);
124 run_request(#{} = ReqCtx, Req, #{schema_endpoint := EpName,
125 authorized := AuthStatus} = State) ->
126 207 Ep = mongoose_graphql:get_endpoint(binary_to_existing_atom(EpName)),
127 207 Ctx = maps:get(schema_ctx, State, #{}),
128 207 ReqCtx2 = ReqCtx#{authorized => AuthStatus, ctx => Ctx},
129 207 case mongoose_graphql:execute(Ep, ReqCtx2) of
130 {ok, Response} ->
131 203 ResponseBody = mongoose_graphql_cowboy_response:term_to_json(Response),
132 203 Req2 = cowboy_req:set_resp_body(ResponseBody, Req),
133 203 Reply = cowboy_req:reply(200, Req2),
134 203 {stop, Reply, State};
135 {error, Reason} ->
136 4 reply_error(Reason, Req, State)
137 end.
138
139 gather(Req) ->
140 209 {ok, Body, Req2} = cowboy_req:read_body(Req),
141 209 Bindings = cowboy_req:bindings(Req2),
142 209 try jiffy:decode(Body, [return_maps]) of
143 JSON ->
144 209 gather(Req2, JSON, Bindings)
145 catch
146 _:_ ->
147
:-(
{error, make_error(decode, invalid_json_body)}
148 end.
149
150 gather(Req, Body, Params) ->
151 209 QueryDocument = document([Params, Body]),
152 209 case variables([Params, Body]) of
153 {ok, Vars} ->
154 209 Operation = operation_name([Params, Body]),
155 209 {ok, Req, #{document => QueryDocument,
156 vars => Vars,
157 operation_name => Operation}};
158 {error, Reason} ->
159
:-(
{error, Reason}
160 end.
161
162 207 document([#{<<"query">> := Q}|_]) -> Q;
163 211 document([_|Next]) -> document(Next);
164 2 document([]) -> undefined.
165
166 variables([#{<<"variables">> := Vars} | _]) ->
167 196 if
168 is_binary(Vars) ->
169
:-(
try jiffy:decode(Vars, [return_maps]) of
170
:-(
null -> {ok, #{}};
171
:-(
JSON when is_map(JSON) -> {ok, JSON};
172
:-(
_ -> {error, make_error(decode, variables_invalid_json)}
173 catch
174 _:_ ->
175
:-(
{error, make_error(decode, variables_invalid_json)}
176 end;
177 is_map(Vars) ->
178 196 {ok, Vars};
179 Vars == null ->
180
:-(
{ok, #{}}
181 end;
182 variables([_ | Next]) ->
183 222 variables(Next);
184 variables([]) ->
185 13 {ok, #{}}.
186
187 operation_name([#{<<"operationName">> := OpName} | _]) ->
188 199 OpName;
189 operation_name([_ | Next]) ->
190 219 operation_name(Next);
191 operation_name([]) ->
192 10 undefined.
193
194 make_error(Phase, Term) ->
195 2 #{error_term => Term, phase => Phase}.
196
197 reply_error(Msg, Req, State) ->
198 6 {Code, Error} = mongoose_graphql_errors:format_error(Msg),
199 6 Body = jiffy:encode(#{errors => [Error]}),
200 6 Req2 = cowboy_req:set_resp_body(Body, Req),
201 6 Reply = cowboy_req:reply(Code, Req2),
202 6 {stop, Reply, State}.
Line Hits Source