Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Replace Addict with Guardian #126

Merged
merged 44 commits into from
Oct 18, 2017

Conversation

pmrukot
Copy link
Collaborator

@pmrukot pmrukot commented Oct 6, 2017

Summary
Replaces Addict with Guardian. Basically changes whole authentication model.

Related issues
#39

Test plan
Manual + additional unit tests. Register and login are not integrated in Elm as a part of single page app.

TODO

  • Delete Addict (libraries + templates)
  • Setup Guardian
  • Add login page
  • Add registration page
  • Add logout
  • Persist token in local storage
  • Add token while making request for resources
  • Update channel token

NOTE(mrapacz|12-10-2017):

What still needs to be done before merging:

  • A proper review
  • Backend unit tests for registration and all the other stuff that appeared
  • Moving the keys to envs and replacing the hardcoded values with System.get_env


config :guardian, Guardian,
allowed_algos: ["HS512"],
secret_key: "@11H41LW35tT3x45",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😆

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thats super experimental PR, please do not laugh 😠

@pmrukot
Copy link
Collaborator Author

pmrukot commented Oct 8, 2017

Added TODO section for easier tracking of a PR

@mrapacz
Copy link
Collaborator

mrapacz commented Oct 8, 2017

Note that this PR's main goal is to swap the libraries.

  • Adding logout is not an essential part of this PR, it may actually be easier to push it as a separate change (we were able to do without it so far, which means we should be able to do without it in this PR too).
  • Adding login/registration page is not essential (at least not both of them at the same time). So far we were able to use only registration page to log in and test that everything works correctly and that may be enough to verify that PR changes are alright. In a separate PR we could extend the registration page with the login feature. If you introduce two different pages right now then it will be much more complex to merge them into one that would offer both registration and login later.

@pmrukot
Copy link
Collaborator Author

pmrukot commented Oct 8, 2017

I'll make login/register page as one view. Idea of this PR is to make it work just as it was before. I agree that logout is not necessary but our users need to be able to register/login just as before.

@mrapacz
Copy link
Collaborator

mrapacz commented Oct 8, 2017

This PR should contain whatever changes are needed to prove that addict was removed totally and guardian was successfully introduced. From that point we can begin to work paralelly on issues connected to that - logout for example (users didn't have an option to do that so far).

@pmrukot
Copy link
Collaborator Author

pmrukot commented Oct 11, 2017

Everything is done, I think this is enough for first version. Im not sure about the case when the token expires but lets leave this case for now. Branch is also synchronised with master.

But I have another issues that are more important:

  • need to verify if config is fine / add configuration for production
  • need help with dialyzer
  • mix.test is screaming something like:
== Compilation error on file lib/aion/endpoint.ex ==
** (UndefinedFunctionError) function Aion.Router.init/1 is undefined (module Aion.Router is not available)
    Aion.Router.init([])
    (plug) lib/plug/builder.ex:193: Plug.Builder.init_module_plug/3
    (plug) lib/plug/builder.ex:181: anonymous fn/4 in Plug.Builder.compile/3
    (elixir) lib/enum.ex:1755: Enum."-reduce/3-lists^foldl/2-0-"/3
    (plug) lib/plug/builder.ex:181: Plug.Builder.compile/3
    (phoenix) expanding macro: Phoenix.Endpoint.__before_compile__/1
    lib/aion/endpoint.ex:1: Aion.Endpoint (module)

So 2 important checks wont pass on CI (tests wont run and dialyzer also screams)

@pmrukot
Copy link
Collaborator Author

pmrukot commented Oct 11, 2017

New login change:
screen shot 2017-10-11 at 22 51 18

New register page:
screen shot 2017-10-11 at 22 51 25

The rest is working as before. Can't wait to actually split this into two apps/repositories.

@pmrukot
Copy link
Collaborator Author

pmrukot commented Oct 11, 2017

Start with manual tests, let's fix the CI and then start reviewing the code. Please checkout to this branch and if everything works for you as before.

Copy link
Collaborator

@mrapacz mrapacz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some of them are to-do-now issues, some of them are to be discussed. Fantastic job in general 👏 🔥 🎆 ✨

Too bad the PR is so big and the review takes so long 😆

pool: Ecto.Adapters.SQL.Sandbox

config :guardian, Guardian,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be one Guardian config only in config.exs, it should import a proper key from the environment (which will vary depending on the environment).

aion/mix.exs Outdated
]
{:postgrex, ">= 0.0.0"},
{:simetric, "~> 0.1.0"},
{:exrm, "~> 1.0.8"},]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move the bracket to a new line.

user = %{ email: "[email protected]", name: "something" }
conn = Plug.Test.init_test_session(build_conn(), %{current_user: user})
{:ok, conn: put_req_header(conn, "accept", "application/json")}
user = %Aion.User{ email: "[email protected]", name: "something", password: "2131231", id: 1 }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be no spaces after { and before }

user = %{ email: "[email protected]", name: "something"}
conn = Plug.Test. init_test_session(build_conn(), %{current_user: user})
{:ok, conn: put_req_header(conn, "accept", "application/json")}
user = %Aion.User{ email: "[email protected]", name: "something", password: "2131231", id: 1 }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{ x }

user = %{ email: "[email protected]", name: "something" }
conn = Plug.Test. init_test_session(build_conn(), %{current_user: user})
{:ok, conn: put_req_header(conn, "accept", "application/json")}
user = %Aion.User{ email: "[email protected]", name: "something", password: "2131231", id: 1 }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{ samehere }

Forms.updateFormInput form name value


unwrapToken : Maybe String -> String
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about

getToken maybeToken = 
    case maybeToken of
        Just token -> token
        Nothing -> ""

oldDisplayLoginInsteadOfRegistration =
oldAuthData.displayLoginInsteadOfRegistration
in
case oldDisplayLoginInsteadOfRegistration of
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will get hugely simplified when we introduce setters, so I'm not going to ask you to fix it now.

includeNavbar =
case currentRoute of
AuthRoute ->
\x y z -> x
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🥇

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seriously, though, any idea for a better naming here? 😄

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Plus if y and z are not needed then I suppose you could do \betterName _ _ -> betterName

|> validate_required([:name, :email, :encrypted_password])
|> cast(params, [:name, :email, :password])
|> validate_required([:name, :email, :password])
|> validate_length(:email, min: 3, max: 30)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could move constants like this to a separate module or just out of this function and not hardcode them. (especially the latter one shouldn't be a lot of work, you could deal with that using the module @attributes which are conventional way of dealing with module constants in Elixir.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is out of scope of this PR.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True

use Aion.Web, :view

def render("user.json", %{user: user}) do
if user != nil do
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still not sure about conventional way of dealing with nils, but this answer and Jose's answer suggest some solutions. I think the second one could actually be better (but it's just my personal opinion because I'm not a die hard fan of multiple function clauses). Here's how the first one could work:

def render("user.json", %user: user}) when is_nil user, do: %{}
def render("user.json", %user: user}), do: Map.fetch(user, [:id, :name, :email])

Similarly to my suggestion ^ in the second one you should definitely use Map.fetch/2

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this work?

def render("user.json", %{user: nil}), do: %{}
def render("user.json", %{user: user}), do: Map.fetch(user, [:id, :name, :email])

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Map.take/2*

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah right, was looking at the docs and eventually used the wrong one
lgtm

@@ -16,4 +16,13 @@ registrationErrorToast =

registrationSuccessfulToast : ( Model, Cmd Msg ) -> ( Model, Cmd Msg )
registrationSuccessfulToast =
addToast (Toasty.Defaults.Success "Success!" "Account created successfuly - proceed to Login.")
addToast (Toasty.Defaults.Success "Success!" "Account created successfuly.")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

successfully

user = %{ email: "[email protected]", name: "something" }
conn = Plug.Test. init_test_session(build_conn(), %{current_user: user})
{:ok, conn: put_req_header(conn, "accept", "application/json")}
user = %Aion.User{ email: "[email protected]", name: "something", password: "2131231", id: 1 }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this piece of code repeats many times ;) How about extracting it somewhere ;)

def unauthenticated(conn, _params) do
conn
|> put_status(401)
|> render("error.json", message: "Authentication required")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree but I would prefer to put it in authorization related module

@pmrukot
Copy link
Collaborator Author

pmrukot commented Oct 16, 2017

I extracted unauthenticated function to errors.ex in views. Test setup is also extracted. As for making a generic request function I would need to put a little more effort into it. Apart from that most of the changes are applied, let me know what you think.

Copy link
Collaborator

@mrapacz mrapacz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move the rest of the common errors to the error file. As for the rest of the issues, if something is not to be fixed during this PR just make sure there is an existing issue for that, so that we don't forget to fix it in the future (like the request stuff in Elm) 😃

@@ -2,3 +2,18 @@ ExUnit.start

Ecto.Adapters.SQL.Sandbox.mode(Aion.Repo, :manual)

defmodule Aion.TestHelpers do
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess the conventional way is to put it in the support/ and name it with _case:
image

def login_by_username_and_pass(conn, email, given_pass, opts) do
repo = Keyword.fetch!(opts, :repo)
user = repo.get_by(Aion.User, email: email)
cond do
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, let's leave it this way then

SessionController.create(conn, %{"email" => user.email, "password" => user.password})
{:error, changeset} ->
conn
|> put_status(:unprocessable_entity)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought this would also end up in errors.ex

|> render("login.json", jwt: jwt)
{:error, _reason, conn} ->
conn
|> put_status(500)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bump

createQuestionWithAnswersRequest url token decoder body =
Http.request
{ method = "POST"
, headers = [ Http.header "Authorization" ("Bearer " ++ token) ]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pmrukot if it doesn't get fixed now, it should be added as a follow-up issue (you can add it to changes listed in #56)

%{"data": "failed to authorise"}
end

def render("login.json", %{jwt: jwt}) do
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The jwt cannot be changed to token cause it would require a change in Guardian's implementation, right?


def unauthenticated(conn) do
conn
|> put_status(401)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tab here is redundant

@@ -0,0 +1,12 @@
defmodule Aion.ErrorCodesViews do
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be easier if the module names resembled the file names, just like they did so far (e.g. RoomChannel -> room_channel.ex, UserRecord -> user_record.ex)

It would be even better if their whole name would represent the filepath, but it's not doable so easily with Phoenix 1.2 architecture. But the file names can mirror the module names at least, it's easier to search through the repo then.

Also, this is part of a controller, not of a view. I think putting it in the same directory as all the controllers would be alright (the code wouldn't move that far in the whole project directory). You could name it then like Aion.ControllerErrors or something similar

@mrapacz
Copy link
Collaborator

mrapacz commented Oct 16, 2017

I've moved the errors stuff to a separate module in controllers directory. I think it looks better now. Also, the imports used in all of the controllers can be stored in the web.ex to avoid repeating stuff.

One last thing that needs to be done is moving Guardian logic to conn_case.ex

We've moved to a lib that is widely used so you shouldn't have any problems finding resources in this area like this or this

@pmrukot
Copy link
Collaborator Author

pmrukot commented Oct 17, 2017

@mrapacz Issue with conn_case is resolved by adding another test helper since one of our tests need to have different headers to work properly.

I've also added tests for Aion.Auth module.

I think we are ready to merge if all issues are resolved.

Copy link
Collaborator

@mrapacz mrapacz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So if I understand right the AuthConnCase sets up a logged in conn for the tests and ConnCase doesn't? Just small nits, looks good to me apart from that, good job 👍


login_result = Aion.Auth.login_by_username_and_pass(conn, @user.email, @user.password, repo: Aion.Repo)

assert tuple_size(login_result) == 2
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just for the future, I think in general we could match the exact results (if it's possible), cause just checking for list's/tuple's length may not be the most accurate thing.

This is what we've been doing so far, so no need to fix it, just suggesting that we could take another approach in the future.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to do it but it was not possible since authenticated conn contains things that depend on time (I think some kind of timestamp).

Second thing is that the result is a tuple that is {:ok, login(conn, user)} and login method was tested above. Also its the only result that provides tuple that has length 2 so I think its fine (although not that scalable).

@@ -1,19 +1,3 @@
ExUnit.start
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this file is not needed anymore?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is

19:52:22.599 [error] Task #PID<0.314.0> started from #PID<0.73.0> terminating
** (stop) exited in: GenServer.call(ExUnit.Server, {:take_async_cases, 16}, 60000)
    ** (EXIT) no process: the process is not alive or there's no process currently associated with the given name, possibly because its application isn't started

@mrapacz
Copy link
Collaborator

mrapacz commented Oct 18, 2017

Also, make sure you update the description of PR and its title/commit message. As for the latter one - pro tip:
github browser extensions
especially refined github which will set the commit message automatically as the PR title + issue number

@pmrukot
Copy link
Collaborator Author

pmrukot commented Oct 18, 2017

AuthConnCase is used used in all controllers tests apart from PageControllerTest - it uses old ConnCase since it requires different headers to pass and renders html, not json as other controllers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants