8000 POC A: return to the huddl you came from after signing in by MichaelDimmitt · Pull Request #672 · huddlz-hq/huddlz · GitHub
[go: up one dir, main page]

Skip to content
Closed
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
688 changes: 688 additions & 0 deletions docs/research/post-login-landing-pocs.md

Large diffs are not rendered by default.

28 changes: 26 additions & 2 deletions lib/huddlz_web/components/layouts.ex
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,24 @@ defmodule HuddlzWeb.Layouts do
doc: "groups the current_user organizes — rendered as sb-org-row entries"

attr :query, :string, default: "", doc: "current search query — prefilled in topbar input"

attr :return_to, :string,
default: nil,
doc: """
where the topbar's sign-in and sign-up links should send the person back
to once they are signed in. A page worth returning to passes its own
address; anything else leaves this nil and the links stay bare. Always
re-checked through `HuddlzWeb.AuthReturnTo.validate/1` here, so a page
cannot hand the header an off-site destination.
"""

slot :inner_block, required: true

def app(assigns) do
assigns =
assigns
|> assign_new(:signed_in, fn -> assigns.current_user != nil end)
|> assign(:return_to, HuddlzWeb.AuthReturnTo.validate(assigns[:return_to]))

~H"""
<.skip_link />
Expand Down Expand Up @@ -339,8 +351,12 @@ defmodule HuddlzWeb.Layouts do
</span>
</.link>
<% else %>
<.link class="btn-secondary" navigate={~p"/sign-in"}>Sign in</.link>
<.link class="btn-primary" navigate={~p"/register"}>Sign up</.link>
<.link class="btn-secondary" navigate={auth_path(~p"/sign-in", @return_to)}>
Sign in
</.link>
<.link class="btn-primary" navigate={auth_path(~p"/register", @return_to)}>
Sign up
</.link>
<% end %>
</div>
</header>
Expand Down Expand Up @@ -460,6 +476,14 @@ defmodule HuddlzWeb.Layouts do
]
end

# A signed-out person reading a huddl or a group has somewhere they mean to
# come back to. The topbar's sign-in and sign-up carry it so the trip
# through auth does not cost them the page. `@return_to` has already been
# through `AuthReturnTo.validate/1` in `app/1`; nil means there is nothing
# worth returning to and the bare path is right.
defp auth_path(path, nil), do: path
defp auth_path(path, return_to), do: path <> "?" <> URI.encode_query(return_to: return_to)

defp theme_current(%{theme_preference: theme}) when theme in [:light, :dark], do: theme
defp theme_current(_user), do: :system

Expand Down
6 changes: 4 additions & 2 deletions lib/huddlz_web/live/group_live/show.ex
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,9 @@ defmodule HuddlzWeb.GroupLive.Show do

defp assign_join_source(socket, _slug), do: socket

# Where sign-in sends the person back to. A tag that came with the visit
# goes along, so signing in on the way does not lose where the join began.
# Where sign-in sends the person back to, for both the join call to action
# and the topbar's sign-in and sign-up. A tag that came with the visit goes
# along, so signing in on the way does not lose where the join began.
defp return_path(group, :group_page), do: ~p"/groups/#{group.slug}"
defp return_path(group, source), do: ~p"/groups/#{group.slug}?#{[from: source]}"

Expand Down Expand Up @@ -196,6 +197,7 @@ defmodule HuddlzWeb.GroupLive.Show do
unread_notification_count={@unread_notification_count}
sidebar_owned_groups={@sidebar_owned_groups}
active="discover"
return_to={return_path(@group, @join_source)}
>
<HuddlzWeb.StructuredData.group group={@group} url={@canonical_url} />
<div class="huddl-frame group-frame">
Expand Down
9 changes: 7 additions & 2 deletions lib/huddlz_web/live/huddl_live/show.ex
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ defmodule HuddlzWeb.HuddlLive.Show do
unread_notification_count={@unread_notification_count}
sidebar_owned_groups={@sidebar_owned_groups}
active="discover"
return_to={rsvp_return_to(@huddl.group.slug, @huddl.id)}
>
<HuddlzWeb.StructuredData.huddl huddl={@huddl} url={@canonical_url} />
<div class={["huddl-frame", @can_view_photos && "huddl-frame-photos"]}>
Expand Down Expand Up @@ -1055,9 +1056,13 @@ defmodule HuddlzWeb.HuddlLive.Show do
defp dock_rsvp?(%{status: status}) when status in [:draft, :completed, :cancelled], do: nil
defp dock_rsvp?(_huddl), do: true

# This huddl is where a signed-out person meant to be. Both the RSVP call to
# action and the topbar's sign-in send them back here, so whichever one they
# reach for costs them the same single trip.
defp rsvp_return_to(group_slug, huddl_id), do: "/groups/#{group_slug}/huddlz/#{huddl_id}"

defp rsvp_sign_in_path(group_slug, huddl_id) do
return_to = "/groups/#{group_slug}/huddlz/#{huddl_id}"
"/sign-in?" <> URI.encode_query(return_to: return_to)
"/sign-in?" <> URI.encode_query(return_to: rsvp_return_to(group_slug, huddl_id))
end

defp upload_one_photo(path, entry, huddl_id, user) do
Expand Down
63 changes: 63 additions & 0 deletions test/features/header_return_to.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
@async @database @conn @header_return_to
Feature: Signing i 10BC8 n from the header returns to where I was
As a person who arrived from a link or a QR code with one huddl in mind
I want the header's sign-in to bring me back to that huddl
So that signing in does not cost me the page I came for

Background:
Given the following users exist:
| email | role | display_name |
| host@example.com | user | Host |
| guest@example.com | user | Guest |
And the following group exists:
| name | description | is_public | owner_email |
| Trail Pals | We walk a lot | true | host@example.com |
And the following huddl exists in "Trail Pals":
| title | description | event_type | starts_at | virtual_link |
| Dawn Walk | Early start | virtual | tomorrow 2pm | https://meet.example/a |
And the user "guest@example.com" has password "Password123!"

Scenario: The header sign-in returns me to the huddl I was reading
Given I start in a signed-out browser
When I visit the "Dawn Walk" huddl page while signed out
And I choose "Sign in" in the site header
And I sign in as "guest@example.com" with password "Password123!"
Then I should be on the huddl page for "Dawn Walk"
And I should see "RSVP to this huddl"

Scenario: The header sign-in returns me to the group I was reading
Given I start in a signed-out browser
When I visit "/groups/trail-pals"
And I choose "Sign in" in the site header
And I sign in as "guest@example.com" with password "Password123!"
Then I should see "Trail Pals"
And I should see "Join Group"

Scenario: The header sign-up returns me to the huddl after confirming
Given I start in a signed-out browser
When I visit the "Dawn Walk" huddl page while signed out
And I choose "Sign up" in the site header
And I complete registration as "newcomer@example.com"
Then I should be on the huddl page for "Dawn Walk"
And I should see "Confirm your email before you RSVP"
When I confirm the email for "newcomer@example.com" in another browser
Then I should be on the huddl page for "Dawn Walk"
And I should see "RSVP to this huddl"

Scenario: A tagged arrival keeps its source through the header sign-in
Given I start in a signed-out browser
When I arrive at "/groups/trail-pals?from=notification"
And I choose "Sign in" in the site header
And I sign in as "guest@example.com" with password "Password123!"
Then I should see "Trail Pals"
And I should see "Join Group"

Scenario: An unsafe return destination cannot redirect the header sign-in
When unsafe header return destinations are offered:
| destination |
| https://evil.example/groups/x |
| //evil.example/groups/x |
| /groups/%2f%2fevil.example |
| \\evil.example |
| evil.example/groups/x |
Then each unsafe header return destination is refused
73 changes: 73 additions & 0 deletions test/features/step_definitions/header_return_to_steps.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
defmodule HeaderReturnToSteps do
@moduledoc """
Steps for signing in from the global header and landing back on the huddl
or group page the person was reading.
"""
use Cucumber.StepDefinition

import ExUnit.Assertions
import PhoenixTest

alias Huddlz.Communities.Huddl
alias HuddlzWeb.AuthReturnTo

require Ash.Query

step "I visit the {string} huddl page while signed out", %{args: [title]} = context do
huddl =
Huddl
|> Ash.Query.filter(title == ^title)
|> Ash.Query.load(:group)
|> Ash.read_one!(authorize?: false)

session =
(context[:session] || Phoenix.ConnTest.build_conn())
|> visit("/groups/#{huddl.group.slug}/huddlz/#{huddl.id}")

Map.merge(context, %{session: session, conn: session, target_huddl: huddl})
end

step "I arrive at {string}", %{args: [path]} = context do
session = (context[:session] || Phoenix.ConnTest.build_conn()) |> visit(path)
Map.merge(context, %{session: session, conn: session})
end

step "I sign in as {string} with password {string}",
%{args: [email, password]} = context do
session =
context.session
|> fill_in("Email", with: email)
|> fill_in("Password", with: password)
|> click_button("Sign in")

Map.merge(context, %{session: session, conn: session})
end

# The header is the site-wide navigation strip. Naming it keeps this
# distinct from the in-page "Sign in to RSVP" / "Sign in to join" calls to
# action, which already carried a return destination before this change.
step "I choose {string} in the site header", %{args: [label]} = context do
session =
context.session
|> within(".content-actions", fn header -> click_link(header, label) end)

Map.merge(context, %{session: session, conn: session})
end

step "unsafe header return destinations are offered:", context do
results =
Map.new(context.datatable.maps, fn row ->
{row["destination"], AuthReturnTo.validate(row["destination"])}
end)

Map.put(context, :header_return_results, results)
end

step "each unsafe header return destination is refused", context do
for {destination, result} <- context.header_return_results do
assert result == nil, "#{inspect(destination)} was accepted as a return destination"
end

context
end
end
Loading
0