pow-auth / pow_site

Website for Pow
https://powauth.com
MIT License
4 stars 2 forks source link

Guide on how to set up organization #11

Open danschultzer opened 4 years ago

danschultzer commented 4 years ago

This would be a good newbie guide, based on this post in the elixir forum:

Hi,

Newbie pow question here! (Thanks for the awesome work on this by the way)

I just read around a bit about pow and it looks really nice. I’m working my way trough my first real Phoenix project. I would like to have accounts authentication with users and parent organizations. I have seen in the documentation some examples that mention this (in the invitation section). Is there any introduction level tutorial on how to set it up correctly?

This would be a very easy guide to write. It can go into PowInvitation, roles and/or user management within the organization, and maybe on how to limit organizations to a certain subdomain (so users in a certain organization can only sign in on that subdomain). Any ideas are welcome!

danschultzer commented 4 years ago

I'll write some short instructions here to answer the above post.


Create the organization and add a migration to add the organization reference:

mix phx.gen.context Organizations Organization organizations name:string
mix ecto.gen.migration add_organization_to_users

Update add_organization_to_users.exs migration file:

defmodule MyApp.Repo.Migrations.AddOrganizationToUsers do
  use Ecto.Migration

  def change do
    alter table(:users) do
      add :organization_id, references("organizations", on_delete: :delete_all), null: false
    end
  end
end

And update the user to restraint them to organizations:

defmodule MyApp.Users.User do
  # ...
  alias MyApp.Organizations.Organization

  schema "users" do
    belongs_to :organization, Organization

    pow_user_fields()

    timestamp()
  end

  def changeset(user_or_changeset, attrs) do
    user_or_changeset
    |> pow_changeset(attrs)
    |> pow_extension_changeset(attrs)
    |> Ecto.Changeset.assoc_constraint(:organization)
  end

  def invite_changeset(user_or_changeset, invited_by, attrs) do
    user_or_changeset
    |> pow_invite_changeset(invited_by, attrs)
    |> changeset_organization(invited_by)
  end

  defp changeset_organization(changeset, invited_by) do
    Ecto.Changeset.change(changeset, organization_id: invited_by.organization_id)
  end

  # ...
end

Update the organizations schema to require an initial user:

defmodule MyApp.Organizations.Organization do
  use Ecto.Schema
  import Ecto.Changeset
  alias MyApp.Users.User

  schema "organizations" do
    field :name, :string
    has_many :users, User, on_delete: :delete_all

    timestamps()
  end

  @doc false
  def changeset(organization, attrs) do
    organization
    |> cast(attrs, [:name])
    |> validate_required([:name])
  end

  defp changeset_users(changeset) do
    case Ecto.get_meta(changeset.data, :state) do
      :built -> cast_assoc(changeset, :users, required: true)
      _any  -> any
  end
end

Set up a controller action to create the organization:

defmodule MyAppWeb.RegistrationController do
  use MyAppWeb, :controller

  alias MyApp.{Organizations.Organization, Repo}

  def new(conn, _params) do
    changeset = Organization.changeset(%Organization{}, %{})

    render(conn, "new.html", changeset: changeset)
  end

  def create(conn, %{"organization" => user_params}) do
   %Organization{}
    |> Organization.changeset(user_params)
    |> Repo.insert()
    |> case do
      {:ok, organization} ->
        conn
        |> auth_user(organization.users)
        |> put_flash(:info, "Welcome!")
        |> redirect(to: Routes.page_path(conn, :index))

      {:error, changeset} ->
        render(conn, "new.html", changeset: changeset)
    end
  end

  defp auth_user(conn, [user]) do
    config = Pow.Plug.fetch_config(conn)

    Pow.Plug.get_plug(config).do_create(conn, user, config)
  end
end

The form could look like this:

<%= form_for @changeset, Routes.organization_path(@conn, :create), fn f -> %>
  <%= text_input f, :name %>

  <%= inputs_for f, :users, [append: [%MyApp.Users.User{}]], fn f_user -> %>
    <%= label f_user, :email, "email" %>
    <%= email_input f_user, :email %>
    <%= error_tag f_user, :email %>

    <%= label f_user, :password, "password" %>
    <%= password_input f_user, :password %>
    <%= error_tag f_user, :password %>

    <%= label f_user, :confirm_password, "confirm_password" %>
    <%= password_input f_user, :confirm_password %>
    <%= error_tag f_user, :confirm_password %>
  <% end %>
<% end %>
elalaouifaris commented 4 years ago

Thank you for the instructions. Could you assign this one to me? I'll start working on it.

Thanks!

elalaouifaris commented 4 years ago

I started implementing the proposed workflow along these steps:

I have an issue with registration view: We can not combine default: on line 4 with changeset. When I remove this, the user fields do not show up. Do you know how to solve this part?

The code is here any other comments are most welcome!

Thank you for your help!

danschultzer commented 4 years ago

Hmm, what kind of error do you get?

Also, you can do the rest in in much less code by overriding the registration :new and :create route with a organization controller.

  scope "/", MyAppWeb do
    pipe_through [:browser]

    resources "/registration", OrganizationController, singleton: true, only: [:new, :create]
  end

  scope "/" do
    pipe_through :browser

    pow_routes()
  end
defmodule MyAppWeb.OrganizationController do
  use MyAppWeb, :controller

  alias MyApp.{Organizations.Organization, Repo}

  def new(conn, _params) do
    changeset = Organization.changeset(%Organization{}, %{})

    render(conn, "new.html", changeset: changeset)
  end

  def create(conn, %{"organization" => organization_params}) do
    %Organization{}
    |> Organization.changeset(user_params)
    |> Repo.insert()
    |> case do
      {:ok, organization} ->
        conn
        |> auth_user(organization.users)
        |> put_flash(:info, "Welcome!")
        |> redirect(to: Routes.page_path(conn, :index))

      {:error, changeset} ->
        render(conn, "new.html", changeset: changeset)
    end
  end

  defp auth_user(conn, [user]) do
    config = Pow.Plug.fetch_config(conn)

    Pow.Plug.get_plug(config).do_create(conn, user, config)
  end
end
# organization new.html.eex
<%= form_for @changeset, Routes.organization_path(@conn, :create), fn f -> %>
  <%= text_input f, :name %>

  <%= inputs_for f, :users, [append: [%MyApp.Users.User{}]], fn f_user -> %>
    <%= label f_user, :email, "email" %>
    <%= email_input f_user, :email %>
    <%= error_tag f_user, :email %>

    <%= label f_user, :password, "password" %>
    <%= password_input f_user, :password %>
    <%= error_tag f_user, :password %>

    <%= label f_user, :confirm_password, "confirm_password" %>
    <%= password_input f_user, :confirm_password %>
    <%= error_tag f_user, :confirm_password %>
  <% end %>
<% end %>

This way you don't need to override anything else in Pow, just the routes 😄 Also the organization constraint in the user changeset will prevent users from signing up through the regular Pow registration controller if you somehow inadvertently expose it, so it's safe.

elalaouifaris commented 4 years ago

Thank you! I updated the branch as you suggested. Much cleaner indeed!

The error I have is: ArgumentError at GET /registration/new :default is not supported on inputs_for with changesets. The default value must be set in the changeset data

It's originating from lib/phoenix_ecto/html.ex line 23.. The source of the problem in my code is the line 4 of the view with the :default argument.

danschultzer commented 4 years ago

Oh, try use :append instead: <%= inputs_for f, :users, [append: [%MyApp.Users.User{}]], fn f_user -> %>

elalaouifaris commented 4 years ago

It works but only with a list of: [%User{}] instead of %User{} directly. <%= inputs_for f, :users, [append: [%MyApp.Users.User{}]], fn f_user -> %> I get a "protocol Enumerable not implemented for %User{} otherwise.

Is it OK to solve this error like this?

danschultzer commented 4 years ago

Yeah, it was my mistake. Updated the examples!