Skip to content

Commit

Permalink
Merge pull request #29 from daveminer/settings-role
Browse files Browse the repository at this point in the history
Settings role
  • Loading branch information
daveminer authored Oct 27, 2024
2 parents 6f316a3 + a1bd802 commit 758ac94
Show file tree
Hide file tree
Showing 13 changed files with 370 additions and 60 deletions.
2 changes: 1 addition & 1 deletion config/test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ config :basket, BasketWeb.Endpoint,
server: false

# In test we don't send emails.
config :basket, Basket.Mailer, adapter: Swoosh.Adapters.Test
config :basket, Basket.Pow.Mailer, adapter: Swoosh.Adapters.Test

# Disable swoosh api client as it is only required for production adapters.
config :swoosh, :api_client, false
Expand Down
8 changes: 8 additions & 0 deletions lib/basket/club.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
defmodule Basket.Club do
@moduledoc false

import Ecto.Changeset

use Ecto.Schema

@type t :: %__MODULE__{}
Expand All @@ -11,4 +13,10 @@ defmodule Basket.Club do

timestamps()
end

def changeset(club, attrs) do
club
|> cast(attrs, [:name])
|> validate_required([:name])
end
end
22 changes: 22 additions & 0 deletions lib/basket/club_member.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
defmodule Basket.ClubMember do
@moduledoc false

use Ecto.Schema
import Ecto.Changeset

alias Basket.{Club, User}

schema "club_members" do
belongs_to :user, User
belongs_to :club, Club
end

@doc false
def changeset(club_member, attrs) do
club_member
|> cast(attrs, [:user_id, :club_id])
|> validate_required([:user_id, :club_id])
|> foreign_key_constraint(:user_id)
|> foreign_key_constraint(:club_id)
end
end
22 changes: 22 additions & 0 deletions lib/basket/club_officer.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
defmodule Basket.ClubOfficer do
@moduledoc false

use Ecto.Schema
import Ecto.Changeset

alias Basket.{Club, User}

schema "club_officers" do
belongs_to :user, User
belongs_to :club, Club
end

@doc false
def changeset(club_member, attrs) do
club_member
|> cast(attrs, [:user_id, :club_id])
|> validate_required([:user_id, :club_id])
|> foreign_key_constraint(:user_id)
|> foreign_key_constraint(:club_id)
end
end
22 changes: 12 additions & 10 deletions lib/basket_web/components/core_components.ex
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ defmodule BasketWeb.CoreComponents do
phx-click={JS.push("lv:clear-flash", value: %{key: @kind}) |> hide("#{@id}")}
role="alert"
class={[
"fixed top-2 right-2 mr-2 w-80 sm:w-96 z-50 rounded-lg p-3 ring-1",
"fixed top-2 left-1/2 transform -translate-x-1/2 w-80 sm:w-96 z-50 rounded-lg p-3 ring-1",
@kind == :info && "bg-emerald-50 text-emerald-800 ring-emerald-500 fill-cyan-900",
@kind == :error && "bg-rose-50 text-rose-900 shadow-md ring-rose-500 fill-rose-900"
]}
Expand Down Expand Up @@ -238,7 +238,7 @@ defmodule BasketWeb.CoreComponents do
def simple_form(assigns) do
~H"""
<.form :let={f} for={@for} as={@as} {@rest}>
<div class="mt-10 space-y-8 bg-white">
<div class="mt-10 space-y-8">
<%= render_slot(@inner_block, f) %>
<div :for={action <- @actions} class="mt-2 flex items-center justify-between gap-6">
<%= render_slot(action, f) %>
Expand Down Expand Up @@ -412,12 +412,14 @@ defmodule BasketWeb.CoreComponents do
name={@name}
id={@id}
value={Phoenix.HTML.Form.normalize_value(@type, @value)}
class={[
"mt-2 block w-full rounded-lg text-zinc-900 focus:ring-0 sm:text-sm sm:leading-6",
"phx-no-feedback:border-zinc-300 phx-no-feedback:focus:border-zinc-400",
@errors == [] && "border-zinc-300 focus:border-zinc-400",
@errors != [] && "border-rose-400 focus:border-rose-400"
]}
class={
[
"mt-2 block w-full rounded-lg focus:ring-0 sm:text-sm sm:leading-6"
# "phx-no-feedback:border-zinc-300 phx-no-feedback:focus:border-zinc-400",
# @errors == [] && "border-zinc-300 focus:border-zinc-400",
# @errors != [] && "border-rose-400 focus:border-rose-400"
]
}
{@rest}
/>
<.error :for={msg <- @errors}><%= msg %></.error>
Expand All @@ -433,7 +435,7 @@ defmodule BasketWeb.CoreComponents do

def label(assigns) do
~H"""
<label for={@for} class="block text-sm font-semibold leading-6 text-zinc-800">
<label for={@for} class="block text-sm font-semibold leading-6 base-content">
<%= render_slot(@inner_block) %>
</label>
"""
Expand Down Expand Up @@ -466,7 +468,7 @@ defmodule BasketWeb.CoreComponents do
~H"""
<header class={[@actions != [] && "flex items-center justify-between gap-6", @class]}>
<div>
<h1 class="text-lg font-semibold leading-8 text-zinc-800">
<h1 class="text-lg font-semibold leading-8 base-content">
<%= render_slot(@inner_block) %>
</h1>
<p :if={@subtitle != []} class="mt-2 text-sm leading-6 text-zinc-600">
Expand Down
2 changes: 1 addition & 1 deletion lib/basket_web/components/layouts/root.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
</script>
</head>
<body class="bg-white antialiased">
<body class="antialiased">
<%= @inner_content %>
</body>
</html>
89 changes: 44 additions & 45 deletions lib/basket_web/controllers/pow/session_html/new.html.heex
Original file line number Diff line number Diff line change
@@ -1,57 +1,56 @@
<div class="mx-auto max-w-sm">
<div class="w-screen h-screen">
<.header class="text-center mb-3">
Welcome to Basket!
</.header>

<img src={~p"/images/stock_basket.webp"} width="216" class="mx-auto block" />

<.simple_form :let={f} for={@changeset} as={:user} action={@action} phx-update="ignore">
<.error :if={@changeset.action}>
Oops, something went wrong! Please check the errors below.
</.error>
<.input
field={f[Pow.Ecto.Schema.user_id_field(@changeset)]}
type={(Pow.Ecto.Schema.user_id_field(@changeset) == :email && "email") || "text"}
label={Phoenix.Naming.humanize(Pow.Ecto.Schema.user_id_field(@changeset))}
required
/>
<.input field={f[:password]} type="password" label="Password" value={nil} required />

<:actions
:let={f}
:if={
Pow.Plug.extension_enabled?(@conn, PowPersistentSession) ||
Pow.Plug.extension_enabled?(@conn, PowResetPassword)
}
>
<div class="mx-auto w-1/2">
<.simple_form :let={f} for={@changeset} as={:user} action={@action} phx-update="ignore">
<.error :if={@changeset.action}>
Oops, something went wrong! Please check the errors below.
</.error>
<.input
:if={Pow.Plug.extension_enabled?(@conn, PowPersistentSession)}
field={f[:persistent_session]}
type="checkbox"
label="Keep me logged in"
field={f[Pow.Ecto.Schema.user_id_field(@changeset)]}
type={(Pow.Ecto.Schema.user_id_field(@changeset) == :email && "email") || "text"}
label={Phoenix.Naming.humanize(Pow.Ecto.Schema.user_id_field(@changeset))}
required
/>
<.link
:if={Pow.Plug.extension_enabled?(@conn, PowResetPassword)}
href={
Pow.Phoenix.Routes.path_for(
@conn,
PowResetPassword.Phoenix.ResetPasswordController,
:new
)
<.input field={f[:password]} type="password" label="Password" value={nil} required />

<:actions
:let={f}
:if={
Pow.Plug.extension_enabled?(@conn, PowPersistentSession) ||
Pow.Plug.extension_enabled?(@conn, PowResetPassword)
}
class="text-sm font-semibold"
>
Forgot your password?
</.link>
</:actions>
<.input
:if={Pow.Plug.extension_enabled?(@conn, PowPersistentSession)}
field={f[:persistent_session]}
type="checkbox"
label="Keep me logged in"
/>
<.link
:if={Pow.Plug.extension_enabled?(@conn, PowResetPassword)}
href={
Pow.Phoenix.Routes.path_for(
@conn,
PowResetPassword.Phoenix.ResetPasswordController,
:new
)
}
class="text-sm font-semibold"
>
Forgot your password?
</.link>
</:actions>

<:actions>
<.button
phx-disable-with="Signing in..."
class="w-full text-black bg-gray-300 hover:bg-gray-400"
>
Sign in <span aria-hidden="true"></span>
</.button>
</:actions>
</.simple_form>
<:actions>
<.button phx-disable-with="Signing in..." class="w-full">
Sign in <span aria-hidden="true"></span>
</.button>
</:actions>
</.simple_form>
</div>
</div>
82 changes: 82 additions & 0 deletions lib/basket_web/controllers/pow_invitation/invitation_controller.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
defmodule BasketWeb.PowInvitation.InvitationController do
use BasketWeb, :controller

import Phoenix.VerifiedRoutes

alias Basket.Repo
alias Pow.{Config, Plug}

plug :load_user when action in [:create]

@doc """
This custom PowInvitation controller allows for a custom redirect after invitation and
creation of a club membership for the new user, based on the inviting user's office.
This controller assumes the inviting user only has one office.
"""
def create(%{assigns: %{current_user: invited_by}} = conn, %{"user" => user_params}) do
case create_user(conn, user_params) do
{:ok, user, conn} ->
url = path(conn, BasketWeb.Router, ~p"/invitations/#{user.invitation_token}/edit")
email = PowInvitation.Phoenix.Mailer.invitation(conn, user, invited_by, url)
Pow.Phoenix.Mailer.deliver(conn, email)

conn
|> put_flash(:info, "Invitation sent successfully.")
|> redirect(to: path(conn, BasketWeb.Router, ~p"/settings"))

{:error, changeset, conn} ->
error_message = format_changeset_errors(changeset)

conn
|> put_flash(:error, error_message)
|> redirect(to: path(conn, BasketWeb.Router, ~p"/settings"))
end
end

defp create_user(conn, user_params) do
config = Plug.fetch_config(conn)

user =
conn
|> Plug.current_user()
|> Repo.preload(:offices)

user_mod = Config.user!(config)

user_params =
Map.put(user_params, :email, user_params["email"])
|> Map.delete("email")

user_mod
|> struct()
|> user_mod.invite_changeset(user, user_params)
|> Ecto.Changeset.put_assoc(:clubs, [hd(user.offices)])
|> Repo.insert()
|> case do
{:ok, user} -> {:ok, user, conn}
{:error, changeset} -> {:error, changeset, conn}
end
end

defp format_changeset_errors(changeset) do
Enum.map_join(changeset.errors, ", ", fn
{:email, {"has already been taken", _}} ->
"That email address has already been invited."

{field, {message, _}} ->
"#{humanize(field)} #{message}"
end)
end

defp humanize(field) do
field
|> to_string()
|> String.replace("_", " ")
|> String.capitalize()
end

defp load_user(conn, _opts) do
user = Pow.Plug.current_user(conn)
assign(conn, :current_user, user)
end
end
4 changes: 2 additions & 2 deletions lib/basket_web/controllers/settings/settings_html.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ defmodule BasketWeb.SettingsHTML do

def index(assigns) do
~H"""
<div class="flex-col p-4">
<div class="flex-col bg-base-100 p-8">
<NavRow.render id="nav-row" />
<%= if @current_user.offices > 0 do %>
Expand All @@ -15,7 +15,7 @@ defmodule BasketWeb.SettingsHTML do
</span>
<div class="w-50">
<input name="user[email]" class="input input-bordered" placeholder="someone@xyz.com" />
<button type="submit" class="btn">
<button type="submit" class="btn btn-primary ms-4">
Invite
</button>
</div>
Expand Down
8 changes: 7 additions & 1 deletion lib/basket_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ defmodule BasketWeb.Router do
use Pow.Phoenix.Router

use Pow.Extension.Phoenix.Router,
extensions: [PowResetPassword, PowEmailConfirmation, PowInvitation, PowPersistentSession]
extensions: [PowResetPassword, PowEmailConfirmation, PowPersistentSession]

pipeline :browser do
plug :accepts, ["html"]
Expand Down Expand Up @@ -43,6 +43,12 @@ defmodule BasketWeb.Router do
pow_extension_routes()
end

scope "/", BasketWeb.PowInvitation, as: "pow_invitation" do
pipe_through :browser

resources "/invitations", InvitationController, only: [:new, :create, :edit]
end

scope "/", BasketWeb do
pipe_through [:browser, :authenticated]

Expand Down
Loading

0 comments on commit 758ac94

Please sign in to comment.