From 31da1067da756f6c6b3f46904342f4265f11da26 Mon Sep 17 00:00:00 2001 From: absc Date: Mon, 9 Sep 2024 20:14:18 +0000 Subject: [PATCH] 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. --- dudeswave_backend/Makefile | 7 + dudeswave_backend/ebin/dudeswave_backend.app | 10 ++ dudeswave_backend/include/defines.hrl | 23 +++ dudeswave_backend/src/Makefile | 19 +++ dudeswave_backend/src/dudeswave_backend.erl | 46 ++++++ .../src/dudeswave_backend_app.erl | 31 ++++ .../src/dudeswave_backend_auth.erl | 131 +++++++++++++++++ .../src/dudeswave_backend_supervisor.erl | 31 ++++ .../src/dudeswave_backend_user.erl | 134 ++++++++++++++++++ 9 files changed, 432 insertions(+) create mode 100644 dudeswave_backend/Makefile create mode 100644 dudeswave_backend/ebin/dudeswave_backend.app create mode 100644 dudeswave_backend/include/defines.hrl create mode 100644 dudeswave_backend/src/Makefile create mode 100644 dudeswave_backend/src/dudeswave_backend.erl create mode 100644 dudeswave_backend/src/dudeswave_backend_app.erl create mode 100644 dudeswave_backend/src/dudeswave_backend_auth.erl create mode 100644 dudeswave_backend/src/dudeswave_backend_supervisor.erl create mode 100644 dudeswave_backend/src/dudeswave_backend_user.erl diff --git a/dudeswave_backend/Makefile b/dudeswave_backend/Makefile new file mode 100644 index 0000000..160022d --- /dev/null +++ b/dudeswave_backend/Makefile @@ -0,0 +1,7 @@ +.PHONY: all clean + +all: + ${MAKE} -C src + +clean: + ${MAKE} -C src clean diff --git a/dudeswave_backend/ebin/dudeswave_backend.app b/dudeswave_backend/ebin/dudeswave_backend.app new file mode 100644 index 0000000..8e59887 --- /dev/null +++ b/dudeswave_backend/ebin/dudeswave_backend.app @@ -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,[]}]}. diff --git a/dudeswave_backend/include/defines.hrl b/dudeswave_backend/include/defines.hrl new file mode 100644 index 0000000..48c12ba --- /dev/null +++ b/dudeswave_backend/include/defines.hrl @@ -0,0 +1,23 @@ +% +% Copyright (c) 2024 Andrea Biscuola +% +% 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"). diff --git a/dudeswave_backend/src/Makefile b/dudeswave_backend/src/Makefile new file mode 100644 index 0000000..8dab13c --- /dev/null +++ b/dudeswave_backend/src/Makefile @@ -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 + diff --git a/dudeswave_backend/src/dudeswave_backend.erl b/dudeswave_backend/src/dudeswave_backend.erl new file mode 100644 index 0000000..399df0b --- /dev/null +++ b/dudeswave_backend/src/dudeswave_backend.erl @@ -0,0 +1,46 @@ +% +% Copyright (c) 2024 Andrea Biscuola +% +% 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. diff --git a/dudeswave_backend/src/dudeswave_backend_app.erl b/dudeswave_backend/src/dudeswave_backend_app.erl new file mode 100644 index 0000000..236581b --- /dev/null +++ b/dudeswave_backend/src/dudeswave_backend_app.erl @@ -0,0 +1,31 @@ +% +% Copyright (c) 2024 Andrea Biscuola +% +% 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. diff --git a/dudeswave_backend/src/dudeswave_backend_auth.erl b/dudeswave_backend/src/dudeswave_backend_auth.erl new file mode 100644 index 0000000..4164fec --- /dev/null +++ b/dudeswave_backend/src/dudeswave_backend_auth.erl @@ -0,0 +1,131 @@ +% +% Copyright (c) 2024 Andrea Biscuola +% +% 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, <>), + + if + Appr =/= true -> false; + Auth =:= Hash -> + Cookie = base64:encode(rand:bytes(64)), + case storage:write(?COOKIESBUCK, <>, + 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. diff --git a/dudeswave_backend/src/dudeswave_backend_supervisor.erl b/dudeswave_backend/src/dudeswave_backend_supervisor.erl new file mode 100644 index 0000000..3229779 --- /dev/null +++ b/dudeswave_backend/src/dudeswave_backend_supervisor.erl @@ -0,0 +1,31 @@ +% +% Copyright (c) 2024 Andrea Biscuola +% +% 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]}]}}. \ No newline at end of file diff --git a/dudeswave_backend/src/dudeswave_backend_user.erl b/dudeswave_backend/src/dudeswave_backend_user.erl new file mode 100644 index 0000000..699f642 --- /dev/null +++ b/dudeswave_backend/src/dudeswave_backend_user.erl @@ -0,0 +1,134 @@ +% +% Copyright (c) 2024 Andrea Biscuola +% +% 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, <>), + + 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).