Hi All,
So I have just started a new project and I wanted to avoid some of the issues I had before, so I started going crazy with macros etc. Now I have something that half of me really likes, and the other half thinks Im doing things the wrong way. I’d like some feedback on whether my approaches are sensible or a complete discrace to the elixir community :).
Ok so the first issue I had was that I had occasionally ended up passing in the wrong value for functions like:
some_func(some_id, _some_other_id)
In this case I had a few small time wasting issues where I had these args in the wrong order. I had tests around this but sometimes with defdelgate in the mix etc I somehow still passed the wrong vars. In this case I really missed types, and I wished I had taken my input and cast it to a struct as early as possible.
Another issue I had is that I was taking in and returning snake cased json, which isn’t the convention in javascript.
Finally, my authorization was a mess of logic and I started to think about moving all this to one place such as described here: https://dockyard.com/blog/2017/08/01/authorization-for-phoenix-contexts
What I wanted to end up with is something that simplifies:
- Taking controller params input in and convert them a DTO struct
- Run validations against the input and bomb out if its invalid
- Use use this DTO to check user authorization
- If all well run the controller logic
In some ways I have achieved all of these but in other ways I might have gone a bit Javary to do so.
Here’s what my controller looks like now:
defmodule LunchFuWeb.AuthController do
use LunchFuWeb.BaseController
alias LunchFu.Dto
alias Lunchfu.Accounts
@bind request: Dto.LoginRequest
def login(conn, _, request: login_request) do
with {:ok, user} <- Accounts.login(login_request) do
json(conn, user)
end
end
@bind request: Dto.UpdateCredentialsRequest
@can_handle :update_user, :request, LunchFu.DefaultUserGloves
def update_user(conn, _, request: update_user_request) do
with {:ok, user} <- Accounts.update_user(update_user_request) do
json(conn, user)
end
end
end
The input mapping uses a small library to define how input should be mapped, and it is defined something like this:
defmodule CamelCaseKeyResolver do
def resolve(key) do
[h | t] =
key
|> Atom.to_string()
|> Macro.camelize()
|> String.split("", trim: true)
String.downcase(h) <> Enum.join(t)
end
end
defmodule LunchFu.Dto.LoginRequest do
use ExMapper.DefMapping
defstruct [:username, :password, :hashed_password]
defmapping do
keys &CamelCaseKeyResolver.resolve/1
override :hashed_password, key: "password", value: &hash_password_input/1
end
def validate(request) do
request
|> Justify.validate_required(:username)
|> Justify.validate_required(:password)
end
def hash_password_input(val) do
# TODO: Hash password
"SOMEHASHED_PASSWORD_EXAMPLE"
end
end
The permissions are handled by a small dsl to define permissions that looks like this:
defmodule LunchFu.DefaultUserGloves do
use Rubbergloves, wearer: LunchFu.User
# Hardcoded rules checked first
phase :pre_checks do
can_handle!(%LunchFu.User{role: :admin}, _any_action) # Admin can do anything
can_handle!(%LunchFu.User{name: "Christopher Owen"}, :update_user, request=%DTO.UpdateCredentialsRequest{}) # Hardcoded that I can update users
cannot_handle!(_anyone, _any_action) # everyone else CANNOT do anything
end
# If rejected check database to see if I have explicit permissions
phase :registery_check do
can_handle?(user, action, conditions) do
Repo.one(from r in PermissionsRegistory, where r.user_id == ^user.id and r.action == ^action and r.conditions == ^conditions) != nil
end
end
end
So the good part to this is that I have a very clean controller, and the mapping/validation/auth logic is all separated. The bad parts are that I have a bunch of magic happening, and also I feel slightly disgusted in myself for using attributes a base controller as it feels very java like.
What are you thoughts on all of this?
Chris