diff --git a/config/config.exs b/config/config.exs index d55dc41..d2d855e 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,30 +1 @@ -# This file is responsible for configuring your application -# and its dependencies with the aid of the Mix.Config module. use Mix.Config - -# This configuration is loaded before any dependency and is restricted -# to this project. If another project depends on this project, this -# file won't be loaded nor affect the parent project. For this reason, -# if you want to provide default values for your application for -# 3rd-party users, it should be done in your "mix.exs" file. - -# You can configure for your application as: -# -# config :redbird, key: :value -# -# And access this configuration in your application as: -# -# Application.get_env(:redbird, :key) -# -# Or configure a 3rd-party app: -# -# config :logger, level: :info -# - -# It is also possible to import configuration files, relative to this -# directory. For example, you can emulate configuration per environment -# by uncommenting the line below and defining dev.exs, test.exs and such. -# Configuration from the imported file will override the ones defined -# here (which is why it is important to import them last). -# -# import_config "#{Mix.env}.exs" diff --git a/lib/redbird.ex b/lib/redbird.ex index 6829f43..5b5353f 100644 --- a/lib/redbird.ex +++ b/lib/redbird.ex @@ -1,2 +1,14 @@ defmodule Redbird do + use Application + + def start(_type, _args) do + import Supervisor.Spec + + children = [ + worker(Redbird.Redis, [Redbird.Redis.pid()]), + ] + + opts = [strategy: :one_for_one, name: Redbird.Supervisor] + Supervisor.start_link(children, opts) + end end diff --git a/lib/redbird/plug/session/redis.ex b/lib/redbird/plug/session/redis.ex new file mode 100644 index 0000000..85b0737 --- /dev/null +++ b/lib/redbird/plug/session/redis.ex @@ -0,0 +1,47 @@ +defmodule Plug.Session.REDIS do + import Redbird.Redis + require IEx + + @moduledoc """ + Stores the session in a redis store. + """ + + @behaviour Plug.Session.Store + + @max_session_time 86_164 * 30 + + def init(opts) do + opts + end + + def get(_conn, redis_key, _init_options) do + case get(redis_key) do + :undefined -> {nil, %{}} + value -> {redis_key, value |> :erlang.binary_to_term} + end + end + + def put(conn, nil, data, init_options) do + put(conn, generate_random_key(), data, init_options) + end + def put(_conn, redis_key, data, init_options) do + setex(%{key: redis_key, value: data, seconds: session_expiration(init_options)}) + redis_key + end + + def delete(_conn, redis_key, _kinit_options) do + del(redis_key) + :ok + end + + defp generate_random_key do + :crypto.strong_rand_bytes(96) |> Base.encode64 + end + + defp session_expiration(opts) do + case opts[:expiration_in_seconds] do + seconds when is_integer(seconds) -> seconds + _ -> @max_session_time + end + end +end diff --git a/lib/redis.ex b/lib/redis.ex new file mode 100644 index 0000000..52e689f --- /dev/null +++ b/lib/redis.ex @@ -0,0 +1,23 @@ +defmodule Redbird.Redis do + def start_link(name) do + {:ok, client} = Exredis.start_link + true = Process.register(client, name) + {:ok, client} + end + + def get(value) do + Exredis.Api.get(pid(), value) + end + + def setex(%{key: key, value: value, seconds: seconds}) do + Exredis.Api.setex(pid(), key, seconds, value) + end + + def del(key) do + Exredis.Api.del(pid(), key) + end + + def pid do + :redbird_phoenix_session + end +end diff --git a/mix.exs b/mix.exs index 865c372..dd23a5c 100644 --- a/mix.exs +++ b/mix.exs @@ -2,31 +2,33 @@ defmodule Redbird.Mixfile do use Mix.Project def project do - [app: :redbird, - version: "0.1.0", - elixir: "~> 1.3", - build_embedded: Mix.env == :prod, - start_permanent: Mix.env == :prod, - deps: deps()] + [ + app: :redbird, + build_embedded: Mix.env == :prod, + deps: deps(), + elixir: "~> 1.3", + start_permanent: Mix.env == :prod, + version: "0.1.0", + package: [ + maintainers: ["anellis", "drapergeek"], + licenses: ["MIT"], + links: %{"GitHub" => "https://github.com/thoughtbot/redbird"} + ], + ] end - # Configuration for the OTP application - # - # Type "mix help compile.app" for more information def application do - [applications: [:logger]] + [ + applications: [:logger], + mod: {Redbird, []}, + ] end - # Dependencies can be Hex packages: - # - # {:mydep, "~> 0.3.0"} - # - # Or git/path repositories: - # - # {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"} - # - # Type "mix help deps" for more examples and options defp deps do - [] + [ + {:ex_doc, "~> 0.13", only: :dev}, + {:exredis, "~> 0.2"}, + {:plug, "~> 1.1"}, + ] end end diff --git a/mix.lock b/mix.lock new file mode 100644 index 0000000..d06cb72 --- /dev/null +++ b/mix.lock @@ -0,0 +1,6 @@ +%{"earmark": {:hex, :earmark, "1.0.3", "89bdbaf2aca8bbb5c97d8b3b55c5dd0cff517ecc78d417e87f1d0982e514557b", [:mix], []}, + "eredis": {:hex, :eredis, "1.0.8", "ab4fda1c4ba7fbe6c19c26c249dc13da916d762502c4b4fa2df401a8d51c5364", [:rebar], []}, + "ex_doc": {:hex, :ex_doc, "0.14.5", "c0433c8117e948404d93ca69411dd575ec6be39b47802e81ca8d91017a0cf83c", [:mix], [{:earmark, "~> 1.0", [hex: :earmark, optional: false]}]}, + "exredis": {:hex, :exredis, "0.2.5", "34d02fa9ef32174cbd790b166746f91b3dbc131ce37a2d9a2451e8bba164b423", [:mix], [{:eredis, ">= 1.0.8", [hex: :eredis, optional: false]}]}, + "mime": {:hex, :mime, "1.0.1", "05c393850524767d13a53627df71beeebb016205eb43bfbd92d14d24ec7a1b51", [:mix], []}, + "plug": {:hex, :plug, "1.3.0", "6e2b01afc5db3fd011ca4a16efd9cb424528c157c30a44a0186bcc92c7b2e8f3", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1", [hex: :cowboy, optional: true]}, {:mime, "~> 1.0", [hex: :mime, optional: false]}]}} diff --git a/test/redbird_test.exs b/test/redbird_test.exs index 3610f7e..48bdf99 100644 --- a/test/redbird_test.exs +++ b/test/redbird_test.exs @@ -1,8 +1,93 @@ defmodule RedbirdTest do - use ExUnit.Case - doctest Redbird + use ExUnit.Case, async: true + use Plug.Test + alias Plug.Session.REDIS - test "the truth" do - assert 1 + 1 == 2 + @default_opts [ + store: :redis, + key: "_session_key", + ] + @secret String.duplicate("thoughtbot", 8) + + defp sign_plug(options) do + options = + options ++ @default_opts + |> Keyword.put(:encrypt, false) + Plug.Session.init(options) + end + + defp sign_conn(conn, options \\ []) do + put_in(conn.secret_key_base, @secret) + |> Plug.Session.call(sign_plug(options)) + |> fetch_session + end + + setup_all do + Application.stop(:redbird) + :ok = Application.start(:redbird) + end + + describe "get" do + test "when there is value stored it is retrieved" do + conn = conn(:get, "/") + |> sign_conn + |> put_session(:foo, "bar") + |> send_resp(200, "") + + conn = + conn(:get, "/") + |> recycle_cookies(conn) + |> sign_conn + |> send_resp(200, "") + + assert conn |> get_session(:foo) == "bar" + end + + test "when there is no session with the key, it returns {:nil, %{}}" do + key = "redis_session" + conn = %{} + options = [] + + assert {nil, %{}} = REDIS.get(conn, key, options) + end + end + + describe "put" do + test "it sets the session properly" do + conn = conn(:get, "/") + |> sign_conn + |> put_session(:foo, "bar") + |> send_resp(200, "") + assert conn |> get_session(:foo) == "bar" + end + + test "it allows configuring session expiration" do + conn = conn(:get, "/") + |> sign_conn(expiration_in_seconds: 1) + |> put_session(:foo, "bar") + |> send_resp(200, "") + + :timer.sleep(1000) + + conn = + conn(:get, "/") + |> recycle_cookies(conn) + |> sign_conn + |> send_resp(200, "") + + assert conn |> get_session(:foo) |> is_nil + end + end + + describe "delete" do + test "delete session" do + key = "redis_session" + conn = %{} + options = [] + REDIS.put(conn, key, %{foo: :bar}, options) + REDIS.delete(conn, key, options) + + assert {nil, %{}} = REDIS.get(conn, key, options) + end end end