diff --git a/lib/cannery/invites.ex b/lib/cannery/invites.ex
index e7e49027..0b459168 100644
--- a/lib/cannery/invites.ex
+++ b/lib/cannery/invites.ex
@@ -50,13 +50,21 @@ defmodule Cannery.Invites do
@spec get_invite_by_token(String.t()) :: Invite.t() | nil
def get_invite_by_token(nil), do: nil
def get_invite_by_token(""), do: nil
- def get_invite_by_token(token), do: Repo.get_by(Invite, token: token, disabled_at: nil)
+
+ def get_invite_by_token(token) do
+ Repo.one(
+ from i in Invite,
+ where: i.token == ^token and i.disabled_at |> is_nil()
+ )
+ end
@doc """
- Uses invite by decrementing uses_left, or markes invite invalid if it's been completely used
-
+ Uses invite by decrementing uses_left, or marks invite invalid if it's been
+ completely used.
"""
@spec use_invite!(Invite.t()) :: Invite.t()
+ def use_invite!(%Invite{uses_left: nil} = invite), do: invite
+
def use_invite!(%Invite{uses_left: uses_left} = invite) do
new_uses_left = uses_left - 1
@@ -96,7 +104,10 @@ defmodule Cannery.Invites do
attrs
|> Map.merge(%{
"user_id" => user_id,
- "token" => :crypto.strong_rand_bytes(@invite_token_length)
+ "token" =>
+ :crypto.strong_rand_bytes(@invite_token_length)
+ |> Base.url_encode64()
+ |> binary_part(0, @invite_token_length)
})
%Invite{} |> Invite.changeset(attrs) |> Repo.insert()
diff --git a/lib/cannery/invites/invite.ex b/lib/cannery/invites/invite.ex
index 26462cdf..ba45a656 100644
--- a/lib/cannery/invites/invite.ex
+++ b/lib/cannery/invites/invite.ex
@@ -20,6 +20,7 @@ defmodule Cannery.Invites.Invite do
invite
|> cast(attrs, [:name, :token, :uses_left, :disabled_at, :user_id])
|> validate_required([:name, :token, :user_id])
+ |> validate_number(:uses_left, greater_than_or_equal_to: 0)
end
@type t :: %{
diff --git a/lib/cannery_web/controllers/user_registration_controller.ex b/lib/cannery_web/controllers/user_registration_controller.ex
index 6ba56f8e..f9e844e5 100644
--- a/lib/cannery_web/controllers/user_registration_controller.ex
+++ b/lib/cannery_web/controllers/user_registration_controller.ex
@@ -1,18 +1,67 @@
defmodule CanneryWeb.UserRegistrationController do
use CanneryWeb, :controller
- alias Cannery.Accounts
+ alias Cannery.{Accounts, Invites}
alias Cannery.Accounts.User
alias CanneryWeb.UserAuth
- def new(conn, _params) do
- changeset = Accounts.change_user_registration(%User{})
- render(conn, "new.html", changeset: changeset)
+ def new(conn, %{"invite" => invite_token}) do
+ invite = Invites.get_invite_by_token(invite_token)
+
+ if invite do
+ conn |> render_new(invite)
+ else
+ conn
+ |> put_flash(:error, "Sorry, this invite was not found or expired")
+ |> redirect(to: Routes.home_path(CanneryWeb.Endpoint, :index))
+ end
end
- def create(conn, %{"user" => user_params}) do
+ def new(conn, _params) do
+ if Accounts.allow_registration?() do
+ conn |> render_new()
+ else
+ conn
+ |> put_flash(:error, "Sorry, public registration is disabled")
+ |> redirect(to: Routes.home_path(CanneryWeb.Endpoint, :index))
+ end
+ end
+
+ # renders new user registration page
+ defp render_new(conn, invite \\ nil) do
+ changeset = Accounts.change_user_registration(%User{})
+ conn |> render("new.html", changeset: changeset, invite: invite)
+ end
+
+ def create(conn, %{"user" => %{"invite_token" => invite_token}} = attrs) do
+ invite = Invites.get_invite_by_token(invite_token)
+
+ if invite do
+ conn |> create_user(attrs, invite)
+ else
+ conn
+ |> put_flash(:error, "Sorry, this invite was not found or expired")
+ |> redirect(to: Routes.home_path(CanneryWeb.Endpoint, :index))
+ end
+ end
+
+ def create(conn, attrs) do
+ if Accounts.allow_registration?() do
+ conn |> create_user(attrs)
+ else
+ conn
+ |> put_flash(:error, "Sorry, public registration is disabled")
+ |> redirect(to: Routes.home_path(CanneryWeb.Endpoint, :index))
+ end
+ end
+
+ defp create_user(conn, %{"user" => user_params}, invite \\ nil) do
case Accounts.register_user(user_params) do
{:ok, user} ->
+ unless invite |> is_nil() do
+ invite |> Invites.use_invite!()
+ end
+
{:ok, _} =
Accounts.deliver_user_confirmation_instructions(
user,
@@ -24,7 +73,7 @@ defmodule CanneryWeb.UserRegistrationController do
|> UserAuth.log_in_user(user)
{:error, %Ecto.Changeset{} = changeset} ->
- render(conn, "new.html", changeset: changeset)
+ render(conn, "new.html", changeset: changeset, invite: invite)
end
end
end
diff --git a/lib/cannery_web/live/invite_live/form_component.ex b/lib/cannery_web/live/invite_live/form_component.ex
index 37731e40..402b1f52 100644
--- a/lib/cannery_web/live/invite_live/form_component.ex
+++ b/lib/cannery_web/live/invite_live/form_component.ex
@@ -37,7 +37,7 @@ defmodule CanneryWeb.InviteLive.FormComponent do
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, :changeset, changeset)}
- end
+ end
end
defp save_invite(socket, :new, invite_params) do
diff --git a/lib/cannery_web/live/invite_live/form_component.html.leex b/lib/cannery_web/live/invite_live/form_component.html.leex
index 46aea755..59304dbc 100644
--- a/lib/cannery_web/live/invite_live/form_component.html.leex
+++ b/lib/cannery_web/live/invite_live/form_component.html.leex
@@ -1,14 +1,28 @@
-
<%= @title %>
+
+ <%= @title %>
+
<%= f = form_for @changeset, "#",
id: "invite-form",
+ class: "grid grid-cols-3 justify-center items-center space-y-4",
phx_target: @myself,
phx_change: "validate",
phx_submit: "save" %>
<%= label f, :name, class: "title text-lg text-primary-500" %>
- <%= text_input f, :name, class: "input input-primary" %>
- <%= error_tag f, :name %>
+ <%= text_input f, :name, class: "input input-primary col-span-2" %>
+
+ <%= error_tag f, :name %>
+
- <%= submit "Save", phx_disable_with: "Saving..." %>
+ <%= label f, :uses_left, class: "title text-lg text-primary-500" %>
+ <%= number_input f, :uses_left, min: 0, class: "input input-primary col-span-2" %>
+
+ <%= error_tag f, :uses_left %>
+
+
+
+ <%= submit "Save", class: "btn btn-primary",
+ phx_disable_with: "Saving..." %>
+
diff --git a/lib/cannery_web/live/invite_live/index.ex b/lib/cannery_web/live/invite_live/index.ex
index 7c6c0ab0..93f1bb0c 100644
--- a/lib/cannery_web/live/invite_live/index.ex
+++ b/lib/cannery_web/live/invite_live/index.ex
@@ -1,46 +1,62 @@
defmodule CanneryWeb.InviteLive.Index do
use CanneryWeb, :live_view
- alias Cannery.Invites
- alias Cannery.Invites.Invite
+ alias Cannery.{Invites}
+ alias Cannery.Invites.{Invite}
@impl true
def mount(_params, session, socket) do
- {:ok, socket |> assign_defaults(session) |> assign(invites: list_invites())}
+ {:ok, socket |> assign_defaults(session) |> display_invites()}
end
@impl true
def handle_params(params, _url, socket) do
- {:noreply, apply_action(socket, socket.assigns.live_action, params)}
+ {:noreply, socket |> apply_action(socket.assigns.live_action, params)}
end
defp apply_action(socket, :edit, %{"id" => id}) do
socket
- |> assign(:page_title, "Edit Invite")
- |> assign(:invite, Invites.get_invite!(id))
+ |> assign(page_title: "Edit Invite", invite: Invites.get_invite!(id))
end
defp apply_action(socket, :new, _params) do
socket
- |> assign(:page_title, "New Invite")
- |> assign(:invite, %Invite{})
+ |> assign(page_title: "New Invite", invite: %Invite{})
end
defp apply_action(socket, :index, _params) do
socket
- |> assign(:page_title, "Listing Invites")
- |> assign(:invite, nil)
+ |> assign(page_title: "Listing Invites", invite: nil)
end
@impl true
def handle_event("delete", %{"id" => id}, socket) do
invite = Invites.get_invite!(id)
{:ok, _} = Invites.delete_invite(invite)
-
- {:noreply, assign(socket, :invites, list_invites())}
+ {:noreply, socket |> display_invites()}
end
- defp list_invites do
- Invites.list_invites()
+ def handle_event("set_unlimited", %{"id" => id}, socket) do
+ id |> Invites.get_invite!() |> Invites.update_invite(%{"uses_left" => nil})
+ {:noreply, socket |> display_invites()}
+ end
+
+ def handle_event("enable", %{"id" => id}, socket) do
+ attrs = %{"uses_left" => nil, "disabled_at" => nil}
+ id |> Invites.get_invite!() |> Invites.update_invite(attrs)
+ {:noreply, socket |> display_invites()}
+ end
+
+ def handle_event("disable", %{"id" => id}, socket) do
+ now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
+ attrs = %{"uses_left" => 0, "disabled_at" => now}
+ id |> Invites.get_invite!() |> Invites.update_invite(attrs)
+ {:noreply, socket |> display_invites()}
+ end
+
+ # redisplays invites to socket
+ defp display_invites(socket) do
+ invites = Invites.list_invites()
+ socket |> assign(:invites, invites)
end
end
diff --git a/lib/cannery_web/live/invite_live/index.html.leex b/lib/cannery_web/live/invite_live/index.html.leex
index d130327d..0b00a3bc 100644
--- a/lib/cannery_web/live/invite_live/index.html.leex
+++ b/lib/cannery_web/live/invite_live/index.html.leex
@@ -1,4 +1,82 @@
-Listing Invites
+
+
+ Listing Invites
+
+
+ <%= if @invites |> Enum.empty?() do %>
+
+
+ No invites 😔
+
+
+ <%= live_patch to: Routes.invite_index_path(@socket, :new),
+ class: "btn btn-primary" do %>
+ Invite someone new!
+ <% end %>
+
+ <% else %>
+ <%= live_patch to: Routes.invite_index_path(@socket, :new),
+ class: "btn btn-primary" do %>
+ Invite
+ <% end %>
+ <% end %>
+
+
+ <%= for invite <- @invites do %>
+
+
+ <%= invite.name %>
+
+
+ <%= if invite.disabled_at |> is_nil() do %>
+
+ Uses Left: <%= invite.uses_left || "Unlimited" %>
+
+ <% else %>
+
+ Invite Disabled
+
+ <% end %>
+
+
+ <%= Routes.user_registration_url(@socket, :new, invite: invite.token) %>
+
+
+
+ <%= live_patch "Edit", to: Routes.invite_index_path(@socket, :edit, invite),
+ class: "text-primary-500 link" %>
+
+ <%= link "Delete", to: "#",
+ class: "text-primary-500 link",
+ phx_click: "delete",
+ phx_value_id: invite.id,
+ data: [confirm: "Are you sure?"] %>
+
+ <%= if invite.disabled_at |> is_nil() do %>
+
+ Disable
+
+ <% else %>
+
+ Enable
+
+ <% end %>
+
+ <%= if invite.disabled_at |> is_nil() and not(invite.uses_left |> is_nil()) do %>
+
+ Set Unlimited
+
+ <% end %>
+
+
+ <% end %>
+
+
<%= if @live_action in [:new, :edit] do %>
<%= live_modal CanneryWeb.InviteLive.FormComponent,
@@ -6,33 +84,6 @@
title: @page_title,
action: @live_action,
invite: @invite,
- return_to: Routes.invite_index_path(@socket, :index) %>
+ return_to: Routes.invite_index_path(@socket, :index),
+ current_user: @current_user %>
<% end %>
-
-
-
-
- Name |
- Token |
- Uses left |
- |
-
-
-
- <%= for invite <- @invites do %>
-
- <%= invite.name %> |
- <%= invite.token %> |
- <%= invite.uses_left || "Unlimited" %> |
-
-
- <%= live_redirect "Show", to: Routes.invite_show_path(@socket, :show, invite) %>
- <%= live_patch "Edit", to: Routes.invite_index_path(@socket, :edit, invite) %>
- <%= link "Delete", to: "#", phx_click: "delete", phx_value_id: invite.id, data: [confirm: "Are you sure?"] %>
- |
-
- <% end %>
-
-
-
-<%= live_patch "New Invite", to: Routes.invite_index_path(@socket, :new) %>
diff --git a/lib/cannery_web/live/invite_live/show.ex b/lib/cannery_web/live/invite_live/show.ex
deleted file mode 100644
index 5e3150cf..00000000
--- a/lib/cannery_web/live/invite_live/show.ex
+++ /dev/null
@@ -1,21 +0,0 @@
-defmodule CanneryWeb.InviteLive.Show do
- use CanneryWeb, :live_view
-
- alias Cannery.Invites
-
- @impl true
- def mount(_params, session, socket) do
- {:ok, socket |> assign_defaults(session)}
- end
-
- @impl true
- def handle_params(%{"id" => id}, _, socket) do
- {:noreply,
- socket
- |> assign(:page_title, page_title(socket.assigns.live_action))
- |> assign(:invite, Invites.get_invite!(id))}
- end
-
- defp page_title(:show), do: "Show Invite"
- defp page_title(:edit), do: "Edit Invite"
-end
diff --git a/lib/cannery_web/live/invite_live/show.html.leex b/lib/cannery_web/live/invite_live/show.html.leex
deleted file mode 100644
index 2944516d..00000000
--- a/lib/cannery_web/live/invite_live/show.html.leex
+++ /dev/null
@@ -1,27 +0,0 @@
-Show Invite
-
-<%= if @live_action in [:edit] do %>
- <%= live_modal CanneryWeb.InviteLive.FormComponent,
- id: @invite.id,
- title: @page_title,
- action: @live_action,
- invite: @invite,
- return_to: Routes.invite_show_path(@socket, :show, @invite) %>
-<% end %>
-
-
-
- -
- Name:
- <%= @invite.name %>
-
-
- -
- Token:
- <%= @invite.token %>
-
-
-
-
-<%= live_patch "Edit", to: Routes.invite_show_path(@socket, :edit, @invite), class: "button" %>
-<%= live_redirect "Back", to: Routes.invite_index_path(@socket, :index) %>
diff --git a/lib/cannery_web/router.ex b/lib/cannery_web/router.ex
index a14aa2b3..43dfbab3 100644
--- a/lib/cannery_web/router.ex
+++ b/lib/cannery_web/router.ex
@@ -87,9 +87,6 @@ defmodule CanneryWeb.Router do
live "/invites", InviteLive.Index, :index
live "/invites/new", InviteLive.Index, :new
live "/invites/:id/edit", InviteLive.Index, :edit
-
- live "/invites/:id", InviteLive.Show, :show
- live "/invites/:id/show/edit", InviteLive.Show, :edit
end
scope "/", CanneryWeb do
diff --git a/lib/cannery_web/templates/user_registration/new.html.eex b/lib/cannery_web/templates/user_registration/new.html.eex
index 4718a19c..f3ee7ecd 100644
--- a/lib/cannery_web/templates/user_registration/new.html.eex
+++ b/lib/cannery_web/templates/user_registration/new.html.eex
@@ -11,6 +11,10 @@
<% end %>
+ <%= if @invite do %>
+ <%= hidden_input f, :invite_token, value: @invite.token %>
+ <% end %>
+
<%= label f, :email, class: "title text-lg text-primary-500" %>
<%= email_input f, :email, required: true, class: "input input-primary col-span-2" %>
@@ -24,7 +28,7 @@
<%= error_tag f, :password %>
<%= submit "Register", class: "btn btn-primary" %>
-
+
@@ -34,4 +38,4 @@
class: "btn btn-primary" %>
<% end %>
-
\ No newline at end of file
+