DEV: Convert account activation pages to use Ember (#28206)

This commit is contained in:
Jan Cernik 2024-08-12 16:02:00 -05:00 committed by GitHub
parent df18bcd029
commit 5b78bbd138
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 313 additions and 106 deletions

View File

@ -0,0 +1,8 @@
import DiscourseRoute from "discourse/routes/discourse";
import I18n from "discourse-i18n";
export default class ActivateAccountRoute extends DiscourseRoute {
titleToken() {
return I18n.t("login.activate_account");
}
}

View File

@ -110,6 +110,7 @@ export default function () {
this.route("resent");
this.route("edit-email");
});
this.route("activate-account", { path: "/u/activate-account/:token" });
this.route("confirm-new-email", { path: "/u/confirm-new-email/:token" });
this.route("confirm-old-email", { path: "/u/confirm-old-email/:token" });
this.route(

View File

@ -0,0 +1,128 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { action } from "@ember/object";
import { service } from "@ember/service";
import RouteTemplate from "ember-route-template";
import DButton from "discourse/components/d-button";
import hideApplicationHeaderButtons from "discourse/helpers/hide-application-header-buttons";
import hideApplicationSidebar from "discourse/helpers/hide-application-sidebar";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { wavingHandURL } from "discourse/lib/waving-hand-url";
import i18n from "discourse-common/helpers/i18n";
export default RouteTemplate(
class extends Component {
@service siteSettings;
@tracked accountActivated = false;
@tracked isLoading = false;
@tracked needsApproval = false;
@tracked errorMessage = null;
@action
async activate() {
this.isLoading = true;
let hp;
try {
const response = await fetch("/session/hp", {
headers: {
Accept: "application/json",
},
});
hp = await response.json();
} catch (error) {
this.isLoading = false;
popupAjaxError(error);
return;
}
try {
const response = await ajax(
`/u/activate-account/${this.args.model.token}.json`,
{
type: "PUT",
data: {
password_confirmation: hp.value,
challenge: hp.challenge.split("").reverse().join(""),
},
}
);
if (!response.success) {
this.errorMessage = i18n("user.activate_account.already_done");
return;
}
this.accountActivated = true;
if (response.redirect_to) {
window.location.href = response.redirect_to;
} else if (response.needs_approval) {
this.needsApproval = true;
} else {
setTimeout(() => (window.location.href = "/"), 2000);
}
} catch (error) {
this.errorMessage = i18n("user.activate_account.already_done");
}
}
<template>
{{hideApplicationSidebar}}
{{hideApplicationHeaderButtons "search" "login" "signup"}}
<div id="simple-container">
{{#if this.errorMessage}}
<div class="alert alert-error">
{{this.errorMessage}}
</div>
{{else}}
<div class="activate-account">
<h1 class="activate-title">{{i18n
"user.activate_account.welcome_to"
site_name=this.siteSettings.title
}}
<img src={{(wavingHandURL)}} alt="" class="waving-hand" />
</h1>
<br />
{{#if this.accountActivated}}
<div class="perform-activation">
<div class="image">
<img
src="/images/wizard/tada.svg"
class="waving-hand"
alt="tada emoji"
/>
</div>
{{#if this.needsApproval}}
<p>{{i18n "user.activate_account.approval_required"}}</p>
{{else}}
<p>{{i18n "user.activate_account.please_continue"}}</p>
<p>
<DButton
class="continue-button"
@translatedLabel={{i18n
"user.activate_account.continue_button"
site_name=this.siteSettings.title
}}
@href="/"
/>
</p>
{{/if}}
</div>
{{else}}
<DButton
id="activate-account-button"
class="btn-primary"
@action={{this.activate}}
@label="user.activate_account.action"
@disabled={{this.isLoading}}
/>
{{/if}}
</div>
{{/if}}
</div>
</template>
}
);

View File

@ -1,27 +0,0 @@
(function () {
const activateButton = document.querySelector("#activate-account-button");
activateButton.addEventListener("click", async function () {
activateButton.setAttribute("disabled", true);
const hpPath = document.getElementById("data-activate-account").dataset
.path;
try {
const response = await fetch(hpPath, {
headers: {
Accept: "application/json",
},
});
const hp = await response.json();
document.querySelector("#password_confirmation").value = hp.value;
document.querySelector("#challenge").value = hp.challenge
.split("")
.reverse()
.join("");
document.querySelector("#activate-account-form").submit();
} catch (e) {
activateButton.removeAttribute("disabled");
throw e;
}
});
})();

View File

@ -1,6 +0,0 @@
(function () {
const path = document.getElementById("data-auto-redirect").dataset.path;
setTimeout(function () {
window.location.href = path;
}, 2000);
})();

View File

@ -1100,7 +1100,11 @@ class UsersController < ApplicationController
def activate_account
expires_now
render layout: "no_ember"
respond_to do |format|
format.html { render "default/empty" }
format.json { render json: success_json }
end
end
def perform_account_activation
@ -1130,21 +1134,22 @@ class UsersController < ApplicationController
end
if Wizard.user_requires_completion?(@user)
return redirect_to(wizard_path)
@redirect_to = wizard_path
elsif destination_url.present?
return redirect_to(destination_url, allow_other_host: true)
@redirect_to = destination_url
elsif SiteSetting.enable_discourse_connect_provider &&
payload = cookies.delete(:sso_payload)
return redirect_to(session_sso_provider_url + "?" + payload)
@redirect_to = session_sso_provider_url + "?" + payload
end
else
@needs_approval = true
end
else
flash.now[:error] = I18n.t("activation.already_done")
return render_json_error(I18n.t("activation.already_done"))
end
render layout: "no_ember"
render json:
success_json.merge(redirect_to: @redirect_to, needs_approval: @needs_approval || false)
end
def update_activation_email

View File

@ -1,19 +0,0 @@
<div id='simple-container'>
<div class='activate-account'>
<h1 class="activate-title"><%= t 'activation.welcome_to', site_name: SiteSetting.title %> <img src="<%= Emoji.url_for("wave") %>" alt="" class="waving-hand"></h1>
<br/>
<button class='btn btn-primary' id='activate-account-button'><%= t 'activation.action' %></button>
<%= form_tag(perform_activate_account_path, method: :put, id: 'activate-account-form') do %>
<%= hidden_field_tag 'password_confirmation' %>
<%= hidden_field_tag 'challenge' %>
<% end %>
</div>
</div>
<%- content_for(:no_ember_head) do %>
<%= render_google_universal_analytics_code %>
<%= tag.meta id: 'data-activate-account', data: { path: path('/session/hp') } %>
<%- end %>
<%= preload_script "activate-account" %>

View File

@ -1,27 +0,0 @@
<div id='simple-container'>
<%if flash[:error]%>
<div class='alert alert-error'>
<%=flash[:error]%>
</div>
<%else%>
<div class='activate-account'>
<h1 class="activate-title"><%= t 'activation.welcome_to', site_name: SiteSetting.title %> <img src="<%= Emoji.url_for("wave") %>" alt="" class="waving-hand"></h1>
<br>
<div class='perform-activation'>
<div class="image">
<img src="<%= Discourse.base_path + '/images/wizard/tada.svg' %>" alt="tada emoji" class="waving-hand">
</div>
<% if @needs_approval %>
<p><%= t 'activation.approval_required' %></p>
<% else %>
<p><%= t('activation.please_continue') %></p>
<p><a class="btn" href="<%= path "/" %>"><%= t('activation.continue_button', site_name: SiteSetting.title) -%></a></p>
<%- content_for(:no_ember_head) do %>
<%= tag.meta id: 'data-auto-redirect', data: { path: path('/') } %>
<%- end %>
<%= preload_script 'auto-redirect' %>
<% end %>
</div>
</div>
<%end%>
</div>

View File

@ -1745,6 +1745,14 @@ en:
account_specific: "Your %{provider} account '%{account_description}' will be used for authentication."
generic: "Your %{provider} account will be used for authentication."
activate_account:
action: "Click here to activate your account"
already_done: "Sorry, this account confirmation link is no longer valid. Perhaps your account is already active?"
please_continue: "Your new account is confirmed; you will be redirected to the home page."
continue_button: "Continue to %{site_name}"
welcome_to: "Welcome to %{site_name}!"
approval_required: "A moderator must manually approve your new account before you can access this forum. You'll get an email when your account is approved!"
name:
title: "Name"
instructions: "Your full name (optional)."
@ -2387,6 +2395,7 @@ en:
resend_activation_email: "Click here to send the activation email again."
omniauth_disallow_totp: "Your account has two-factor authentication enabled. Please log in with your password."
activate_account: "Activate Account"
resend_title: "Resend Activation Email"
change_email: "Change Email Address"
provide_new_email: "Provide a new address and we'll resend your confirmation email."

View File

@ -1046,11 +1046,7 @@ en:
connected: "(connected)"
activation:
action: "Click here to activate your account"
already_done: "Sorry, this account confirmation link is no longer valid. Perhaps your account is already active?"
please_continue: "Your new account is confirmed; you will be redirected to the home page."
continue_button: "Continue to %{site_name}"
welcome_to: "Welcome to %{site_name}!"
approval_required: "A moderator must manually approve your new account before you can access this forum. You'll get an email when your account is approved!"
missing_session: "We cannot detect if your account was created, please ensure you have cookies enabled."
activated: "Sorry, this account has already been activated."

View File

@ -60,9 +60,8 @@ RSpec.describe UsersController do
context "with invalid token" do
it "return success" do
put "/u/activate-account/invalid-tooken"
expect(response.status).to eq(200)
expect(flash[:error]).to be_present
put "/u/activate-account/invalid-token"
expect(response.status).to eq(422)
end
end
@ -108,12 +107,11 @@ RSpec.describe UsersController do
)
expect(response.status).to eq(200)
expect(flash[:error]).to be_blank
expect(session[:current_user_id]).to be_present
expect(CGI.unescapeHTML(response.body)).to_not include(
I18n.t("activation.approval_required"),
)
data = JSON.parse(response.body)
expect(data["needs_approval"]).to eq(false)
expect(session[:current_user_id]).to be_present
end
end
@ -124,11 +122,9 @@ RSpec.describe UsersController do
put "/u/activate-account/#{email_token.token}"
expect(response.status).to eq(200)
expect(CGI.unescapeHTML(response.body)).to include(I18n.t("activation.approval_required"))
data = JSON.parse(response.body)
expect(data["needs_approval"]).to eq(true)
expect(response.body).to_not have_tag(:script, with: { src: "/assets/application.js" })
expect(flash[:error]).to be_blank
expect(session[:current_user_id]).to be_blank
end
end
@ -141,7 +137,8 @@ RSpec.describe UsersController do
put "/u/activate-account/#{email_token.token}"
expect(response).to redirect_to(destination_url)
expect(response.status).to eq(200)
expect(response.parsed_body["redirect_to"]).to eq(destination_url)
end
end
@ -158,7 +155,8 @@ RSpec.describe UsersController do
it "should redirect to the topic" do
put "/u/activate-account/#{email_token.token}"
expect(response).to redirect_to(topic.relative_url)
expect(response.status).to eq(200)
expect(response.parsed_body["redirect_to"]).to eq(topic.relative_url)
end
end
end

View File

@ -4,8 +4,10 @@ require "rotp"
shared_examples "login scenarios" do
let(:login_modal) { PageObjects::Modals::Login.new }
let(:activate_account) { PageObjects::Pages::ActivateAccount.new }
let(:user_preferences_security_page) { PageObjects::Pages::UserPreferencesSecurity.new }
fab!(:user) { Fabricate(:user, username: "john", password: "supersecurepassword") }
fab!(:admin) { Fabricate(:admin, username: "admin", password: "supersecurepassword") }
let(:user_menu) { PageObjects::Components::UserMenu.new }
before { Jobs.run_immediately! }
@ -43,12 +45,39 @@ shared_examples "login scenarios" do
activation_link = wait_for_email_link(user, :activation)
visit activation_link
find("#activate-account-button").click
activate_account.click_activate_account
activate_account.click_continue
expect(page).to have_current_path("/")
expect(page).to have_css(".header-dropdown-toggle.current-user")
end
it "redirects to the wizard after activating account" do
login_modal.open
login_modal.fill(username: "admin", password: "supersecurepassword")
login_modal.click_login
expect(page).to have_css(".not-activated-modal")
login_modal.click(".activation-controls button.resend")
activation_link = wait_for_email_link(admin, :activation)
visit activation_link
activate_account.click_activate_account
expect(page).to have_current_path("/wizard")
end
it "shows error when when activation link is invalid" do
login_modal.open
login_modal.fill(username: "john", password: "supersecurepassword")
login_modal.click_login
expect(page).to have_css(".not-activated-modal")
visit "/u/activate-account/invalid"
activate_account.click_activate_account
expect(activate_account).to have_error
end
it "displays the right message when user's email has been marked as expired" do
password = "myawesomepassword"
user.update!(password:)

View File

@ -0,0 +1,20 @@
# frozen_string_literal: true
module PageObjects
module Pages
class ActivateAccount < PageObjects::Pages::Base
def click_activate_account
find("#activate-account-button").click
end
def click_continue
find(".perform-activation .continue-button").click
end
def has_error?
has_css?("#simple-container .alert-error")
has_content?(I18n.t("js.user.activate_account.already_done"))
end
end
end
end

View File

@ -0,0 +1,40 @@
# frozen_string_literal: true
module PageObjects
module Pages
class InviteForm < PageObjects::Pages::Base
def open(key)
visit "/invites/#{key}"
end
def fill_username(username)
find("#new-account-username").fill_in(with: username)
end
def fill_password(password)
find("#new-account-password").fill_in(with: password)
end
def has_valid_username?
find(".username-input").has_css?("#username-validation.good")
end
def has_valid_password?
find(".password-input").has_css?("#password-validation.good")
end
def has_valid_fields?
has_valid_username?
has_valid_password?
end
def click_create_account
find(".invitation-cta__accept.btn-primary").click
end
def has_successful_message?
has_css?(".invite-success")
end
end
end
end

View File

@ -3,11 +3,15 @@
shared_examples "signup scenarios" do
let(:login_modal) { PageObjects::Modals::Login.new }
let(:signup_modal) { PageObjects::Modals::Signup.new }
let(:invite_form) { PageObjects::Pages::InviteForm.new }
let(:activate_account) { PageObjects::Pages::ActivateAccount.new }
let(:invite) { Fabricate(:invite, email: "johndoe@example.com") }
let(:topic) { Fabricate(:topic, title: "Super cool topic") }
context "when anyone can create an account" do
it "can signup" do
Jobs.run_immediately!
before { Jobs.run_immediately! }
it "can signup" do
signup_modal.open
signup_modal.fill_email("johndoe@example.com")
signup_modal.fill_username("john")
@ -18,6 +22,53 @@ shared_examples "signup scenarios" do
expect(page).to have_css(".account-created")
end
it "can signup and activate account" do
signup_modal.open
signup_modal.fill_email("johndoe@example.com")
signup_modal.fill_username("john")
signup_modal.fill_password("supersecurepassword")
expect(signup_modal).to have_valid_fields
signup_modal.click_create_account
expect(page).to have_css(".account-created")
mail = ActionMailer::Base.deliveries.first
expect(mail.to).to contain_exactly("johndoe@example.com")
activation_link = mail.body.to_s[%r{/u/activate-account/\S+}]
visit activation_link
activate_account.click_activate_account
activate_account.click_continue
expect(page).to have_current_path("/")
expect(page).to have_css(".header-dropdown-toggle.current-user")
end
it "redirects to the topic the user was invited to after activating account" do
TopicInvite.create!(invite: invite, topic: topic)
invite_form.open(invite.invite_key)
invite_form.fill_username("john")
invite_form.fill_password("supersecurepassword")
expect(invite_form).to have_valid_fields
invite_form.click_create_account
expect(invite_form).to have_successful_message
mail = ActionMailer::Base.deliveries.first
expect(mail.to).to contain_exactly("johndoe@example.com")
activation_link = mail.body.to_s[%r{/u/activate-account/\S+}]
visit activation_link
activate_account.click_activate_account
expect(page).to have_current_path("/t/#{topic.slug}/#{topic.id}")
end
context "with invite code" do
before { SiteSetting.invite_code = "cupcake" }
@ -124,6 +175,7 @@ shared_examples "signup scenarios" do
user = User.find_by(username: "john")
EmailToken.confirm(Fabricate(:email_token, user: user).token)
visit "/"
login_modal.open
login_modal.fill_username("john")
login_modal.fill_password("supersecurepassword")