Add the backend app.
Better to split the main functions in it's separate application. This will be used by all the other part of the dudeswave experience.main
parent
0e7c173cba
commit
31da1067da
|
@ -0,0 +1,7 @@
|
||||||
|
.PHONY: all clean
|
||||||
|
|
||||||
|
all:
|
||||||
|
${MAKE} -C src
|
||||||
|
|
||||||
|
clean:
|
||||||
|
${MAKE} -C src clean
|
|
@ -0,0 +1,10 @@
|
||||||
|
{application,dudeswave_backend,
|
||||||
|
[{description,"The dudeswave core"},
|
||||||
|
{vsn,"1.0.0"},
|
||||||
|
{modules,[dudeswave_backend,dudeswave_backend_app,
|
||||||
|
,dudeswave_backend_supervisor,
|
||||||
|
dudeswave_backend_auth,dudeswave_backend_user]},
|
||||||
|
{registered,[]},
|
||||||
|
{applications,[kernel,stdlib,erts,cowboy,ranch]},
|
||||||
|
{mod,{dudeswave_backend_app,[]}},
|
||||||
|
{start_phases,[]}]}.
|
|
@ -0,0 +1,23 @@
|
||||||
|
%
|
||||||
|
% 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.
|
||||||
|
%
|
||||||
|
|
||||||
|
-define(APPBUCK, dudeswave).
|
||||||
|
-define(USERSBUCK, dudes).
|
||||||
|
-define(COOKIESBUCK, cookies).
|
||||||
|
-define(RANDBYTES, 32).
|
||||||
|
-define(DEFVALIDITY, 365).
|
||||||
|
-define(DUDENAME, "dudename").
|
||||||
|
-define(DUDEAUTH, "dudeauth").
|
|
@ -0,0 +1,19 @@
|
||||||
|
.PHONY: all clean
|
||||||
|
.SUFFIXES: .erl .beam
|
||||||
|
|
||||||
|
ERLC?= erlc -server
|
||||||
|
|
||||||
|
ERLFLAGS= -I ../../
|
||||||
|
|
||||||
|
OBJS= dudeswave_backend.beam dudeswave_backend_app.beam
|
||||||
|
OBJS+= dudeswave_backend_supervisor.beam
|
||||||
|
OBJS+= dudeswave_backend_auth.beam dudeswave_backend_user.beam
|
||||||
|
|
||||||
|
all: ${OBJS}
|
||||||
|
|
||||||
|
.erl.beam:
|
||||||
|
${ERLC} ${ERLOPTS} ${ERLFLAGS} $<
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f *.beam
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
%
|
||||||
|
% 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_backend).
|
||||||
|
|
||||||
|
-behaviour(gen_server).
|
||||||
|
|
||||||
|
% Server start function
|
||||||
|
-export([start_link/0]).
|
||||||
|
|
||||||
|
% Module callbacks
|
||||||
|
-export([init/1, handle_call/3, handle_cast/2, terminate/2]).
|
||||||
|
|
||||||
|
%
|
||||||
|
% Startup functions
|
||||||
|
%
|
||||||
|
|
||||||
|
start_link() ->
|
||||||
|
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
|
||||||
|
|
||||||
|
init([]) ->
|
||||||
|
process_flag(trap_exit, true),
|
||||||
|
|
||||||
|
{ok, 0}.
|
||||||
|
|
||||||
|
%
|
||||||
|
% Callbacks
|
||||||
|
%
|
||||||
|
|
||||||
|
handle_call(_Msg, _From, State) -> {noreply, State}.
|
||||||
|
|
||||||
|
handle_cast(_Msg, State) -> {noreply, State}.
|
||||||
|
|
||||||
|
terminate(_Reason, _N) -> ok.
|
|
@ -0,0 +1,31 @@
|
||||||
|
%
|
||||||
|
% 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_backend_app).
|
||||||
|
-behaviour(application).
|
||||||
|
|
||||||
|
-export([bootstrap/3, start/2, stop/1]).
|
||||||
|
|
||||||
|
start(_Type, StartArgs) ->
|
||||||
|
crypto:rand_seed(),
|
||||||
|
|
||||||
|
dudeswave_backend_supervisor:start_link(StartArgs).
|
||||||
|
|
||||||
|
stop(_State) -> ok.
|
||||||
|
|
||||||
|
%
|
||||||
|
% Bootstrap procedure to be completed
|
||||||
|
%
|
||||||
|
bootstrap(_Admin, _Password, _Nodes) -> ok.
|
|
@ -0,0 +1,131 @@
|
||||||
|
%
|
||||||
|
% 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_backend_auth).
|
||||||
|
-moduledoc """
|
||||||
|
Dudes authentication module
|
||||||
|
|
||||||
|
Here lives all the functions for the APIs needed to handle users authentication.
|
||||||
|
""".
|
||||||
|
|
||||||
|
-include_lib("dudeswave/include/defines.hrl").
|
||||||
|
-include_lib("storage/include/storage.hrl").
|
||||||
|
|
||||||
|
-export([authenticate/2, logout/2]).
|
||||||
|
|
||||||
|
-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.
|
|
@ -0,0 +1,31 @@
|
||||||
|
%
|
||||||
|
% 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_backend_supervisor).
|
||||||
|
-behaviour(supervisor).
|
||||||
|
|
||||||
|
-export([start/0,
|
||||||
|
start_link/1,
|
||||||
|
init/1]).
|
||||||
|
|
||||||
|
start() ->
|
||||||
|
spawn(fun() -> supervisor:start_link({local, ?MODULE}, ?MODULE, _Arg = []) end).
|
||||||
|
|
||||||
|
start_link(Args) ->
|
||||||
|
supervisor:start_link({local, ?MODULE}, ?MODULE, Args).
|
||||||
|
|
||||||
|
init([]) ->
|
||||||
|
{ok, {{one_for_one, 3, 10}, [{tag1, {dudeswave_backend, start_link, []}, permanent,
|
||||||
|
10000, worker, [dudeswave_backend]}]}}.
|
|
@ -0,0 +1,134 @@
|
||||||
|
%
|
||||||
|
% 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_backend_user).
|
||||||
|
|
||||||
|
-include_lib("dudeswave/include/defines.hrl").
|
||||||
|
-include_lib("storage/include/storage.hrl").
|
||||||
|
|
||||||
|
-export([details/1, new/3, update/4, delete/1]).
|
||||||
|
|
||||||
|
-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).
|
Loading…
Reference in New Issue