Skip to content

Commit 1e49578

Browse files
authored
Sign users in from emailed links instead of a password (#4019)
New signups no longer pick a password. `users.passwordless_user` marks them, the confirmation email is a sign in link rather than a verification button, and entering their email at `/session/new` mails a fresh link and lands on a new "we sent you a magic sign in link" page. - **They're offered a password on every sign in, not just at signup.** The flash links to the reset form, which now accepts a signed in user without a token so it doesn't have to email one. Organization passwordless users (magic link or SSO) just get "You're signed in" — their organization is what signs them in, so there's nothing to set. - **Passwordless users get a random digest from `set_calculated_attributes`**, which is what `Users::ProcessOrganizationRoleJob` had been doing inline for org-created users, so `has_secure_password` and the password validations are untouched. That job now just passes the flag. - **Flash keys suffixed `_html` render as markup.** The flash cookie is JSON, so it drops `html_safe` across the redirect and a link in a flash needs an explicit opt-in. Wired into all three flash render sites. - Existing org passwordless members now skip the intermediate "Email me the link" screen, since they're `passwordless_user` too. The screen still handles a passwordless-domain email with no account yet, which is what mints it.
1 parent 0776d52 commit 1e49578

47 files changed

Lines changed: 551 additions & 133 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

app/assets/tailwind/application.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,11 @@
106106
@apply tw:text-blue-700 tw:dark:text-blue-300 tw:font-bold tw:underline;
107107
}
108108

109+
/* Links inherit the alert's own color, so the underline is what marks them as links */
110+
.twalert-body .twlink {
111+
@apply tw:underline tw:hover:text-blue-800;
112+
}
113+
109114
.twless-strong {
110115
@apply tw:opacity-65 tw:decoration-gray-900/65 tw:dark:decoration-gray-400/65;
111116
}

app/components/admin/bikes_table/component.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ module BikesTable
88
# checkboxes, and skip_user to drop the owner column.
99
class Component < ApplicationComponent
1010
# Digest of the markup inside the row cache — the cached_markup_digest spec keeps it current
11-
MARKUP_DIGEST = "96fa3d808254"
11+
MARKUP_DIGEST = "1556da37892e"
1212

1313
def initialize(bikes:, no_show_header: false, show_serial: false, render_sortable: false,
1414
skip_user: false, render_multi_check: false)

app/components/admin/users_table/component.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ module UsersTable
66
# render_deleted to show the deleted_at column.
77
class Component < ApplicationComponent
88
# Digest of the markup inside the row cache — the cached_markup_digest spec keeps it current
9-
MARKUP_DIGEST = "ffd7f9f91f03"
9+
MARKUP_DIGEST = "d9337123b3b7"
1010

1111
def initialize(users:, render_sortable: false, render_deleted: false)
1212
@users = users
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
en:
3+
components:
4+
emails:
5+
confirmation_email:
6+
click_to_sign_in_html: Click the <em>Sign in</em> button to finish signing
7+
up!
8+
follow_this_link_to_sign_in: Follow this link to sign in
9+
sign_in: Sign in
10+
youre_almost_there: You're almost there!
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<h1 class="center-text" style="margin-top: 2rem;">
2+
<%= translation("youre_almost_there") %>
3+
</h1>
4+
5+
<h3 class="center-text" style="margin: 2rem 0 3rem;">
6+
<%= translation("follow_this_link_to_sign_in") %>
7+
</h3>
8+
9+
<%= link_to translation("sign_in"), tokenized_url, "data-pm-no-track" => true, class: "binx-button" %>
10+
11+
<% unless @partner %>
12+
<p class="less-strong center-text">
13+
<%= translation("click_to_sign_in_html") %>
14+
</p>
15+
<% end %>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# frozen_string_literal: true
2+
3+
module Emails
4+
module ConfirmationEmail
5+
class Component < ApplicationComponent
6+
def initialize(user:)
7+
@user = user
8+
@partner = user.partner_sign_up
9+
end
10+
11+
private
12+
13+
def tokenized_url
14+
confirm_users_url(id: @user.id, code: @user.confirmation_token, partner: @partner)
15+
end
16+
end
17+
end
18+
end

app/components/registrations/show/wrapper/component.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ module Wrapper
88
class Component < ApplicationComponent
99
# Digest of the markup inside the cache block — the cached_markup_digest spec
1010
# keeps it current, following what this tree renders out into UI:: and elsewhere
11-
MARKUP_DIGEST = "86faa178a90f"
11+
MARKUP_DIGEST = "c1f5cebfb160"
1212

1313
def initialize(bike:, current_user:, view:, available_views:, bike_sticker: nil, current_alerts: {})
1414
@bike = bike
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
en:
3+
components:
4+
sessions:
5+
sign_in_interstitial:
6+
if_not_click_button: If you are not, please click the button below
7+
sign_in: Sign in
8+
you_should_be_signed_in_automatically: You should be automatically signed
9+
in.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<div class="tw:text-center">
2+
<h3><%= translation("you_should_be_signed_in_automatically") %></h3>
3+
4+
<p class="tw:mt-2 tw:mb-6"><%= translation("if_not_click_button") %></p>
5+
6+
<%= form_tag @url, method: "post", data: {controller: @auto_submit ? "auto-submit" : nil} do %>
7+
<% @fields.each do |name, value| %>
8+
<%= hidden_field_tag name, value %>
9+
<% end %>
10+
11+
<%= render UI::Button::Component.new(text: translation("sign_in"), kind: :submit, color: :primary, size: :lg) %>
12+
<% end %>
13+
</div>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# frozen_string_literal: true
2+
3+
module Sessions
4+
module SignInInterstitial
5+
# Emailed sign in links have to be GETs, so they land on a page that renders this and
6+
# it posts the token — a scanner or prefetcher following the link doesn't spend it.
7+
class Component < ApplicationComponent
8+
def initialize(url:, fields:, auto_submit: true)
9+
@url = url
10+
@fields = fields.compact_blank
11+
@auto_submit = auto_submit
12+
end
13+
end
14+
end
15+
end

0 commit comments

Comments
 (0)