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

Guide on how to set up organization #11

Open
danschultzer opened this issue Sep 24, 2019 · 8 comments
Open

Guide on how to set up organization #11

danschultzer opened this issue Sep 24, 2019 · 8 comments
Assignees
Labels
documentation Improvements or additions to documentation good first issue Good for newcomers

Comments

@danschultzer
Copy link
Collaborator

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 danschultzer added documentation Improvements or additions to documentation good first issue Good for newcomers labels Sep 24, 2019
@danschultzer
Copy link
Collaborator Author

danschultzer commented Sep 24, 2019

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
Copy link

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

Thanks!

@elalaouifaris
Copy link

I started implementing the proposed workflow along these steps:

  • Basic pow setup + organization resources
  • Add the custom controller / session setup as proposed (without the invite_changeset for now)
  • (Not done yet) Add email invitation

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
Copy link
Collaborator Author

danschultzer commented Sep 26, 2019

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
Copy link

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
Copy link
Collaborator Author

danschultzer commented Sep 26, 2019

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

@elalaouifaris
Copy link

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
Copy link
Collaborator Author

Yeah, it was my mistake. Updated the examples!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation good first issue Good for newcomers
Projects
None yet
Development

No branches or pull requests

2 participants