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

Invites to join an Org require the user to accept the invite #1346

Draft
wants to merge 18 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 39 additions & 2 deletions lib/nerves_hub/accounts.ex
Original file line number Diff line number Diff line change
Expand Up @@ -424,8 +424,12 @@ defmodule NervesHub.Accounts do
| {:error, Changeset.t()}
def add_or_invite_to_org(%{"email" => email} = params, org, invited_by) do
case get_user_by_email(email) do
{:error, :not_found} -> invite(params, org, invited_by)
{:ok, user} -> add_org_user(org, user, %{role: params["role"]})
{:error, :not_found} ->
invite(params, org, invited_by)

{:ok, _user} ->
invite(params, org, invited_by)
# add_org_user(org, user, %{role: params["role"]})
end
end

Expand Down Expand Up @@ -473,6 +477,14 @@ defmodule NervesHub.Accounts do
|> Repo.all()
end

@spec get_invites_for_user(User.t()) :: [Invite.t()]
def get_invites_for_user(user) do
Invite
|> where([i], i.email == ^user.email)
|> where([i], i.accepted == false)
|> Repo.all()
end

def delete_invite(org, token) do
query =
Invite
Expand Down Expand Up @@ -513,6 +525,31 @@ defmodule NervesHub.Accounts do
end)
end

@spec accept_invite(Invite.t(), Org.t()) ::
{:ok, OrgUser.t()} | {:error, Ecto.Changeset.t()}
def accept_invite(invite, org) do
Repo.transaction(fn ->
with {:ok, user} <- get_user_by_email(invite.email),
{:ok, user} <- add_org_user(org, user, %{role: invite.role}),
{:ok, _invite} <- set_invite_accepted(invite) do
# Repo.transaction will wrap this in an {:ok, user}
user
else
{:error, error} -> Repo.rollback(error)
end
end)
end

@spec user_invite_recipient?(Invite.t(), User.t()) ::
{:ok, Invite.t()} | {:error, :invite_not_for_user}
def user_invite_recipient?(invite, user) do
if invite.email == user.email do
{:ok, invite}
else
{:error, :invite_not_for_user}
end
end

@spec update_user(User.t(), map) ::
{:ok, User.t()}
| {:error, Changeset.t()}
Expand Down
102 changes: 88 additions & 14 deletions lib/nerves_hub_web/controllers/account_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,32 @@ defmodule NervesHubWeb.AccountController do
alias NervesHub.Accounts.{User, SwooshEmail}
alias NervesHub.SwooshMailer

import Phoenix.HTML.Link

plug(:registrations_allowed when action in [:new, :create])

def new(conn, _params) do
render(conn, "new.html", changeset: Ecto.Changeset.change(%User{}))
end

def create(conn, %{"user" => user_params} = _) do
case Accounts.create_user(user_params) do
{:ok, new_user} ->
new_user
|> SwooshEmail.welcome_user()
|> SwooshMailer.deliver()
def delete(conn, %{"user_name" => username}) do
with {:ok, user} <- Accounts.get_user_by_username(username),
{:ok, _} <- Accounts.remove_account(user.id) do
conn
|> put_flash(:info, "Success")
|> redirect(to: "/login")
end
end

def update(conn, params) do
cleaned =
params["user"]
|> whitelist([:current_password, :password, :username, :email, :orgs])

conn.assigns.user
|> Accounts.update_user(cleaned)
|> case do
{:ok, _user} ->
conn
|> put_flash(:info, "Account successfully created, login below")
|> redirect(to: "/login")
Expand All @@ -31,13 +44,43 @@ defmodule NervesHubWeb.AccountController do
def invite(conn, %{"token" => token} = _) do
with {:ok, invite} <- Accounts.get_valid_invite(token),
{:ok, org} <- Accounts.get_org(invite.org_id) do
render(
conn,
"invite.html",
changeset: %Changeset{data: invite},
org: org,
token: token
)
# QUESTION: Should this be here raw or in a method somewhere else?
case Map.has_key?(conn.assigns, :user) && !is_nil(conn.assigns.user) do
true ->
if invite.email == conn.assigns.user.email do
render(
conn,
# QUESTION: Should this be a separate template or the same one with conditional rendering?
"invite_existing.html",
changeset: %Changeset{data: invite},
org: org,
token: token
)
else
conn
|> put_flash(:error, "Invite not intended for the current user")
|> redirect(to: "/")
end

false ->
case Accounts.get_user_by_email(invite.email) do
# Invites for existing users
{:ok, _recipient} ->
conn
|> put_flash(:error, "You must be logged in to accept this invite")
|> redirect(to: "/login")

# Invites for new users
{:error, :not_found} ->
render(
conn,
"invite.html",
changeset: %Changeset{data: invite},
org: org,
token: token
)
end
end
else
_ ->
conn
Expand All @@ -47,9 +90,11 @@ defmodule NervesHubWeb.AccountController do
end

def accept_invite(conn, %{"user" => user_params, "token" => token} = _) do
clean_params = whitelist(user_params, [:password, :username])

with {:ok, invite} <- Accounts.get_valid_invite(token),
{:ok, org} <- Accounts.get_org(invite.org_id) do
_accept_invite(conn, token, user_params, invite, org)
_accept_invite(conn, token, clean_params, invite, org)
else
{:error, :invite_not_found} ->
conn
Expand Down Expand Up @@ -92,6 +137,35 @@ defmodule NervesHubWeb.AccountController do
end
end

def maybe_show_invites(conn) do
case Map.has_key?(conn.assigns, :user) && !is_nil(conn.assigns.user) do
true ->
case conn.assigns.user
|> Accounts.get_invites_for_user() do
[] ->
conn

invites ->
conn
|> put_flash(
:info,
[
"You have " <>
(length(invites) |> Integer.to_string()) <>
" pending invite" <>
if(length(invites) > 1, do: "s", else: "") <> " to organizations. ",
link("Click here to view pending invites.",
to: "/org/" <> conn.assigns.user.username <> "/invites"
)
]
)
end

false ->
conn
end
end

defp registrations_allowed(conn, _options) do
if Application.get_env(:nerves_hub, :open_for_registrations) do
conn
Expand Down
6 changes: 5 additions & 1 deletion lib/nerves_hub_web/controllers/home_controller.ex
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
defmodule NervesHubWeb.HomeController do
use NervesHubWeb, :controller

alias NervesHubWeb.AccountController

def index(conn, _params) do
redirect(conn, to: ~p"/orgs")
conn
|> AccountController.maybe_show_invites()
|> redirect(to: ~p"/orgs")
end
end
6 changes: 6 additions & 0 deletions lib/nerves_hub_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,12 @@ defmodule NervesHubWeb.Router do

get("/invite/:token", AccountController, :invite)
post("/invite/:token", AccountController, :accept_invite)

scope "/invites/:user_name" do
pipe_through([:logged_in])

get("/", AccountController, :invites)
end
end

scope "/", NervesHubWeb do
Expand Down
12 changes: 12 additions & 0 deletions lib/nerves_hub_web/templates/account/invite_existing.html.heex
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<div class="form-page-wrapper">
<h2 class="form-title">
Organization Invitation
</h2>

<h5 class="mt-2"><%= gettext("You have been invited to join to the %{organization_name} organization", organization_name: @org.name) %></h5>

<%= form_for @changeset, Routes.account_path(@conn, :accept_invite, @token), [method: "post", class: "form-page"], fn f -> %>
<div class="has-error"><%= error_tag(f, :email) %></div>
<%= submit("Accept Invitation", class: "btn btn-primary btn-lg w-100") %>
<% end %>
</div>