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
parent
31da1067da
commit
eebf62d527
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -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
|
||||
|
|
@ -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}.
|
|
@ -23,6 +23,10 @@
|
|||
% Module callbacks
|
||||
-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
|
||||
%
|
||||
|
@ -35,11 +39,86 @@ init([]) ->
|
|||
|
||||
{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
|
||||
%
|
||||
|
||||
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}.
|
||||
|
||||
|
|
|
@ -20,10 +20,10 @@ Dudes authentication module
|
|||
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").
|
||||
|
||||
-export([authenticate/2, logout/2]).
|
||||
-export([authenticate/3, logout/2]).
|
||||
|
||||
-doc """
|
||||
Verify a session with an existing cookie.
|
||||
|
@ -31,9 +31,12 @@ Verify a session with an existing cookie.
|
|||
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(),
|
||||
Auth :: {cookie, binary()} | {password, binary()},
|
||||
Cookie :: binary(),
|
||||
Validity :: pos_integer(),
|
||||
Reason :: term().
|
||||
```
|
||||
|
||||
|
@ -43,14 +46,15 @@ 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
|
||||
-spec authenticate(Type, User, Auth) -> true | false | {true, Cookie, Validity} | {error, Reason} when
|
||||
Type :: cookie | password,
|
||||
User :: binary(),
|
||||
Auth :: {cookie, binary()} | {password, binary()},
|
||||
Cookie :: binary(),
|
||||
Validity :: pos_integer(),
|
||||
Reason :: term().
|
||||
|
||||
authenticate(User, {cookie, Cookie}) ->
|
||||
authenticate(cookie, User, Cookie) ->
|
||||
case storage:read(?COOKIESBUCK, Cookie) of
|
||||
{ok, [R]} ->
|
||||
CurTime = calendar:now_to_universal_time(erlang:timestamp()),
|
||||
|
@ -69,7 +73,7 @@ authenticate(User, {cookie, Cookie}) ->
|
|||
{error, _} -> {error, service_unavailable}
|
||||
end;
|
||||
|
||||
authenticate(User, {password, Password}) ->
|
||||
authenticate(password, User, Password) ->
|
||||
case storage:read(?USERSBUCK, User) of
|
||||
{ok, [R]} ->
|
||||
Validity = case application:get_env(cookie_validity) of
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
%
|
||||
-module(dudeswave_backend_user).
|
||||
|
||||
-include_lib("dudeswave/include/defines.hrl").
|
||||
-include_lib("dudeswave_backend/include/defines.hrl").
|
||||
-include_lib("storage/include/storage.hrl").
|
||||
|
||||
-export([details/1, new/3, update/4, delete/1]).
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
{application,dudeswave,
|
||||
{application,dudeswave_web,
|
||||
[{description,"The dudeswave web experience"},
|
||||
{vsn,"1.0.0"},
|
||||
{modules,[dudeswave,dudeswave_app,dudeswave_handler,
|
||||
dudeswave_user_handler,dudeswave_supervisor,
|
||||
dudeswave_auth,dudeswave_auth_handler]},
|
||||
{modules,[dudeswave_web,dudeswave_web_app,dudeswave_web_handler,
|
||||
dudeswave_web_user_handler,dudeswave_web_supervisor,
|
||||
dudeswave_web_auth_handler]},
|
||||
{registered,[]},
|
||||
{applications,[kernel,stdlib,erts,cowboy,ranch]},
|
||||
{mod,{dudeswave_app,[]}},
|
||||
{mod,{dudeswave_web_app,[]}},
|
||||
{env, [
|
||||
{ip,"127.0.0.1"},
|
||||
{port,8080}
|
|
@ -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
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
%
|
||||
-module(dudeswave).
|
||||
-module(dudeswave_web).
|
||||
|
||||
-behaviour(gen_server).
|
||||
|
|
@ -13,14 +13,12 @@
|
|||
% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
%
|
||||
-module(dudeswave_app).
|
||||
-module(dudeswave_web_app).
|
||||
-behaviour(application).
|
||||
|
||||
-export([bootstrap/3, start/2, stop/1]).
|
||||
|
||||
start(_Type, StartArgs) ->
|
||||
crypto:rand_seed(),
|
||||
|
||||
{ok, Addr} = case application:get_env(ip) of
|
||||
{ok, AddrConf} -> inet:parse_address(AddrConf);
|
||||
undefined -> undefined
|
|
@ -94,9 +94,9 @@ forbidden(Req, State) ->
|
|||
<<"POST">> ->
|
||||
{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);
|
||||
true -> {false, Req, State};
|
||||
false -> {true, Req, State}
|
||||
|
@ -112,9 +112,9 @@ content_types_accepted(Req, State) ->
|
|||
end.
|
||||
|
||||
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};
|
||||
{error, Reason} ->
|
||||
|
@ -135,11 +135,11 @@ is_conflict(Req, State) -> {true, Req, State}.
|
|||
allow_missing_post(Req, State) -> {false, 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 ->
|
||||
{true, dudeswave_auth:invalidate_cookies(Req), State};
|
||||
{true, invalidate_cookies(Req), State};
|
||||
{error, _} ->
|
||||
{false, Req, State}
|
||||
end.
|
||||
|
@ -151,11 +151,11 @@ delete_completed(Req, State) -> {false, 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, dudeswave_auth:set_auth_cookies(Req, User, Cookie, Validity), State};
|
||||
{true, set_auth_cookies(Req, User, Cookie, Validity), State};
|
||||
false ->
|
||||
{false, Req0, State};
|
||||
{error, _} ->
|
||||
|
@ -170,3 +170,88 @@ logout(Req, State) -> {ok, Req, State}.
|
|||
%
|
||||
|
||||
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.
|
|
@ -13,11 +13,29 @@
|
|||
% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
%
|
||||
-module(dudeswave_web_common).
|
||||
|
||||
-define(APPBUCK, dudeswave).
|
||||
-define(USERSBUCK, dudes).
|
||||
-define(COOKIESBUCK, cookies).
|
||||
-define(RANDBYTES, 32).
|
||||
-define(DEFVALIDITY, 365).
|
||||
-define(DUDENAME, "dudename").
|
||||
-define(DUDEAUTH, "dudeauth").
|
||||
-export([auth_cookies/1]).
|
||||
|
||||
-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} = cowboy_req:match_cookies([?DUDEAUTH], Req),
|
||||
#{?DUDENAME := User} = cowboy_req:match_cookies([?DUDENAME], Req),
|
||||
|
||||
{User, Cookie}.
|
|
@ -13,7 +13,7 @@
|
|||
% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
%
|
||||
-module(dudeswave_handler).
|
||||
-module(dudeswave_web_handler).
|
||||
-moduledoc """
|
||||
The dudeswave GET handler.
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
%
|
||||
-module(dudeswave_supervisor).
|
||||
-module(dudeswave_web_supervisor).
|
||||
-behaviour(supervisor).
|
||||
|
||||
-export([start/0,
|
|
@ -13,7 +13,7 @@
|
|||
% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
%
|
||||
-module(dudeswave_user_handler).
|
||||
-module(dudeswave_web_user_handler).
|
||||
-moduledoc """
|
||||
JSON API to manage users.
|
||||
|
||||
|
@ -138,9 +138,9 @@ forbidden(Req, State) ->
|
|||
<<"PUT">> ->
|
||||
{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};
|
||||
true -> {false, Req, State};
|
||||
false -> {true, Req, State}
|
||||
|
@ -168,9 +168,9 @@ content_types_accepted(Req, State) ->
|
|||
end.
|
||||
|
||||
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};
|
||||
{error, _} ->
|
||||
|
@ -194,9 +194,9 @@ is_conflict(Req, State) -> {false, Req, State}.
|
|||
allow_missing_post(Req, State) -> {false, 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};
|
||||
{error, _} -> {false, Req, State}
|
||||
end.
|
||||
|
@ -208,31 +208,85 @@ delete_completed(Req, State) -> {true, 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, []};
|
||||
{error, _} -> {false, Req0, State}
|
||||
end.
|
||||
|
||||
modify_user(Req, State) ->
|
||||
{User, _} = dudeswave_auth:auth_cookies(Req),
|
||||
{Email, Desc, Name, Req0} = dudeswave_auth:read_update_user_data(Req),
|
||||
{User, _} = dudeswave_web_common:auth_cookies(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, []};
|
||||
{error, _} -> {false, Req0, State}
|
||||
end.
|
||||
|
||||
user_details(Req, State) ->
|
||||
{User, _} = dudeswave_auth:auth_cookies(Req),
|
||||
{User, _} = dudeswave_web_common:auth_cookies(Req),
|
||||
#{details := Details} = State,
|
||||
Data = Details#{user => User},
|
||||
|
||||
{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}.
|
Loading…
Reference in New Issue