From 006f220c5cc6b002aea7605217019cc1c23385c0 Mon Sep 17 00:00:00 2001 From: Felipe Ripoll Date: Mon, 30 Jul 2018 06:30:36 -0600 Subject: [PATCH] [#33] adding mnesia and setting up users --- .circleci/config.yml | 2 + README.md | 16 ++++- config/config.exs | 12 ++++ config/test.exs | 5 +- lib/poa_backend/application.ex | 1 + lib/poa_backend/auth.ex | 70 +++++++++++++++++++ lib/poa_backend/auth/models/user.ex | 43 ++++++++++++ lib/poa_backend/auth/repo.ex | 4 ++ mix.exs | 8 ++- mix.lock | 7 ++ .../20180727134525_create_users.exs | 13 ++++ test/ancillary/utils.ex | 8 +++ test/auth/user_test.exs | 63 +++++++++++++++++ 13 files changed, 249 insertions(+), 3 deletions(-) create mode 100644 lib/poa_backend/auth.ex create mode 100644 lib/poa_backend/auth/models/user.ex create mode 100644 lib/poa_backend/auth/repo.ex create mode 100644 priv/repo/migrations/20180727134525_create_users.exs create mode 100644 test/ancillary/utils.ex create mode 100644 test/auth/user_test.exs diff --git a/.circleci/config.yml b/.circleci/config.yml index f45f865..076d0a1 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -14,6 +14,8 @@ jobs: - run: mix local.hex --force - run: mix local.rebar --force - run: mix deps.get + - run: env MIX_ENV=test mix ecto.create + - run: env MIX_ENV=test mix ecto.migrate - run: mix test - run: mix credo - restore_cache: diff --git a/README.md b/README.md index a836ac3..6eb896d 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,21 @@ That command will create a `doc/` folder with the actual Documentation. ## Run Tests -In order to run the tests we have to run the command +`POABackend` uses [Mnesia](http://erlang.org/doc/man/mnesia.html) as a local database with [Ecto](https://hexdocs.pm/ecto/Ecto.html). In order to have this running we have to create a folder where `Mnesia` will store our data. In order to do that we have to define it in the `config/test.exs` file like this: + +``` +config :mnesia, + dir: 'your/local/path' # make sure this directory exists! +``` + +once we have the path defined we have to create the database (those commands must be run only once if you are going to use always this same path for testing). In your root folder run: + +``` +MIX_ENV=test mix ecto.create +MIX_ENV=test mix ecto.migrate +``` + +Now the environment is set. We can run the tests with: ``` mix test diff --git a/config/config.exs b/config/config.exs index 36be129..4501539 100644 --- a/config/config.exs +++ b/config/config.exs @@ -7,4 +7,16 @@ config :plug, :statuses, %{ 422 => "Unprocessable Entity" } +config :poa_backend, + ecto_repos: [POABackend.Auth.Repo] + +# here we configure the needed data for Ecto and Mnesia (DB) +config :poa_backend, POABackend.Auth.Repo, + adapter: EctoMnesia.Adapter, + host: Kernel.node(), + storage_type: :disc_copies # this will store the data on disk and memory + +config :mnesia, + dir: 'priv/data/mnesia' # make sure this directory exists! + import_config "#{Mix.env}.exs" diff --git a/config/test.exs b/config/test.exs index 2f40e1e..855f10a 100644 --- a/config/test.exs +++ b/config/test.exs @@ -35,4 +35,7 @@ config :poa_backend, :subscriptions, [ {:dashboard_receiver, [:ethereum_metrics]} - ] \ No newline at end of file + ] + +config :mnesia, + dir: '_build/test' # make sure this directory exists! \ No newline at end of file diff --git a/lib/poa_backend/application.ex b/lib/poa_backend/application.ex index 3db8e61..0cab2a1 100644 --- a/lib/poa_backend/application.ex +++ b/lib/poa_backend/application.ex @@ -7,6 +7,7 @@ defmodule POABackend.Application do import Supervisor.Spec children = [ + supervisor(POABackend.Auth.Repo, []), supervisor(POABackend.CustomHandler.Supervisor, []), supervisor(POABackend.Metrics.Supervisor, []), supervisor(POABackend.Receivers.Supervisor, []) diff --git a/lib/poa_backend/auth.ex b/lib/poa_backend/auth.ex new file mode 100644 index 0000000..f7646ef --- /dev/null +++ b/lib/poa_backend/auth.ex @@ -0,0 +1,70 @@ +defmodule POABackend.Auth do + + @moduledoc """ + This module defines the API for the Authorisation + """ + + alias POABackend.Auth.Models.User + alias POABackend.Auth.Repo + + @doc """ + Registers a user in the system. + """ + @spec create_user(String.t, String.t, Boolean.t) :: {:ok, User.t} | {:error, :already_exists} | {:error, Ecto.Changeset.t} + def create_user(user_name, password, active \\ true) do + try do + %User{} + |> User.changeset(%{user: user_name, + password: password, + active: active}) + |> Repo.insert + rescue + x in CaseClauseError -> x.term + end + end + + @doc """ + Get a user from the database based in the user name + """ + @spec get_user(String.t) :: User.t | nil + def get_user(user) do + Repo.get(User, user) + end + + @doc """ + Deletes a user from the database based in the given user + """ + @spec remove_user(User.t) :: :ok + def remove_user(user) do + _ = Repo.delete(user) + :ok + end + + @doc """ + This function Activates a user, storing `active: true` in the database for the given user + """ + @spec activate_user(User.t) :: {:ok, User.t} | {:error, Ecto.Changeset.t} + def activate_user(user) do + user + |> User.changeset(%{active: true}) + |> Repo.update + end + + @doc """ + This function Deactivates a user, storing `active: false` in the database for the given user + """ + @spec deactivate_user(User.t) :: {:ok, User.t} | {:error, Ecto.Changeset.t} + def deactivate_user(user) do + user + |> User.changeset(%{active: false}) + |> Repo.update + end + + @doc """ + Checks if a user is active + """ + @spec user_active?(User.t) :: Boolean.t + def user_active?(%User{active: true}), do: true + def user_active?(%User{active: _}), do: false + +end \ No newline at end of file diff --git a/lib/poa_backend/auth/models/user.ex b/lib/poa_backend/auth/models/user.ex new file mode 100644 index 0000000..6b3566a --- /dev/null +++ b/lib/poa_backend/auth/models/user.ex @@ -0,0 +1,43 @@ +defmodule POABackend.Auth.Models.User do + use Ecto.Schema + alias __MODULE__ + import Ecto.Changeset + + @moduledoc """ + This module encapsulates the _User_ model + """ + + @primary_key {:user, :string, []} + + schema "users" do + # field :user, :string, primary_key: true + field :password_hash, :string + field :password, :string, virtual: true + field :active, :boolean, default: true + + timestamps() + end + + @type t :: %__MODULE__{user: String.t, + password_hash: String.t, + password: String.t, + active: :boolean} + + def changeset(%User{} = user, params \\ %{}) do + user + |> cast(params, ~w(user password active)) + |> validate_required([:user]) + |> validate_length(:password, min: 8) + |> unique_constraint(:user) + |> put_password_hash() + end + + defp put_password_hash(%{changes: %{password: password}} = changeset) do + alias Comeonin.Bcrypt + + changeset + |> put_change(:password_hash, Bcrypt.hashpwsalt(password)) + |> put_change(:password, nil) + end + defp put_password_hash(%{changes: %{}} = changeset), do: changeset +end \ No newline at end of file diff --git a/lib/poa_backend/auth/repo.ex b/lib/poa_backend/auth/repo.ex new file mode 100644 index 0000000..3a3482c --- /dev/null +++ b/lib/poa_backend/auth/repo.ex @@ -0,0 +1,4 @@ +defmodule POABackend.Auth.Repo do + use Ecto.Repo, otp_app: :poa_backend + @moduledoc false +end \ No newline at end of file diff --git a/mix.exs b/mix.exs index 044323c..9391d68 100644 --- a/mix.exs +++ b/mix.exs @@ -9,6 +9,7 @@ defmodule POABackend.MixProject do version: @version, elixir: "~> 1.6", start_permanent: Mix.env() == :prod, + elixirc_paths: elixirc_paths(Mix.env), deps: deps(), aliases: aliases(), docs: docs(), @@ -16,10 +17,13 @@ defmodule POABackend.MixProject do ] end + defp elixirc_paths(:test), do: ["lib", "test/ancillary"] + defp elixirc_paths(_), do: ["lib"] + # Run "mix help compile.app" to learn about applications. def application do [ - extra_applications: [:logger, :cowboy, :plug, :poison, :worker_pool], + extra_applications: [:logger, :cowboy, :plug, :poison, :worker_pool, :ecto_mnesia], mod: {POABackend.Application, []} ] end @@ -35,6 +39,8 @@ defmodule POABackend.MixProject do {:ex_aws_dynamo, "~> 2.0"}, {:hackney, "~> 1.12"}, {:msgpax, "~> 2.1"}, + {:ecto_mnesia, "~> 0.9.1"}, + {:comeonin, "~> 3.2"}, # Tests {:credo, "~> 0.9", only: [:dev, :test], runtime: false}, diff --git a/mix.lock b/mix.lock index 2434819..fb6aefc 100644 --- a/mix.lock +++ b/mix.lock @@ -1,12 +1,18 @@ %{ "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, "certifi": {:hex, :certifi, "2.3.1", "d0f424232390bf47d82da8478022301c561cf6445b5b5fb6a84d49a9e76d2639", [:rebar3], [{:parse_trans, "3.2.0", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"}, + "comeonin": {:hex, :comeonin, "3.2.0", "cb10995a22aed6812667efb3856f548818c85d85394d8132bc116fbd6995c1ef", [], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm"}, + "confex": {:hex, :confex, "3.3.1", "8febaf751bf293a16a1ed2cbd258459cdcc7ca53cfa61d3f83d49dd276a992b4", [], [], "hexpm"}, "cowboy": {:hex, :cowboy, "1.0.4", "a324a8df9f2316c833a470d918aaf73ae894278b8aa6226ce7a9bf699388f878", [:make, :rebar], [{:cowlib, "~> 1.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"}, "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], [], "hexpm"}, "credo": {:hex, :credo, "0.9.3", "76fa3e9e497ab282e0cf64b98a624aa11da702854c52c82db1bf24e54ab7c97a", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:poison, ">= 0.0.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, + "decimal": {:hex, :decimal, "1.5.0", "b0433a36d0e2430e3d50291b1c65f53c37d56f83665b43d79963684865beab68", [], [], "hexpm"}, "dialyxir": {:hex, :dialyxir, "0.5.1", "b331b091720fd93e878137add264bac4f644e1ddae07a70bf7062c7862c4b952", [:mix], [], "hexpm"}, "distillery": {:hex, :distillery, "1.5.3", "b2f4fc34ec71ab4f1202a796f9290e068883b042319aa8c9aa45377ecac8597a", [], [], "hexpm"}, "earmark": {:hex, :earmark, "1.2.5", "4d21980d5d2862a2e13ec3c49ad9ad783ffc7ca5769cf6ff891a4553fbaae761", [:mix], [], "hexpm"}, + "ecto": {:hex, :ecto, "2.1.6", "29b45f393c2ecd99f83e418ea9b0a2af6078ecb30f401481abac8a473c490f84", [], [{:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: true]}, {:decimal, "~> 1.2", [hex: :decimal, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.8.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"}, + "ecto_mnesia": {:hex, :ecto_mnesia, "0.9.1", "5887e3bcae972d9a26494a87cbd318d401a65b4af324480073d11df654d1b235", [], [{:confex, "~> 3.3", [hex: :confex, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 2.1.6", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm"}, + "elixir_make": {:hex, :elixir_make, "0.4.2", "332c649d08c18bc1ecc73b1befc68c647136de4f340b548844efc796405743bf", [], [], "hexpm"}, "ex_aws": {:hex, :ex_aws, "2.0.2", "8df2f96f58624a399abe5a0ce26db648ee848aca6393b9c65c939ece9ac07bfa", [:mix], [{:configparser_ex, "~> 2.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "1.6.3 or 1.6.5 or 1.7.1 or 1.8.6 or ~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8", [hex: :jsx, repo: "hexpm", optional: true]}, {:poison, ">= 1.2.0", [hex: :poison, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:xml_builder, "~> 0.1.0", [hex: :xml_builder, repo: "hexpm", optional: true]}], "hexpm"}, "ex_aws_dynamo": {:hex, :ex_aws_dynamo, "2.0.0", "07b1117bbd1b1d04e2598190834c69c271db1d357cc21b82240d1a0b17194165", [:mix], [{:ex_aws, "~> 2.0.0", [hex: :ex_aws, repo: "hexpm", optional: false]}], "hexpm"}, "ex_doc": {:hex, :ex_doc, "0.18.3", "f4b0e4a2ec6f333dccf761838a4b253d75e11f714b85ae271c9ae361367897b7", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"}, @@ -25,6 +31,7 @@ "parse_trans": {:hex, :parse_trans, "3.2.0", "2adfa4daf80c14dc36f522cf190eb5c4ee3e28008fc6394397c16f62a26258c2", [:rebar3], [], "hexpm"}, "plug": {:hex, :plug, "1.6.0", "90d338a44c8cd762c32d3ea324f6728445c6145b51895403854b77f1536f1617", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm"}, "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, + "poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [], [], "hexpm"}, "ranch": {:hex, :ranch, "1.5.0", "f04166f456790fee2ac1aa05a02745cc75783c2bfb26d39faf6aefc9a3d3a58a", [:rebar3], [], "hexpm"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], [], "hexpm"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.3.1", "a1f612a7b512638634a603c8f401892afbf99b8ce93a45041f8aaca99cadb85e", [:rebar3], [], "hexpm"}, diff --git a/priv/repo/migrations/20180727134525_create_users.exs b/priv/repo/migrations/20180727134525_create_users.exs new file mode 100644 index 0000000..2c3d9e4 --- /dev/null +++ b/priv/repo/migrations/20180727134525_create_users.exs @@ -0,0 +1,13 @@ +defmodule POABackend.Auth.Repo.Migrations.CreateUsers do + use Ecto.Migration + + def change do + create_if_not_exists table(:users, primary_key: false) do + add :user, :string, primary_key: true + add :password_hash, :string + add :active, :boolean, default: true + + timestamps() + end + end +end diff --git a/test/ancillary/utils.ex b/test/ancillary/utils.ex new file mode 100644 index 0000000..adf97a9 --- /dev/null +++ b/test/ancillary/utils.ex @@ -0,0 +1,8 @@ +defmodule POABackend.Ancillary.Utils do + @moduledoc false + + def clear_db do + :mnesia.clear_table(:users) + end + +end \ No newline at end of file diff --git a/test/auth/user_test.exs b/test/auth/user_test.exs new file mode 100644 index 0000000..71ee016 --- /dev/null +++ b/test/auth/user_test.exs @@ -0,0 +1,63 @@ +defmodule Auth.UserTest do + use ExUnit.Case + alias POABackend.Auth + alias POABackend.Ancillary.Utils + + setup do + Utils.clear_db() + + on_exit fn -> + Utils.clear_db() + end + + [] + end + + test "create a new user" do + alias Comeonin.Bcrypt + + password = "mypassword" + + {:ok, user} = Auth.create_user("ferigis", password) + + assert Bcrypt.checkpw(password, user.password_hash) + + {:error, :already_exists} = Auth.create_user("ferigis", "mypassword") + end + + test "get a user" do + {:ok, user} = Auth.create_user("ferigis", "mypassword") + + assert user == Auth.get_user("ferigis") + + assert nil == Auth.get_user("otheruser") + end + + test "remove a user" do + assert nil == Auth.get_user("ferigis") + + {:ok, user} = Auth.create_user("ferigis", "mypassword") + + assert user == Auth.get_user("ferigis") + + :ok = Auth.remove_user(user) + + assert nil == Auth.get_user("ferigis") + end + + test "activate/deactivate a user" do + {:ok, user} = Auth.create_user("ferigis", "mypassword") + + assert Auth.user_active?(user) + + {:ok, _} = Auth.deactivate_user(user) + user = Auth.get_user("ferigis") + + refute Auth.user_active?(user) + + {:ok, _} = Auth.activate_user(user) + user = Auth.get_user("ferigis") + + assert Auth.user_active?(user) + end +end \ No newline at end of file