defmodule PlausibleWeb.Site.MembershipController do @moduledoc """ This controller deals with user management via the UI in Site Settings -> People. It's important to enforce permissions in this controller. Owner and Admin - Can manage users, can trigger a 'transfer ownership' request Editor and Viewer - Can not access user management settings Anyone - Can accept invitations Everything else should be explicitly disallowed. """ use PlausibleWeb, :controller use Plausible.Repo use Plausible alias Plausible.Teams plug PlausibleWeb.RequireAccountPlug plug PlausibleWeb.Plugs.AuthorizeSiteAccess, [:owner, :admin] def invite_member_form(conn, _params) do site = conn.assigns.current_user |> Plausible.Sites.get_for_user!(conn.assigns.site.domain) |> Plausible.Repo.preload(:owners) limit = Teams.Billing.team_member_limit(site.team) usage = Teams.Billing.team_member_usage(site.team) below_limit? = Plausible.Billing.Quota.below_limit?(usage, limit) render( conn, "invite_member_form.html", site: site, team_member_limit: limit, is_at_limit: not below_limit?, skip_plausible_tracking: true ) end def invite_member(conn, %{"email" => email, "role" => role}) do site_domain = conn.assigns.site.domain site = Plausible.Sites.get_for_user!(conn.assigns.current_user, site_domain) |> Plausible.Repo.preload(:owners) case Teams.Invitations.InviteToSite.invite( site, conn.assigns.current_user, email, role ) do {:ok, invitation} -> conn |> put_flash( :success, "#{email} has been invited to #{site_domain} as #{PlausibleWeb.SiteView.with_indefinite_article("#{invitation.role}")}" ) |> redirect(to: Routes.site_path(conn, :settings_people, site.domain)) {:error, :already_a_member} -> render(conn, "invite_member_form.html", error: "Cannot send invite because #{email} is already a member of #{site.domain}", site: site, skip_plausible_tracking: true ) {:error, {:over_limit, limit}} -> render(conn, "invite_member_form.html", error: "Your account is limited to #{limit} team members. You can upgrade your plan to increase this limit.", site: site, skip_plausible_tracking: true, is_at_limit: true, team_member_limit: limit ) {:error, %Ecto.Changeset{} = changeset} -> error_msg = case changeset.errors[:invitation] do {"already sent", _} -> "This invitation has been already sent. To send again, remove it from pending invitations first." _ -> "Something went wrong." end conn |> put_flash(:error, error_msg) |> redirect(to: Routes.site_path(conn, :settings_people, site.domain)) end end @doc """ Updates the role of a user. The user being updated could be the same or different from the user taking the action. When updating the role, it's important to enforce permissions: Owner - Can update anyone's role except for themselves. If they want to change their own role, they have to use the 'transfer ownership' feature. Admin - Can update anyone's role except for owners. Can downgrade their own access to 'viewer'. Can promote a viewer to admin. """ def update_role_by_user(conn, %{"id" => user_id, "new_role" => new_role_str}) do %{site: site, current_user: current_user, site_role: site_role} = conn.assigns case Teams.Memberships.update_role( site, user_id, new_role_str, current_user, site_role ) do {:ok, guest_membership} -> redirect_target = if guest_membership.team_membership.user_id == current_user.id and guest_membership.role == :viewer do Routes.stats_path(conn, :stats, site.domain, []) else Routes.site_path(conn, :settings_people, site.domain) end conn |> put_flash( :success, "#{guest_membership.team_membership.user.name} is now #{PlausibleWeb.SiteView.with_indefinite_article(to_string(guest_membership.role))}" ) |> redirect(to: redirect_target) {:error, _} -> conn |> put_flash(:error, "You are not allowed to grant the #{new_role_str} role") |> redirect(to: Routes.site_path(conn, :settings_people, site.domain)) end end def remove_member_by_user(conn, %{"id" => user_id} = _params) do site = conn.assigns.site if user = Repo.get(Plausible.Auth.User, user_id) do Teams.Memberships.remove(site, user) redirect_target = if user_id == conn.assigns[:current_user].id do Routes.site_path(conn, :index) else Routes.site_path(conn, :settings_people, site.domain) end conn |> put_flash( :success, "#{user.name} has been removed from #{site.domain}" ) |> redirect(to: redirect_target) else conn |> put_flash( :success, "User has been removed from #{site.domain}" ) |> redirect(to: Routes.site_path(conn, :settings_people, site.domain)) end end end