Split the web front-end and the backend into separate applications.

This design will make things easier to both scale and test.
Having a dedicated back-end application allow us to keep the API
stable, while improving the internals.
main
absc 2024-09-10 19:40:30 +00:00
parent 31da1067da
commit eebf62d527
19 changed files with 308 additions and 479 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,20 +0,0 @@
.PHONY: all clean
.SUFFIXES: .erl .beam
ERLC?= erlc -server
ERLFLAGS= -I ../../
OBJS= dudeswave.beam dudeswave_app.beam
OBJS+= dudeswave_supervisor.beam dudeswave_handler.beam
OBJS+= dudeswave_user_handler.beam dudeswave_auth.beam
OBJS+= dudeswave_auth_handler.beam
all: ${OBJS}
.erl.beam:
${ERLC} ${ERLOPTS} ${ERLFLAGS} $<
clean:
rm -f *.beam

View File

@ -1,408 +0,0 @@
%
% Copyright (c) 2024 Andrea Biscuola <a@abiscuola.com>
%
% Permission to use, copy, modify, and distribute this software for any
% purpose with or without fee is hereby granted, provided that the above
% copyright notice and this permission notice appear in all copies.
%
% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
%
-module(dudeswave_auth).
-moduledoc """
Dudes users management module.
Here lives all the functions for the APIs needed to create, update and delete
users from the dudeswave database.
""".
-include_lib("dudeswave/include/defines.hrl").
-include_lib("storage/include/storage.hrl").
-export([authenticate/2, details/1, new/3,
update/4, delete/1, logout/2, auth_cookies/1, invalidate_cookies/1,
set_auth_cookies/4, read_login_data/1, read_new_user_data/1,
read_update_user_data/1]).
-doc """
Verify a session with an existing cookie.
Spec:
```
-spec authenticate(User, Auth) -> true | false | {true, Cookie, Validity} | {error, Reason} when
User :: binary(),
Auth :: {cookie, binary()} | {password, binary()},
Reason :: term().
```
Authenticate a user with either an existing `Cookie` or by issuing a new one
after authenticating with `Password`.
If `Cookie` is valid, the function returns `true`. If the authentication is denied
returns `false`
""".
-spec authenticate(User, Auth) -> true | false | {true, Cookie, Validity} | {error, Reason} when
User :: binary(),
Auth :: {cookie, binary()} | {password, binary()},
Cookie :: binary(),
Validity :: pos_integer(),
Reason :: term().
authenticate(User, {cookie, Cookie}) ->
case storage:read(?COOKIESBUCK, Cookie) of
{ok, [R]} ->
CurTime = calendar:now_to_universal_time(erlang:timestamp()),
CookieTime = R#object.value,
{user, CookieUser} = proplists:lookup(user, R#object.metadata),
if
CookieTime >= CurTime ->
if
User =:= CookieUser -> true;
true -> false
end;
true -> false
end;
{ok, []} -> false;
{error, _} -> {error, service_unavailable}
end;
authenticate(User, {password, Password}) ->
case storage:read(?USERSBUCK, User) of
{ok, [R]} ->
Validity = case application:get_env(cookie_validity) of
{ok, Value} ->
erlang:system_time(seconds) + Value * 86400;
undefined ->
erlang:system_time(seconds) + ?DEFVALIDITY * 86400
end,
{hash, Hash} = proplists:lookup(hash, R#object.metadata),
{salt, Salt} = proplists:lookup(salt, R#object.metadata),
{approved, Appr} = proplists:lookup(approved, R#object.metadata),
Auth = crypto:hash(sha256, <<Password/binary, Salt/binary>>),
if
Appr =/= true -> false;
Auth =:= Hash ->
Cookie = base64:encode(rand:bytes(64)),
case storage:write(?COOKIESBUCK, <<Cookie/binary>>,
Validity, [{user, User}]) of
ok -> {true, Cookie, Validity};
{error, Reason} -> {error, Reason}
end;
true -> false
end;
{ok, []} -> false;
{error, Reason} -> {error, Reason}
end.
-doc """
Close an existing session
Spec:
```
-spec logout(User, Cookie) -> ok | {error, Reason} when
User :: binary(),
Cookie :: binary(),
Reason :: term().
```
Invalidate and delete `Cookie` associated with `User` from the system.
""".
-spec logout(User, Cookie) -> ok | {error, Reason} when
User :: binary(),
Cookie :: binary(),
Reason :: term().
logout(User, Cookie) ->
case storage:read(?COOKIESBUCK, Cookie) of
{ok, [R]} ->
{user, User} = proplists:lookup(user, R#object.metadata),
storage:delete(?COOKIESBUCK, Cookie);
{ok, []} ->
{error, not_found};
{error, Reason} ->
{error, Reason}
end.
-doc """
Return user details.
Spec:
```
-spec details(User) -> Value | {error, Reason} when
User :: binary(),
Value :: term(),
Reason :: term().
```
""".
-spec details(User) -> Value | {error, Reason} when
User :: binary(),
Value :: term(),
Reason :: term().
details(User) ->
case storage:read(?USERSBUCK, User) of
{ok, [R]} -> R#object.value;
{error, Reason} -> {error, Reason};
{ok, []} -> {error, not_found}
end.
-doc """
Create a new user.
Spec:
```
-spec new(User, Password, Email) -> ok | {error, Reason} when
User :: binary(),
Password :: binary(),
Email :: binary(),
Reason :: term().
```
The `User` is created, and stored in the application's users bucket
`Password` is salted and hashed with SHA256 before being stored.
The new user is saved with a metadata `approved` of `false`,
""".
-spec new(User, Password, Email) -> ok | {error, Reason} when
User :: binary(),
Password :: binary(),
Email :: binary(),
Reason :: term().
new(User, Password, Email) ->
Salt = rand:bytes(?RANDBYTES),
Hash = crypto:hash(sha256, <<Password/binary, Salt/binary>>),
Data = #{<<"email">> => Email},
Metadata = [{salt, Salt}, {hash, Hash}, {approved, false}],
storage:write(?USERSBUCK, User, Data, Metadata).
-doc """
Update user's details
Spec:
```
-spec update(User, Name, Email, Desc) -> ok | {error, Reason} when
User :: binary(),
Name :: binary(),
Email :: binary(),
Desc :: binary(),
Reason :: term().
```
The details apart from `User` are updated. The username itself is immutable
and cannot be modified. All the other fields, excluding the e-mail, are the
ones that can be seen in the public page.
""".
-spec update(User, Name, Email, Desc) -> ok | {error, Reason} when
User :: binary(),
Name :: binary(),
Email :: binary(),
Desc :: binary(),
Reason :: term().
update(User, Name, Email, Desc) ->
{ok, CurData, Metadata} = case storage:read(?USERSBUCK, User) of
{ok, [R]} ->
{ok, R#object.value, R#object.metadata};
{error, Reason} -> {error, Reason}
end,
Data = CurData#{<<"email">> => Email, <<"name">> => Name,
<<"description">> => Desc},
storage:write(?USERSBUCK, User, Data, Metadata).
-doc """
Delete an existing user from the database.
```
-spec delete(User) -> ok | {error, Reason} when
User :: binary(),
Reason :: term().
```
""".
-spec delete(User) -> ok | {error, Reason} when
User :: binary(),
Reason :: term().
delete(User) ->
% We are missing the cleanup of the cookies
% here. For that, we need to add at least another
% API to the storage layer.
storage:delete(?USERSBUCK, User).
-doc """
Get the authentication cookies from a cowboy request.
Spec:
```
-spec auth_cookies(Req) -> {User, Cookie} when
Req :: cowboy_req:req(),
User :: binary(),
Cookie :: binary().
```
""".
-spec auth_cookies(Req) -> {User, Cookie} when
Req :: cowboy_req:req(),
User :: binary(),
Cookie :: binary().
auth_cookies(Req) ->
#{?DUDEAUTH := Cookie, ?DUDENAME := User} = cowboy_req:match_cookies([?DUDEAUTH,
?DUDENAME], Req),
{User, Cookie}.
-doc """
Invalidate the cookies in the passed request.
Spec:
```
-spec invalidate_cookies(Req) -> Req0 when
Req :: cowboy_req:req(),
Req0 :: cowboy_req:req().
```
A new request `Req0` is returned to the caller with the cookies zeroed and
completely invalidated.
""".
-spec invalidate_cookies(Req) -> Req0 when
Req :: cowboy_req:req(),
Req0 :: cowboy_req:req().
invalidate_cookies(Req) ->
Req0 = cowboy_req:set_resp_cookie(<<"?DUDEAUTH">>, <<"">>, Req,
#{max_age => 0}),
Req1 = cowboy_req:set_resp_cookie(<<"?DUDENAME">>, <<"">>, Req0,
#{max_age => 0}),
Req1.
-doc """
Set the authentication cookies for the provided client request
Spec:
```
-spec set_auth_cookies(Req, User, Cookie, Validity) -> Req0 when
Req :: cowboy_req:req(),
User :: binary(),
Cookie :: binary(),
Validity :: pos_integer(),
Req0 :: cowboy_req:req().
```
A new request object `Req0`is returned, with the user and auth cookies set.
""".
-spec set_auth_cookies(Req, User, Cookie, Validity) -> Req0 when
Req :: cowboy_req:req(),
User :: binary(),
Cookie :: binary(),
Validity :: pos_integer(),
Req0 :: cowboy_req:req().
set_auth_cookies(Req, User, Cookie, Validity) ->
Req0 = cowboy_req:set_resp_cookie(<<"?DUDEAUTH">>, Cookie, Req,
#{max_age => Validity}),
Req1 = cowboy_req:set_resp_cookie(<<"?DUDENAME">>, User, Req0,
#{max_age => Validity}),
Req1.
-doc """
Spec:
```
-spec read_login_data(Req) -> {User, Pass, Req0} when
Req :: cowboy_req:req(),
User :: binary(),
Pass :: binary(),
Req0 :: cowboy_req:req().
```
Read the login details from the `Req` body and return `User` and `Password`.
""".
-spec read_login_data(Req) -> {User, Pass, Req0} when
Req :: cowboy_req:req(),
User :: binary(),
Pass :: binary(),
Req0 :: cowboy_req:req().
read_login_data(Req) ->
{ok, Data, Req0} = cowboy_req:read_body(Req),
#{<<"user">> := User, <<"password">> := Pass} = json:decode(Data),
{User, Pass, Req0}.
-doc """
Read new registration informations from the request
Spec:
```
-spec read_new_user_data(Req) -> {User, Pass, Email Req0} when
Req :: cowboy_req:req(),
User :: binary(),
Pass :: binary(),
Email :: binary(),
Req0 :: cowboy_req:req().
```
""".
-spec read_new_user_data(Req) -> {User, Pass, Email, Req0} when
Req :: cowboy_req:req(),
User :: binary(),
Pass :: binary(),
Email :: binary(),
Req0 :: cowboy_req:req().
read_new_user_data(Req) ->
{ok, Data, Req0} = cowboy_req:read_body(Req),
#{<<"user">> := User, <<"password">> := Pass,
<<"email">> := Email} = json:decode(Data),
{User, Pass, Email, Req0}.
-doc """
Update user informations.
Spec:
```
-spec read_update_user_data(Req) -> {Email, Desc, Name, Req0} when
Req :: cowboy_req:req(),
Email :: binary(),
Desc :: binary(),
Name :: binary(),
Req0 :: cowboy_req:req().
```
""".
-spec read_update_user_data(Req) -> {Email, Desc, Name, Req0} when
Req :: cowboy_req:req(),
Email :: binary(),
Desc :: binary(),
Name :: binary(),
Req0 :: cowboy_req:req().
read_update_user_data(Req) ->
{ok, Data, Req0} = cowboy_req:read_body(Req),
#{<<"email">> := Email, <<"description">> := Desc,
<<"name">> := Name} = json:decode(Data),
{Email, Desc, Name, Req0}.

View File

@ -23,6 +23,10 @@
% Module callbacks % Module callbacks
-export([init/1, handle_call/3, handle_cast/2, terminate/2]). -export([init/1, handle_call/3, handle_cast/2, terminate/2]).
% Public API exports
-export([auth/3, logout/2, user_details/1, new_user/3,
update_user/4, delete_user/1]).
% %
% Startup functions % Startup functions
% %
@ -35,11 +39,86 @@ init([]) ->
{ok, 0}. {ok, 0}.
%
% Public APIs
%
auth(cookie, User, Cookie) ->
gen_server:call({local, ?MODULE}, {cookie, User, Cookie});
auth(password, User, Password) ->
gen_server:call({local, ?MODULE}, {password, User, Password}).
logout(User, Cookie) ->
gen_server:call({local, ?MODULE}, {logout, User, Cookie}).
user_details(User) ->
gen_server:call({local, ?MODULE}, {user_details, User}).
new_user(User, Password, Email) ->
gen_server:call({local, ?MODULE}, {new_user, User, Password, Email}).
update_user(User, Name, Email, Desc) ->
gen_server:call({local, ?MODULE}, {update_user, User, Name, Email, Desc}).
delete_user(User) ->
gen_server:call({local, ?MODULE}, {delete_user, User}).
% %
% Callbacks % Callbacks
% %
handle_call(_Msg, _From, State) -> {noreply, State}. handle_call({cookie, User, Cookie}, _From, State) ->
case dudeswave_backend_auth:authenticate(cookie, User, Cookie) of
true ->
{reply, true, State};
false ->
{reply, false, State};
{error, Reason} ->
{reply, {error, Reason}, State}
end;
handle_call({password, User, Password}, _From, State) ->
case dudeswave_backend_auth:authenticate(password, User, Password) of
{true, Cookie, Validity} ->
{reply, {true, Cookie, Validity}, State};
false ->
{reply, false, State};
{error, Reason} ->
{reply, {error, Reason}, State}
end;
handle_call({logout, User, Cookie}, _From, State) ->
case dudeswave_backend_auth:logout(User, Cookie) of
ok -> {reply, ok, State};
{error, Reason} -> {reply, {error, Reason}, State}
end;
handle_call({user_details, User}, _From, State) ->
case dudeswave_backend_user:details(User) of
{error, not_found} -> {reply, not_found, State};
{error, Reason} -> {reply, {error, Reason}, State};
Val -> {reply, Val, State}
end;
handle_call({new_user, User, Password, Email}, _From, State) ->
case dudeswave_backend_user:new(User, Password, Email) of
ok -> {reply, ok, State};
{error, Reason} -> {reply, {error, Reason}, State}
end;
handle_call({update_user, User, Name, Email, Desc}, _From, State) ->
case dudeswave_backend_user:update(User, Name, Email, Desc) of
ok -> {reply, ok, State};
{error, Reason} -> {reply, {error, Reason}, State}
end;
handle_call({delete_user, User}, _From, State) ->
case dudeswave_backend_user:delete(User) of
ok -> {reply, ok, State};
{error, Reason} -> {reply, {error, Reason}, State}
end.
handle_cast(_Msg, State) -> {noreply, State}. handle_cast(_Msg, State) -> {noreply, State}.

View File

@ -20,10 +20,10 @@ Dudes authentication module
Here lives all the functions for the APIs needed to handle users authentication. Here lives all the functions for the APIs needed to handle users authentication.
""". """.
-include_lib("dudeswave/include/defines.hrl"). -include_lib("dudeswave_backend/include/defines.hrl").
-include_lib("storage/include/storage.hrl"). -include_lib("storage/include/storage.hrl").
-export([authenticate/2, logout/2]). -export([authenticate/3, logout/2]).
-doc """ -doc """
Verify a session with an existing cookie. Verify a session with an existing cookie.
@ -31,9 +31,12 @@ Verify a session with an existing cookie.
Spec: Spec:
``` ```
-spec authenticate(User, Auth) -> true | false | {true, Cookie, Validity} | {error, Reason} when -spec authenticate(Type, User, Auth) -> true | false | {true, Cookie, Validity} | {error, Reason} when
Type :: cookie | password,
User :: binary(), User :: binary(),
Auth :: {cookie, binary()} | {password, binary()}, Auth :: {cookie, binary()} | {password, binary()},
Cookie :: binary(),
Validity :: pos_integer(),
Reason :: term(). Reason :: term().
``` ```
@ -43,14 +46,15 @@ after authenticating with `Password`.
If `Cookie` is valid, the function returns `true`. If the authentication is denied If `Cookie` is valid, the function returns `true`. If the authentication is denied
returns `false` returns `false`
""". """.
-spec authenticate(User, Auth) -> true | false | {true, Cookie, Validity} | {error, Reason} when -spec authenticate(Type, User, Auth) -> true | false | {true, Cookie, Validity} | {error, Reason} when
Type :: cookie | password,
User :: binary(), User :: binary(),
Auth :: {cookie, binary()} | {password, binary()}, Auth :: {cookie, binary()} | {password, binary()},
Cookie :: binary(), Cookie :: binary(),
Validity :: pos_integer(), Validity :: pos_integer(),
Reason :: term(). Reason :: term().
authenticate(User, {cookie, Cookie}) -> authenticate(cookie, User, Cookie) ->
case storage:read(?COOKIESBUCK, Cookie) of case storage:read(?COOKIESBUCK, Cookie) of
{ok, [R]} -> {ok, [R]} ->
CurTime = calendar:now_to_universal_time(erlang:timestamp()), CurTime = calendar:now_to_universal_time(erlang:timestamp()),
@ -69,7 +73,7 @@ authenticate(User, {cookie, Cookie}) ->
{error, _} -> {error, service_unavailable} {error, _} -> {error, service_unavailable}
end; end;
authenticate(User, {password, Password}) -> authenticate(password, User, Password) ->
case storage:read(?USERSBUCK, User) of case storage:read(?USERSBUCK, User) of
{ok, [R]} -> {ok, [R]} ->
Validity = case application:get_env(cookie_validity) of Validity = case application:get_env(cookie_validity) of

View File

@ -15,7 +15,7 @@
% %
-module(dudeswave_backend_user). -module(dudeswave_backend_user).
-include_lib("dudeswave/include/defines.hrl"). -include_lib("dudeswave_backend/include/defines.hrl").
-include_lib("storage/include/storage.hrl"). -include_lib("storage/include/storage.hrl").
-export([details/1, new/3, update/4, delete/1]). -export([details/1, new/3, update/4, delete/1]).

View File

@ -1,12 +1,12 @@
{application,dudeswave, {application,dudeswave_web,
[{description,"The dudeswave web experience"}, [{description,"The dudeswave web experience"},
{vsn,"1.0.0"}, {vsn,"1.0.0"},
{modules,[dudeswave,dudeswave_app,dudeswave_handler, {modules,[dudeswave_web,dudeswave_web_app,dudeswave_web_handler,
dudeswave_user_handler,dudeswave_supervisor, dudeswave_web_user_handler,dudeswave_web_supervisor,
dudeswave_auth,dudeswave_auth_handler]}, dudeswave_web_auth_handler]},
{registered,[]}, {registered,[]},
{applications,[kernel,stdlib,erts,cowboy,ranch]}, {applications,[kernel,stdlib,erts,cowboy,ranch]},
{mod,{dudeswave_app,[]}}, {mod,{dudeswave_web_app,[]}},
{env, [ {env, [
{ip,"127.0.0.1"}, {ip,"127.0.0.1"},
{port,8080} {port,8080}

View File

@ -0,0 +1,19 @@
.PHONY: all clean
.SUFFIXES: .erl .beam
ERLC?= erlc -server
ERLFLAGS= -I ../../
OBJS= dudeswave_web.beam dudeswave_web_app.beam
OBJS+= dudeswave_web_supervisor.beam dudeswave_web_handler.beam
OBJS+= dudeswave_web_user_handler.beam dudeswave_web_common.beam
all: ${OBJS}
.erl.beam:
${ERLC} ${ERLOPTS} ${ERLFLAGS} $<
clean:
rm -f *.beam

View File

@ -13,7 +13,7 @@
% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF % ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. % OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
% %
-module(dudeswave). -module(dudeswave_web).
-behaviour(gen_server). -behaviour(gen_server).

View File

@ -13,14 +13,12 @@
% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF % ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. % OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
% %
-module(dudeswave_app). -module(dudeswave_web_app).
-behaviour(application). -behaviour(application).
-export([bootstrap/3, start/2, stop/1]). -export([bootstrap/3, start/2, stop/1]).
start(_Type, StartArgs) -> start(_Type, StartArgs) ->
crypto:rand_seed(),
{ok, Addr} = case application:get_env(ip) of {ok, Addr} = case application:get_env(ip) of
{ok, AddrConf} -> inet:parse_address(AddrConf); {ok, AddrConf} -> inet:parse_address(AddrConf);
undefined -> undefined undefined -> undefined

View File

@ -94,9 +94,9 @@ forbidden(Req, State) ->
<<"POST">> -> <<"POST">> ->
{false, Req, State}; {false, Req, State};
_ -> _ ->
{User, Auth} = dudeswave_auth:auth_cookies(Req), {User, Auth} = dudeswave_web_common:auth_cookies(Req),
case dudeswave_auth:authenticate(User, {cookie, Auth}) of case dudeswave_backend:auth(cookie, User, Cookie) of
{error, service_unavailable} -> exit(service_unavailable); {error, service_unavailable} -> exit(service_unavailable);
true -> {false, Req, State}; true -> {false, Req, State};
false -> {true, Req, State} false -> {true, Req, State}
@ -112,9 +112,9 @@ content_types_accepted(Req, State) ->
end. end.
resource_exists(Req, State) -> resource_exists(Req, State) ->
{User, _} = dudeswave_auth:auth_cookies(Req), {User, _} = dudeswave_web_common:auth_cookies(Req),
case dudeswave_auth:details(User) of case dudeswave_backend:user_details(User) of
[] -> [] ->
{false, Req, State}; {false, Req, State};
{error, Reason} -> {error, Reason} ->
@ -135,11 +135,11 @@ is_conflict(Req, State) -> {true, Req, State}.
allow_missing_post(Req, State) -> {false, Req, State}. allow_missing_post(Req, State) -> {false, Req, State}.
delete_resource(Req, State) -> delete_resource(Req, State) ->
{User, Auth} = dudeswave_auth:auth_cookies(Req), {User, Auth} = dudeswave_web_common:auth_cookies(Req),
case dudeswave_auth:logout(User, Auth) of case dudeswave_backend:logout(User, Auth) of
ok -> ok ->
{true, dudeswave_auth:invalidate_cookies(Req), State}; {true, invalidate_cookies(Req), State};
{error, _} -> {error, _} ->
{false, Req, State} {false, Req, State}
end. end.
@ -151,11 +151,11 @@ delete_completed(Req, State) -> {false, Req, State}.
% %
login(Req, State) -> login(Req, State) ->
{User, Pass, Req0} = dudeswave_auth:read_login_data(Req), {User, Pass, Req0} = read_login_data(Req),
case dudeswave_auth:authenticate(User, {password, Pass}) of case dudeswave_backend:auth(password, User, Pass) of
{true, Cookie, Validity} -> {true, Cookie, Validity} ->
{true, dudeswave_auth:set_auth_cookies(Req, User, Cookie, Validity), State}; {true, set_auth_cookies(Req, User, Cookie, Validity), State};
false -> false ->
{false, Req0, State}; {false, Req0, State};
{error, _} -> {error, _} ->
@ -170,3 +170,88 @@ logout(Req, State) -> {ok, Req, State}.
% %
terminate(_Reason, _Req, _State) -> ok. terminate(_Reason, _Req, _State) -> ok.
%
% Private functions
%
-doc """
Invalidate the cookies in the passed request.
Spec:
```
-spec invalidate_cookies(Req) -> Req0 when
Req :: cowboy_req:req(),
Req0 :: cowboy_req:req().
```
A new request `Req0` is returned to the caller with the cookies zeroed and
completely invalidated.
""".
-spec invalidate_cookies(Req) -> Req0 when
Req :: cowboy_req:req(),
Req0 :: cowboy_req:req().
invalidate_cookies(Req) ->
Req0 = cowboy_req:set_resp_cookie(<<"?DUDEAUTH">>, <<"">>, Req,
#{max_age => 0}),
Req1 = cowboy_req:set_resp_cookie(<<"?DUDENAME">>, <<"">>, Req0,
#{max_age => 0}),
Req1.
-doc """
Spec:
```
-spec read_login_data(Req) -> {User, Pass, Req0} when
Req :: cowboy_req:req(),
User :: binary(),
Pass :: binary(),
Req0 :: cowboy_req:req().
```
Read the login details from the `Req` body and return `User` and `Password`.
""".
-spec read_login_data(Req) -> {User, Pass, Req0} when
Req :: cowboy_req:req(),
User :: binary(),
Pass :: binary(),
Req0 :: cowboy_req:req().
read_login_data(Req) ->
{ok, Data, Req0} = cowboy_req:read_body(Req),
#{<<"user">> := User, <<"password">> := Pass} = json:decode(Data),
{User, Pass, Req0}.
-doc """
Set the authentication cookies for the provided client request
Spec:
```
-spec set_auth_cookies(Req, User, Cookie, Validity) -> Req0 when
Req :: cowboy_req:req(),
User :: binary(),
Cookie :: binary(),
Validity :: pos_integer(),
Req0 :: cowboy_req:req().
```
A new request object `Req0`is returned, with the user and auth cookies set.
""".
-spec set_auth_cookies(Req, User, Cookie, Validity) -> Req0 when
Req :: cowboy_req:req(),
User :: binary(),
Cookie :: binary(),
Validity :: pos_integer(),
Req0 :: cowboy_req:req().
set_auth_cookies(Req, User, Cookie, Validity) ->
Req0 = cowboy_req:set_resp_cookie(<<"?DUDEAUTH">>, Cookie, Req,
#{max_age => Validity}),
Req1 = cowboy_req:set_resp_cookie(<<"?DUDENAME">>, User, Req0,
#{max_age => Validity}),
Req1.

View File

@ -13,11 +13,29 @@
% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF % ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. % OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
% %
-module(dudeswave_web_common).
-define(APPBUCK, dudeswave). -export([auth_cookies/1]).
-define(USERSBUCK, dudes).
-define(COOKIESBUCK, cookies). -doc """
-define(RANDBYTES, 32). Get the authentication cookies from a cowboy request.
-define(DEFVALIDITY, 365).
-define(DUDENAME, "dudename"). Spec:
-define(DUDEAUTH, "dudeauth").
```
-spec auth_cookies(Req) -> {User, Cookie} when
Req :: cowboy_req:req(),
User :: binary(),
Cookie :: binary().
```
""".
-spec auth_cookies(Req) -> {User, Cookie} when
Req :: cowboy_req:req(),
User :: binary(),
Cookie :: binary().
auth_cookies(Req) ->
#{?DUDEAUTH := Cookie} = cowboy_req:match_cookies([?DUDEAUTH], Req),
#{?DUDENAME := User} = cowboy_req:match_cookies([?DUDENAME], Req),
{User, Cookie}.

View File

@ -13,7 +13,7 @@
% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF % ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. % OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
% %
-module(dudeswave_handler). -module(dudeswave_web_handler).
-moduledoc """ -moduledoc """
The dudeswave GET handler. The dudeswave GET handler.

View File

@ -13,7 +13,7 @@
% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF % ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. % OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
% %
-module(dudeswave_supervisor). -module(dudeswave_web_supervisor).
-behaviour(supervisor). -behaviour(supervisor).
-export([start/0, -export([start/0,

View File

@ -13,7 +13,7 @@
% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF % ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. % OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
% %
-module(dudeswave_user_handler). -module(dudeswave_web_user_handler).
-moduledoc """ -moduledoc """
JSON API to manage users. JSON API to manage users.
@ -138,9 +138,9 @@ forbidden(Req, State) ->
<<"PUT">> -> <<"PUT">> ->
{false, Req, State}; {false, Req, State};
_ -> _ ->
{User, Auth} = dudeswave_auth:auth_cookies(Req), {User, Auth} = dudeswave_web_common:auth_cookies(Req),
case dudeswave_auth:authenticate(User, {cookie, Auth}) of case dudeswave_backend:auth(cookie, User, Auth) of
{error, service_unavailable} -> {true, Req, State}; {error, service_unavailable} -> {true, Req, State};
true -> {false, Req, State}; true -> {false, Req, State};
false -> {true, Req, State} false -> {true, Req, State}
@ -168,9 +168,9 @@ content_types_accepted(Req, State) ->
end. end.
resource_exists(Req, State) -> resource_exists(Req, State) ->
{User, _} = dudeswave_auth:auth_cookies(Req), {User, _} = dudeswave_web_common:auth_cookies(Req),
case dudeswave_auth:details(User) of case dudeswave_backend:user_details(User) of
[] -> [] ->
{false, Req, State}; {false, Req, State};
{error, _} -> {error, _} ->
@ -194,9 +194,9 @@ is_conflict(Req, State) -> {false, Req, State}.
allow_missing_post(Req, State) -> {false, Req, State}. allow_missing_post(Req, State) -> {false, Req, State}.
delete_resource(Req, State) -> delete_resource(Req, State) ->
{User, _} = dudeswave_auth:auth_cookies(Req), {User, _} = dudeswave_web_common:auth_cookies(Req),
case dudeswave_auth:delete(User) of case dudeswave_backend:delete_user(User) of
ok -> {true, Req, State}; ok -> {true, Req, State};
{error, _} -> {false, Req, State} {error, _} -> {false, Req, State}
end. end.
@ -208,31 +208,85 @@ delete_completed(Req, State) -> {true, Req, State}.
% %
create_user(Req, State) -> create_user(Req, State) ->
{User, Pass, Email, Req0} = dudeswave_auth:read_new_user_data(Req), {User, Pass, Email, Req0} = read_new_user_data(Req),
case dudeswave_auth:new(User, Pass, Email) of case dudeswave_backend:new_user(User, Pass, Email) of
ok -> {true, Req0, []}; ok -> {true, Req0, []};
{error, _} -> {false, Req0, State} {error, _} -> {false, Req0, State}
end. end.
modify_user(Req, State) -> modify_user(Req, State) ->
{User, _} = dudeswave_auth:auth_cookies(Req), {User, _} = dudeswave_web_common:auth_cookies(Req),
{Email, Desc, Name, Req0} = dudeswave_auth:read_update_user_data(Req), {Email, Desc, Name, Req0} = read_update_user_data(Req),
case dudeswave_auth:update(User, Name, Email, Desc) of case dudeswave_backend:update_user(User, Name, Email, Desc) of
ok -> {true, Req0, []}; ok -> {true, Req0, []};
{error, _} -> {false, Req0, State} {error, _} -> {false, Req0, State}
end. end.
user_details(Req, State) -> user_details(Req, State) ->
{User, _} = dudeswave_auth:auth_cookies(Req), {User, _} = dudeswave_web_common:auth_cookies(Req),
#{details := Details} = State, #{details := Details} = State,
Data = Details#{user => User}, Data = Details#{user => User},
{iolist_to_binary(json:encode(Data)), Req, State}. {iolist_to_binary(json:encode(Data)), Req, State}.
% %
% gen_server callbacks % Private functions
% %
terminate(_Reason, _Req, _State) -> ok. -doc """
Update user informations.
Spec:
```
-spec read_update_user_data(Req) -> {Email, Desc, Name, Req0} when
Req :: cowboy_req:req(),
Email :: binary(),
Desc :: binary(),
Name :: binary(),
Req0 :: cowboy_req:req().
```
""".
-spec read_update_user_data(Req) -> {Email, Desc, Name, Req0} when
Req :: cowboy_req:req(),
Email :: binary(),
Desc :: binary(),
Name :: binary(),
Req0 :: cowboy_req:req().
read_update_user_data(Req) ->
{ok, Data, Req0} = cowboy_req:read_body(Req),
#{<<"email">> := Email, <<"description">> := Desc,
<<"name">> := Name} = json:decode(Data),
{Email, Desc, Name, Req0}.
-doc """
Read new registration informations from the request
Spec:
```
-spec read_new_user_data(Req) -> {User, Pass, Email Req0} when
Req :: cowboy_req:req(),
User :: binary(),
Pass :: binary(),
Email :: binary(),
Req0 :: cowboy_req:req().
```
""".
-spec read_new_user_data(Req) -> {User, Pass, Email, Req0} when
Req :: cowboy_req:req(),
User :: binary(),
Pass :: binary(),
Email :: binary(),
Req0 :: cowboy_req:req().
read_new_user_data(Req) ->
{ok, Data, Req0} = cowboy_req:read_body(Req),
#{<<"user">> := User, <<"password">> := Pass,
<<"email">> := Email} = json:decode(Data),
{User, Pass, Email, Req0}.