- add user management to invite page
	
		
			
	
		
	
	
		
	
		
			Some checks reported errors
		
		
	
	
		
			
				
	
				continuous-integration/drone/push Build was killed
				
			
		
		
	
	
				
					
				
			
		
			Some checks reported errors
		
		
	
	continuous-integration/drone/push Build was killed
				
			- harden accounts context
This commit is contained in:
		
							
								
								
									
										39
									
								
								lib/cannery_web/components/invite_card.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								lib/cannery_web/components/invite_card.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | ||||
| defmodule CanneryWeb.Components.InviteCard do | ||||
|   @moduledoc """ | ||||
|   Display card for an invite | ||||
|   """ | ||||
|  | ||||
|   use CanneryWeb, :component | ||||
|   alias CanneryWeb.Endpoint | ||||
|  | ||||
|   def invite_card(assigns) do | ||||
|     ~H""" | ||||
|     <div class="px-8 py-4 flex flex-col justify-center items-center space-y-4 | ||||
|       border border-gray-400 rounded-lg shadow-lg hover:shadow-md"> | ||||
|       <h1 class="title text-xl"> | ||||
|         <%= @invite.name %> | ||||
|       </h1> | ||||
|  | ||||
|       <%= if @invite.disabled_at |> is_nil() do %> | ||||
|         <h2 class="title text-md"> | ||||
|           <%= gettext("Uses Left:") %> | ||||
|           <%= @invite.uses_left || "Unlimited" %> | ||||
|         </h2> | ||||
|       <% else %> | ||||
|         <h2 class="title text-md"> | ||||
|           <%= gettext("Invite Disabled") %> | ||||
|         </h2> | ||||
|       <% end %> | ||||
|  | ||||
|       <code class="text-xs px-4 py-2 rounded-lg text-gray-100 bg-primary-800"><%= Routes.user_registration_url(Endpoint, :new, invite: @invite.token) %> | ||||
|       </code> | ||||
|  | ||||
|       <%= if @inner_block do %> | ||||
|         <div class="flex space-x-4 justify-center items-center"> | ||||
|           <%= render_slot(@inner_block) %> | ||||
|         </div> | ||||
|       <% end %> | ||||
|     </div> | ||||
|     """ | ||||
|   end | ||||
| end | ||||
							
								
								
									
										35
									
								
								lib/cannery_web/components/user_card.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								lib/cannery_web/components/user_card.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| defmodule CanneryWeb.Components.UserCard do | ||||
|   @moduledoc """ | ||||
|   Display card for a user | ||||
|   """ | ||||
|  | ||||
|   use CanneryWeb, :component | ||||
|  | ||||
|   def user_card(assigns) do | ||||
|     ~H""" | ||||
|     <div | ||||
|       id={"user-#{@user.id}"} | ||||
|       class="mx-4 my-2 px-8 py-4 flex flex-col justify-center items-center | ||||
|           border border-gray-400 rounded-lg shadow-lg hover:shadow-md" | ||||
|     > | ||||
|       <h1 class="px-4 py-2 rounded-lg title text-xl"> | ||||
|         <%= @user.email %> | ||||
|       </h1> | ||||
|  | ||||
|       <h3 class="px-4 py-2 rounded-lg title text-lg"> | ||||
|         <%= if @user.confirmed_at |> is_nil() do %> | ||||
|           Email unconfirmed | ||||
|         <% else %> | ||||
|           User was confirmed at <%= @user.confirmed_at |> display_datetime() %> | ||||
|         <% end %> | ||||
|       </h3> | ||||
|  | ||||
|       <%= if @inner_block do %> | ||||
|         <div class="px-4 py-2 flex space-x-4 justify-center items-center"> | ||||
|           <%= render_slot(@inner_block) %> | ||||
|         </div> | ||||
|       <% end %> | ||||
|     </div> | ||||
|     """ | ||||
|   end | ||||
| end | ||||
| @@ -3,7 +3,7 @@ defmodule CanneryWeb.UserRegistrationController do | ||||
|   import CanneryWeb.Gettext | ||||
|   alias Cannery.{Accounts, Invites} | ||||
|   alias Cannery.Accounts.User | ||||
|   alias CanneryWeb.{HomeLive, UserAuth} | ||||
|   alias CanneryWeb.{Endpoint, HomeLive} | ||||
|  | ||||
|   def new(conn, %{"invite" => invite_token}) do | ||||
|     invite = Invites.get_invite_by_token(invite_token) | ||||
| @@ -13,7 +13,7 @@ defmodule CanneryWeb.UserRegistrationController do | ||||
|     else | ||||
|       conn | ||||
|       |> put_flash(:error, dgettext("errors", "Sorry, this invite was not found or expired")) | ||||
|       |> redirect(to: Routes.live_path(CanneryWeb.Endpoint, HomeLive)) | ||||
|       |> redirect(to: Routes.live_path(Endpoint, HomeLive)) | ||||
|     end | ||||
|   end | ||||
|  | ||||
| @@ -23,7 +23,7 @@ defmodule CanneryWeb.UserRegistrationController do | ||||
|     else | ||||
|       conn | ||||
|       |> put_flash(:error, dgettext("errors", "Sorry, public registration is disabled")) | ||||
|       |> redirect(to: Routes.live_path(CanneryWeb.Endpoint, HomeLive)) | ||||
|       |> redirect(to: Routes.live_path(Endpoint, HomeLive)) | ||||
|     end | ||||
|   end | ||||
|  | ||||
| @@ -41,7 +41,7 @@ defmodule CanneryWeb.UserRegistrationController do | ||||
|     else | ||||
|       conn | ||||
|       |> put_flash(:error, dgettext("errors", "Sorry, this invite was not found or expired")) | ||||
|       |> redirect(to: Routes.live_path(CanneryWeb.Endpoint, HomeLive)) | ||||
|       |> redirect(to: Routes.live_path(Endpoint, HomeLive)) | ||||
|     end | ||||
|   end | ||||
|  | ||||
| @@ -51,7 +51,7 @@ defmodule CanneryWeb.UserRegistrationController do | ||||
|     else | ||||
|       conn | ||||
|       |> put_flash(:error, dgettext("errors", "Sorry, public registration is disabled")) | ||||
|       |> redirect(to: Routes.live_path(CanneryWeb.Endpoint, HomeLive)) | ||||
|       |> redirect(to: Routes.live_path(Endpoint, HomeLive)) | ||||
|     end | ||||
|   end | ||||
|  | ||||
| @@ -68,8 +68,8 @@ defmodule CanneryWeb.UserRegistrationController do | ||||
|         ) | ||||
|  | ||||
|         conn | ||||
|         |> put_flash(:info, dgettext("prompts", "User created successfully.")) | ||||
|         |> UserAuth.log_in_user(user) | ||||
|         |> put_flash(:info, dgettext("prompts", "Please check your email to verify your account")) | ||||
|         |> redirect(to: Routes.user_session_path(Endpoint, :new)) | ||||
|  | ||||
|       {:error, %Ecto.Changeset{} = changeset} -> | ||||
|         render(conn, "new.html", changeset: changeset, invite: invite) | ||||
|   | ||||
| @@ -4,9 +4,8 @@ defmodule CanneryWeb.InviteLive.Index do | ||||
|   """ | ||||
|  | ||||
|   use CanneryWeb, :live_view | ||||
|  | ||||
|   alias Cannery.Invites | ||||
|   alias Cannery.Invites.Invite | ||||
|   import CanneryWeb.Components.{InviteCard, UserCard} | ||||
|   alias Cannery.{Accounts, Invites, Invites.Invite} | ||||
|   alias CanneryWeb.{Endpoint, HomeLive} | ||||
|  | ||||
|   @impl true | ||||
| @@ -44,7 +43,11 @@ defmodule CanneryWeb.InviteLive.Index do | ||||
|   end | ||||
|  | ||||
|   @impl true | ||||
|   def handle_event("delete", %{"id" => id}, %{assigns: %{current_user: current_user}} = socket) do | ||||
|   def handle_event( | ||||
|         "delete_invite", | ||||
|         %{"id" => id}, | ||||
|         %{assigns: %{current_user: current_user}} = socket | ||||
|       ) do | ||||
|     %{name: invite_name} = | ||||
|       id |> Invites.get_invite!(current_user) |> Invites.delete_invite!(current_user) | ||||
|  | ||||
| @@ -106,7 +109,30 @@ defmodule CanneryWeb.InviteLive.Index do | ||||
|     {:noreply, socket} | ||||
|   end | ||||
|  | ||||
|   @impl true | ||||
|   def handle_event( | ||||
|         "delete_user", | ||||
|         %{"id" => id}, | ||||
|         %{assigns: %{current_user: current_user}} = socket | ||||
|       ) do | ||||
|     %{email: user_email} = Accounts.get_user!(id) |> Accounts.delete_user!(current_user) | ||||
|  | ||||
|     prompt = dgettext("prompts", "%{name} deleted succesfully", name: user_email) | ||||
|  | ||||
|     {:noreply, socket |> put_flash(:info, prompt) |> display_invites()} | ||||
|   end | ||||
|  | ||||
|   defp display_invites(%{assigns: %{current_user: current_user}} = socket) do | ||||
|     socket |> assign(:invites, Invites.list_invites(current_user)) | ||||
|     invites = Invites.list_invites(current_user) | ||||
|  | ||||
|     all_users = Accounts.list_all_users_by_role(current_user) | ||||
|  | ||||
|     admins = | ||||
|       all_users | ||||
|       |> Map.get(:admin, []) | ||||
|       |> Enum.reject(fn %{id: user_id} -> user_id == current_user.id end) | ||||
|  | ||||
|     users = all_users |> Map.get(:user, []) | ||||
|     socket |> assign(invites: invites, admins: admins, users: users) | ||||
|   end | ||||
| end | ||||
|   | ||||
							
								
								
									
										136
									
								
								lib/cannery_web/live/invite_live/index.html.heex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								lib/cannery_web/live/invite_live/index.html.heex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,136 @@ | ||||
| <div class="flex flex-col space-y-8 justify-center items-center"> | ||||
|   <h1 class="title text-2xl title-primary-500"> | ||||
|     <%= gettext("Listing Invites") %> | ||||
|   </h1> | ||||
|  | ||||
|   <%= if @invites |> Enum.empty?() do %> | ||||
|     <h1 class="title text-xl text-primary-500"> | ||||
|       <%= gettext("No invites") %> 😔 | ||||
|     </h1> | ||||
|  | ||||
|     <%= live_patch(dgettext("actions", "Invite someone new!"), | ||||
|       to: Routes.invite_index_path(@socket, :new), | ||||
|       class: "btn btn-primary" | ||||
|     ) %> | ||||
|   <% else %> | ||||
|     <%= live_patch(dgettext("actions", "Create Invite"), | ||||
|       to: Routes.invite_index_path(@socket, :new), | ||||
|       class: "btn btn-primary" | ||||
|     ) %> | ||||
|   <% end %> | ||||
|  | ||||
|   <div class="flex flex-row flex-wrap space-x-4 space-y-4"> | ||||
|     <%= for invite <- @invites do %> | ||||
|       <.invite_card invite={invite}> | ||||
|         <%= live_patch to: Routes.invite_index_path(Endpoint, :edit, invite), | ||||
|                    class: "text-primary-500 link" do %> | ||||
|           <i class="fa-fw fa-lg fas fa-edit"></i> | ||||
|         <% end %> | ||||
|  | ||||
|         <%= link to: "#", | ||||
|              class: "text-primary-500 link", | ||||
|              phx_click: "delete_invite", | ||||
|              phx_value_id: invite.id, | ||||
|              data: [ | ||||
|                confirm: | ||||
|                  dgettext("prompts", "Are you sure you want to delete the invite for %{name}?", | ||||
|                    name: invite.name | ||||
|                  ) | ||||
|              ] do %> | ||||
|           <i class="fa-fw fa-lg fas fa-trash"></i> | ||||
|         <% end %> | ||||
|  | ||||
|         <%= if invite.disabled_at |> is_nil() do %> | ||||
|           <a href="#" class="btn btn-primary" phx-click="disable_invite" phx-value-id={invite.id}> | ||||
|             <%= gettext("Disable") %> | ||||
|           </a> | ||||
|         <% else %> | ||||
|           <a href="#" class="btn btn-primary" phx-click="enable_invite" phx-value-id={invite.id}> | ||||
|             <%= gettext("Enable") %> | ||||
|           </a> | ||||
|         <% end %> | ||||
|  | ||||
|         <%= if invite.disabled_at |> is_nil() and not (invite.uses_left |> is_nil()) do %> | ||||
|           <a | ||||
|             href="#" | ||||
|             class="btn btn-primary" | ||||
|             phx-click="set_unlimited" | ||||
|             phx-value-id={invite.id} | ||||
|             data-confirm={dgettext("prompts", "Are you sure you want to make %{name} unlimited?", name: invite.name)} | ||||
|           > | ||||
|             <%= gettext("Set Unlimited") %> | ||||
|           </a> | ||||
|         <% end %> | ||||
|       </.invite_card> | ||||
|     <% end %> | ||||
|   </div> | ||||
|  | ||||
|   <%= unless @admins |> Enum.empty?() do %> | ||||
|     <hr class="hr"> | ||||
|  | ||||
|     <div class="flex flex-col justify-center items-center space-y-4"> | ||||
|       <h1 class="title text-xl text-primary-500"> | ||||
|         <%= gettext("Admins") %> | ||||
|       </h1> | ||||
|  | ||||
|       <%= for admin <- @admins do %> | ||||
|         <.user_card user={admin}> | ||||
|           <%= link to: "#", | ||||
|                class: "text-primary-500 link", | ||||
|                phx_click: "delete_user", | ||||
|                phx_value_id: admin.id, | ||||
|                data: [ | ||||
|                  confirm: | ||||
|                    dgettext( | ||||
|                      "prompts", | ||||
|                      "Are you sure you want to delete %{email}? This action is permanent!", | ||||
|                      email: admin.email | ||||
|                    ) | ||||
|                ] do %> | ||||
|             <i class="fa-fw fa-lg fas fa-trash"></i> | ||||
|           <% end %> | ||||
|         </.user_card> | ||||
|       <% end %> | ||||
|     </div> | ||||
|   <% end %> | ||||
|  | ||||
|   <%= unless @users |> Enum.empty?() do %> | ||||
|     <hr class="hr"> | ||||
|  | ||||
|     <div class="flex flex-col justify-center items-center space-y-4"> | ||||
|       <h1 class="title text-xl text-primary-500"> | ||||
|         <%= gettext("Users") %> | ||||
|       </h1> | ||||
|  | ||||
|       <%= for user <- @users do %> | ||||
|         <.user_card user={user}> | ||||
|           <%= link to: "#", | ||||
|                class: "text-primary-500 link", | ||||
|                phx_click: "delete_user", | ||||
|                phx_value_id: user.id, | ||||
|                data: [ | ||||
|                  confirm: | ||||
|                    dgettext( | ||||
|                      "prompts", | ||||
|                      "Are you sure you want to delete %{email}? This action is permanent!", | ||||
|                      email: user.email | ||||
|                    ) | ||||
|                ] do %> | ||||
|             <i class="fa-fw fa-lg fas fa-trash"></i> | ||||
|           <% end %> | ||||
|         </.user_card> | ||||
|       <% end %> | ||||
|     </div> | ||||
|   <% end %> | ||||
| </div> | ||||
|  | ||||
| <%= if @live_action in [:new, :edit] do %> | ||||
|   <%= live_modal(CanneryWeb.InviteLive.FormComponent, | ||||
|     id: @invite.id || :new, | ||||
|     title: @page_title, | ||||
|     action: @live_action, | ||||
|     invite: @invite, | ||||
|     return_to: Routes.invite_index_path(@socket, :index), | ||||
|     current_user: @current_user | ||||
|   ) %> | ||||
| <% end %> | ||||
| @@ -1,90 +0,0 @@ | ||||
| <div class="flex flex-col space-y-8 justify-center items-center"> | ||||
|   <h1 class="title text-2xl title-primary-500"> | ||||
|     <%= gettext("Listing Invites") %> | ||||
|   </h1> | ||||
|  | ||||
|   <%= if @invites |> Enum.empty?() do %> | ||||
|     <h1 class="title text-xl text-primary-500"> | ||||
|       <%= gettext("No invites") %> 😔 | ||||
|     </h1> | ||||
|  | ||||
|     <%= live_patch dgettext("actions", "Invite someone new!"), | ||||
|       to: Routes.invite_index_path(@socket, :new), | ||||
|       class: "btn btn-primary" %> | ||||
|   <% else %> | ||||
|     <%= live_patch dgettext("actions", "Create Invite"), | ||||
|       to: Routes.invite_index_path(@socket, :new), | ||||
|       class: "btn btn-primary" %> | ||||
|   <% end %> | ||||
|  | ||||
|   <div class="flex flex-row flex-wrap space-x-4 space-y-4"> | ||||
|     <%= for invite <- @invites do %> | ||||
|       <div class="px-8 py-4 flex flex-col justify-center items-center space-y-4 | ||||
|         border border-gray-400 rounded-lg shadow-lg hover:shadow-md"> | ||||
|         <h1 class="title text-xl"> | ||||
|           <%= invite.name %> | ||||
|         </h1> | ||||
|  | ||||
|         <%= if invite.disabled_at |> is_nil() do %> | ||||
|           <h2 class="title text-md"> | ||||
|             <%= gettext("Uses Left:") %> | ||||
|             <%= invite.uses_left || "Unlimited" %> | ||||
|           </h2> | ||||
|         <% else %> | ||||
|           <h2 class="title text-md"> | ||||
|             <%= gettext("Invite Disabled") %> | ||||
|           </h2> | ||||
|         <% end %> | ||||
|  | ||||
|         <code class="text-xs px-4 py-2 rounded-lg text-gray-100 bg-primary-800"> | ||||
|           <%= Routes.user_registration_url(@socket, :new, invite: invite.token) %> | ||||
|         </code> | ||||
|  | ||||
|         <div class="flex space-x-4 justify-center items-center"> | ||||
|           <%= live_patch to: Routes.invite_index_path(@socket, :edit, invite), | ||||
|                     class: "text-primary-500 link" do %> | ||||
|             <i class="fa-fw fa-lg fas fa-edit"></i> | ||||
|           <% end %> | ||||
|  | ||||
|           <%= link to: "#", | ||||
|               class: "text-primary-500 link", | ||||
|               phx_click: "delete", | ||||
|             phx_value_id: invite.id, | ||||
|             data: [confirm: dgettext("prompts", "Are you sure you want to delete the invite for %{name}?", name: invite.name)] do %> | ||||
|             <i class="fa-fw fa-lg fas fa-trash"></i> | ||||
|           <% end %> | ||||
|  | ||||
|           <%= if invite.disabled_at |> is_nil() do %> | ||||
|             <a href="#" class="btn btn-primary" | ||||
|               phx-click="disable" phx-value-id="<%= invite.id %>"> | ||||
|               <%= gettext("Disable") %> | ||||
|             </a> | ||||
|           <% else %> | ||||
|             <a href="#" class="btn btn-primary" | ||||
|               phx-click="enable" phx-value-id="<%= invite.id %>"> | ||||
|               <%= gettext("Enable") %> | ||||
|             </a> | ||||
|           <% end %> | ||||
|  | ||||
|           <%= if invite.disabled_at |> is_nil() and not(invite.uses_left |> is_nil()) do %> | ||||
|             <a href="#" class="btn btn-primary" | ||||
|               phx-click="set_unlimited" phx-value-id="<%= invite.id %>" | ||||
|               data-confirm={dgettext("prompts", "Are you sure you want to make %{name} unlimited?", name: invite.name)}> | ||||
|               <%= gettext("Set Unlimited") %> | ||||
|             </a> | ||||
|           <% end %> | ||||
|         </div> | ||||
|       </div> | ||||
|     <% end %> | ||||
|   </div> | ||||
| </div> | ||||
|  | ||||
| <%= if @live_action in [:new, :edit] do %> | ||||
|   <%= live_modal CanneryWeb.InviteLive.FormComponent, | ||||
|     id: @invite.id || :new, | ||||
|     title: @page_title, | ||||
|     action: @live_action, | ||||
|     invite: @invite, | ||||
|     return_to: Routes.invite_index_path(@socket, :index), | ||||
|     current_user: @current_user %> | ||||
| <% end %> | ||||
		Reference in New Issue
	
	Block a user