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 |
41 |
IndexLocation = {priv_file, mongooseim, "graphql/wsite/index.html"}, |
37 |
41 |
OptsMap = maps:from_list(Opts), |
38 |
41 |
{cowboy_rest, |
39 |
|
Req, |
40 |
|
OptsMap#{index_location => IndexLocation} |
41 |
|
}. |
42 |
|
|
43 |
|
allowed_methods(Req, State) -> |
44 |
41 |
{[<<"GET">>, <<"POST">>], Req, State}. |
45 |
|
|
46 |
|
content_types_accepted(Req, State) -> |
47 |
39 |
{[ |
48 |
|
{{<<"application">>, <<"json">>, []}, from_json} |
49 |
|
], Req, State}. |
50 |
|
|
51 |
|
content_types_provided(Req, State) -> |
52 |
41 |
{[ |
53 |
|
{{<<"application">>, <<"json">>, []}, to_json}, |
54 |
|
{{<<"text">>, <<"html">>, []}, to_html} |
55 |
|
], Req, State}. |
56 |
|
|
57 |
|
charsets_provided(Req, State) -> |
58 |
41 |
{[<<"utf-8">>], Req, State}. |
59 |
|
|
60 |
|
is_authorized(Req, State) -> |
61 |
41 |
try cowboy_req:parse_header(<<"authorization">>, Req) of |
62 |
|
Auth -> |
63 |
41 |
case check_auth(Auth, State) of |
64 |
|
{ok, State2} -> |
65 |
41 |
{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 |
39 |
{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 |
39 |
case gather(Req) of |
87 |
|
{error, Reason} -> |
88 |
:-( |
reply_error(Reason, Req, State); |
89 |
|
{ok, Req2, Decoded} -> |
90 |
39 |
run_request(Decoded, Req2, State) |
91 |
|
end. |
92 |
|
|
93 |
39 |
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 |
34 |
auth_admin(Auth, State); |
100 |
|
check_auth(Auth, #{schema_endpoint := <<"user">>} = State) -> |
101 |
7 |
auth_user(Auth, State). |
102 |
|
|
103 |
|
auth_user({basic, User, Password}, State) -> |
104 |
4 |
JID = jid:from_binary(User), |
105 |
4 |
case mongoose_api_common:check_password(JID, Password) of |
106 |
4 |
{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 |
31 |
{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 |
37 |
Ep = mongoose_graphql:get_endpoint(binary_to_existing_atom(EpName)), |
127 |
37 |
Ctx = maps:get(schema_ctx, State, #{}), |
128 |
37 |
ReqCtx2 = ReqCtx#{authorized => AuthStatus, ctx => Ctx}, |
129 |
37 |
case mongoose_graphql:execute(Ep, ReqCtx2) of |
130 |
|
{ok, Response} -> |
131 |
37 |
ResponseBody = mongoose_graphql_cowboy_response:term_to_json(Response), |
132 |
37 |
Req2 = cowboy_req:set_resp_body(ResponseBody, Req), |
133 |
37 |
Reply = cowboy_req:reply(200, Req2), |
134 |
37 |
{stop, Reply, State}; |
135 |
|
{error, Reason} -> |
136 |
:-( |
reply_error(Reason, Req, State) |
137 |
|
end. |
138 |
|
|
139 |
|
gather(Req) -> |
140 |
39 |
{ok, Body, Req2} = cowboy_req:read_body(Req), |
141 |
39 |
Bindings = cowboy_req:bindings(Req2), |
142 |
39 |
try jiffy:decode(Body, [return_maps]) of |
143 |
|
JSON -> |
144 |
39 |
gather(Req2, JSON, Bindings) |
145 |
|
catch |
146 |
|
_:_ -> |
147 |
:-( |
{error, make_error(decode, invalid_json_body)} |
148 |
|
end. |
149 |
|
|
150 |
|
gather(Req, Body, Params) -> |
151 |
39 |
QueryDocument = document([Params, Body]), |
152 |
39 |
case variables([Params, Body]) of |
153 |
|
{ok, Vars} -> |
154 |
39 |
Operation = operation_name([Params, Body]), |
155 |
39 |
{ok, Req, #{document => QueryDocument, |
156 |
|
vars => Vars, |
157 |
|
operation_name => Operation}}; |
158 |
|
{error, Reason} -> |
159 |
:-( |
{error, Reason} |
160 |
|
end. |
161 |
|
|
162 |
37 |
document([#{<<"query">> := Q}|_]) -> Q; |
163 |
41 |
document([_|Next]) -> document(Next); |
164 |
2 |
document([]) -> undefined. |
165 |
|
|
166 |
|
variables([#{<<"variables">> := Vars} | _]) -> |
167 |
32 |
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 |
32 |
{ok, Vars}; |
179 |
|
Vars == null -> |
180 |
:-( |
{ok, #{}} |
181 |
|
end; |
182 |
|
variables([_ | Next]) -> |
183 |
46 |
variables(Next); |
184 |
|
variables([]) -> |
185 |
7 |
{ok, #{}}. |
186 |
|
|
187 |
|
operation_name([#{<<"operationName">> := OpName} | _]) -> |
188 |
32 |
OpName; |
189 |
|
operation_name([_ | Next]) -> |
190 |
46 |
operation_name(Next); |
191 |
|
operation_name([]) -> |
192 |
7 |
undefined. |
193 |
|
|
194 |
|
make_error(Phase, Term) -> |
195 |
2 |
#{error_term => Term, phase => Phase}. |
196 |
|
|
197 |
|
reply_error(Msg, Req, State) -> |
198 |
2 |
{Code, Error} = mongoose_graphql_errors:format_error(Msg), |
199 |
2 |
Body = jiffy:encode(#{errors => [Error]}), |
200 |
2 |
Req2 = cowboy_req:set_resp_body(Body, Req), |
201 |
2 |
Reply = cowboy_req:reply(Code, Req2), |
202 |
2 |
{stop, Reply, State}. |