New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Change email dialog #22309
Change email dialog #22309
Conversation
Hey Eric. I've still got to do a little cleanup and write tests for this, but could you do an initial scan? I'm taking a somewhat unconventional approach here and maybe you can stop me from doing something dumb now before I spend a bunch of time cleaning it up. 😁 |
a5546b9
to
2a9db7f
Compare
Also, security/optics question (@poorvasingal @jeremydstone): Email must have changed (checked on client) For students, this check sort of lets the user manually rainbow-table attack their own hashed email, guessing email addresses until they get this validation error. This not new from a security point of view: We're already rendering the hashed email to the client's DOM, and the user must be signed in to see this view (this is the "edit my account" page). But do we think this will look like a security problem to users that don't understand how hashed emails work? Do we care? I personally think the improved user experience outweighs the imagined security concern, but thought I should bring it up. |
I wonder if we need to care if the email address is the same? So basically, just end up doing a no-op if the email address is updated to the same as before? That way we can avoid this issue? Other alternative is to not show the error until they hit the submit button. |
|
Oh, one other possibility is to not post via a form at all, but to use a more simple web API. While you wouldn't have the token, your server-side code can still authorize the user using CanCan. @aoby has expertise here. |
@@ -61,8 +91,6 @@ $(document).ready(() => { | |||
if (needToConfirmEmail) { | |||
e.preventDefault(); | |||
showConfirmEmailModal(onCancelModal, onSubmitModal(e)); | |||
} else if ($('#user_email').length) { | |||
prepareEmailData(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we sure that everything else works fine if this is removed? I don't really remember the logic here, and when prepareEmailData()
needs to be called or not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I presume this is ok because the email fields have been removed from the form, and are now handled by the React modal.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Andrew's got it: I took a pretty close look at this. I've removed the only #user_email
element on this page to replace it with the uneditable email display and "Update" link, so $('#user_email').length
would always evaluate false, making this dead code.
@@ -116,7 +116,11 @@ def respond_to_account_update(successfully_updated, flash_message_kind = :update | |||
format.any {head :no_content} | |||
else | |||
format.html {render "edit", formats: [:html]} | |||
format.any {head :unprocessable_entity} | |||
format.any do | |||
render status: :unprocessable_entity, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The opaque erroring here has bothered me for sooooo long, but I always forgot about it when I was working nearby - thank you for setting this up!
As far as I'm aware, the way you've handled the HAML/JS stuff there is about as good as it can get while sticking with that method of updating (leaning on the autogenerated user update machinery). @breville's suggestion about using a Web API and handling auth through CanCan is interesting, and maybe worth looking into whether it's a better pattern for all of this stuff? I don't really like the autogenerated Rails updaters at all, because they're opaque to debug, a bit too magical, and (especially) leave the configuration of business functionality inside a HAML view. But that may be a longer term thing to look into, depending how tough API-ifying it would be. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I only skimmed the UI part, but the Rails side LGTM. As @breville mentioned, and as I discussed offline w Brad, pulling this out into a separate API has tradeoffs: without a token, it will be arguably less secure in that it won't require updating through the UI (but will still require log in), and it will mean writing a new API controller. On the other hand, the submit logic will be cleaner, send the state directly over JSON instead of filling and submitting a hidden form. I don't have a strong preference.
@@ -61,8 +91,6 @@ $(document).ready(() => { | |||
if (needToConfirmEmail) { | |||
e.preventDefault(); | |||
showConfirmEmailModal(onCancelModal, onSubmitModal(e)); | |||
} else if ($('#user_email').length) { | |||
prepareEmailData(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I presume this is ok because the email fields have been removed from the form, and are now handled by the React modal.
apps/src/lib/ui/ChangeEmailModal.jsx
Outdated
this._form.find('#change-email-modal_user_email').val(this.props.userAge < 13 ? '' : this.state.newEmail); | ||
this._form.find('#change-email-modal_user_hashed_email').val(hashEmail(this.state.newEmail)); | ||
this._form.find('#change-email-modal_user_current_password').val(this.state.currentPassword); | ||
// this._form.find('#user_email_opt_in').val(this.state.emailOptIn); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why commented out? Is this a future field?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. The whole reason we're converting this to a dialog is so that we can explicitly ask teachers to re-opt-in to marketing email when they change their email address. I started building this out, then realized just switching to a dialog for this flow was complicated enough, so I'll be adding the opt-in as a follow-on PR. I wasn't sure it was worth extracting the work I'd done so far though.
ChangeEmailForm calls onChange ✔ when the email field changes ✔ when the password field changes ✔ when the email opt-in field changes calls onSubmit ✔ when the enter key is pressed in the email field ✔ when the enter key is pressed in the password field ✔ when the enter key is pressed on the opt-in field ✔ but not when other keys are pressed ✔ and not when the form is disabled when disabled ✔ the email field is disabled ✔ the password field is disabled ✔ the opt-in field is disabled focusOnAnError() ✔ does nothing if there are no validation errors ✔ focuses on the email field if there is an email validation error ✔ focuses on the password field if there is a password validation error ✔ focuses on the email field if there are both email and password validation errors
ChangeEmailModal ✔ disables everything and shows save text when saving ✔ disables everything and shows save text when saving ✔ calls handleCancel when clicking the cancel button validation ✔ checks that email is present ✔ checks that email is valid ✔ checks that email is not the same as the current email ✔ reports email server errors ✔ checks that password is present ✔ reports password server errors ✔ disables the submit button when validation errors are present ✔ enables the submit button form passes validation changes clear server errors ✔ on email ✔ on password
PTAL! I've done the additional cleanup I wanted to get to before merging this and added some unit test coverage of the React parts of this change. I've extracted a lot of the rendering code out to a I'm not submitting a UI test with this change because there's expected follow-on work immediately after it (the email opt-in work) and I'd like to save the end-to-end test until that's in place. |
Just discovered that this change breaks the ability to upgrade a student account to a teacher account, because it depends on the email field that this PR removes. My immediate follow-up work involves changing that "change account type" flow 😢 and maybe I need to combine that work with this one to not break anything? |
Spoke with Poorva and we're okay with temporary disabling the ability for student accounts to upgrade to teacher accounts, for the day or two between these two features. We'll give our helpdesk a heads-up before we ship this change. |
Progress on the spec Email opt-in consent on account changes.
What's in this PR
This first change replaces the current email update flow on the account page with a new React dialog for changing email address.
Here's the flow for a teacher:
And for a student (we leave the email hidden on the actual account page):
Validation
Email is required (checked on client and server)
Email must be valid (checked on client and server)
Email must have changed (checked on client)
Email must be available (checked on server)
Password is required (checked on client and server)
Password must be correct (checked on server)
Error handling
An unexpected server error results in this message:
Unit-tested behaviors