src/nova_session.erl

%%% @author Niclas Axelsson <niclas@burbas.se>
%%% @doc
%%% All kind of operations on sessions is handled by the nova_session-module. The module also
%%% presents a behaviour that can be used to create customized backends for the session-data.
%%%
%%% @end
-module(nova_session).
-export([
         get/2,
         set/3,
         delete/1,
         delete/2,
         generate_session_id/0
        ]).

-include_lib("kernel/include/logger.hrl").

%%%===================================================================
%%% Callbacks
%%%===================================================================

%% Get a value from a given session_id
-callback get_value(SessionId, Key) ->
    {ok, Value :: any()} |
    {error, Reason :: atom()}
        when SessionId :: binary(),
             Key :: binary().

%% Set a value
-callback set_value(SessionId, Key, Value) ->
    ok |
    {error, Reason :: atom()}
    when SessionId :: binary(),
         Key :: binary(),
         Value :: binary().

%% Deletes a whole session
-callback delete_value(SessionId) ->
    ok |
    {error, Reason :: atom()}
        when SessionId :: binary().

%% Deletes a specific key of a session
-callback delete_value(SessionId, Key) ->
    ok |
    {error, Reason :: atom()}
        when SessionId :: binary(),
             Key :: binary().

%%%===================================================================
%%% Public functions
%%%===================================================================
-spec get(Req :: cowboy_req:req(), Key :: binary()) ->
                 {ok, Value :: binary()} | {error, Reason :: atom()} | no_return().
get(Req, Key) ->
    case get_session_id(Req) of
        {ok, SessionId} ->
            Mod = get_session_module(),
            erlang:apply(Mod, get_value, [SessionId, Key]);
        _ ->
            {error, not_found}
    end.

-spec set(Req :: cowboy_req:req(), Key :: binary(), Value :: binary()) ->
                 ok | {error, Reason :: atom()} | no_return().
set(Req, Key, Value) ->
    case get_session_id(Req) of
        {ok, SessionId} ->
            Mod = get_session_module(),
            erlang:apply(Mod, set_value, [SessionId, Key, Value]);
        _ ->
            {error, session_id_not_set}
    end.

-spec delete(Req :: cowboy_req:req()) -> {ok, Req :: cowboy_req:req()} |
                                                                      {error, Reason :: atom()}.
delete(Req) ->
    case get_session_id(Req) of
        {ok, SessionId} ->
            Mod = get_session_module(),
            erlang:apply(Mod, delete_value, [SessionId]),
            Req1 = cowboy_req:set_resp_cookie(<<"session_id">>, SessionId, Req,
                                              #{max_age => 0}),
            {ok, Req1};
        _ ->
            %% Session not found
            {ok, Req}
    end.

-spec delete(Req :: cowboy_req:req(), Key :: binary()) -> {ok, Req :: cowboy_req:req()} |
                                                          {error, Reason :: atom()} | no_return().
delete(Req, Key) ->
    case get_session_id(Req) of
        {ok, SessionId} ->
            Mod = get_session_module(),
            erlang:apply(Mod, delete_value, [SessionId, Key]),
            {ok, Req};
        _ ->
            %% Session not found
            {ok, Req}
    end.


%%%===================================================================
%%% Private functions
%%%===================================================================
get_session_module() ->
    application:get_env(nova, session_manager, nova_session_ets).

get_session_id(Req) ->
    case nova:get_env(use_sessions, true) of
        true ->
            #{session_id := SessionId} = cowboy_req:match_cookies([{session_id, [], undefined}], Req),
            case SessionId of
                undefined ->
                    {error, not_found};
                _ ->
                    {ok, SessionId}
            end;
        _ ->
            ?LOG_ERROR(#{msg => <<"Session called but 'use_session' option is set to false">>}),
            throw({nova_session, unsupported_session_used})
    end.

generate_session_id() ->
    SessionId =
        << <<X:8/unsigned-integer>> ||
            X <- [ rand:uniform(255) || _ <- lists:seq(0, 31) ] >>,
    {ok, base64:encode(SessionId)}.