On an account management page, I want users to be able to change their emails. But I’d like that to go through the same email confirmation process they originally used when they signed up.
Is there a way to trigger that process again?
On an account management page, I want users to be able to change their emails. But I’d like that to go through the same email confirmation process they originally used when they signed up.
Is there a way to trigger that process again?
I get an AuthenticationFailed exception when trying to call that, either client or server side. Must be something only intended to be used internally?
I tried a test where I just tried to get it to resend to the same email the user is already using:
anvil.users.send_token_login_email(anvil.users.get_user()['email'])
from the docs that function is only applicable if you want your user to be able to login with an email link:
https://anvil.works/docs/users/authentication_choices#sign-in-with-email-link
But I don’t think it’s relevant here.
When i’ve thought about this before I would take the twitter approach:
So in anvil something like:
"""
validate the new email is an email address
check the email address doesn't already exist
send the email with a random code
store code in the user_row using anvil.secrets.encryp_with_key
in the app - ask the user to check their email and enter the code in the box
Server call to check the codes match - great - if not try again with new code or different email
if they navigate away from the page and back then it should start the process again
"""
I like that approach, even though it does mean rolling my own code for it. That way at least the email only gets changed after it’s verified. In the meantime they continue using their old email.
To save others some time, here are the server functions that I came up with to manage the email change. I’m using the hash routing dependency for routing. Not shown is the client code that calls these, that’s fairly straightforward, with one form prompting the user for their new email, and another handling the clicks from the emailed links.
Even if you want to manage things differently, hopefully these serve as a good starting point.
Edited to add in the id_generator function and to fix some errors
def id_generator(size=10):
chars = string.ascii_uppercase + string.digits
# Remove characters that are problematic in some fonts
chars = chars.replace('O', '')
chars = chars.replace('0', '')
chars = chars.replace('1', '')
chars = chars.replace('I', '')
chars = chars.replace('L', '')
return ''.join(random.choice(chars) for _ in range(size))
#########################################################################################
# Start the process of changing the user's email
#########################################################################################
@anvil.server.callable(require_user=True)
@anvil.tables.in_transaction
def account_email_change(email):
user = anvil.users.get_user()
if user['email'] == email:
raise ValueError("Invalid email")
if app_tables.users.get(email=email):
return "That email already has an account in the system."
# Build the email change template
email_template = app_tables.globals.get(key='email_change_template')
email_template = email_template['value']
token = id_generator()
while app_tables.email_changes.get(token=token):
token = id_generator()
app_tables.email_changes.add_row(email=email, token=token, issued=datetime.datetime.now())
link = APP_URL+"#account/info/email/verify?token=" + token
import jinja2
template = jinja2.Template(email_template)
email_text = template.render(link=link)
from_address = 'admin'
from_name = 'From Name'
subject = "Requested Email Change"
anvil.email.send(to=email, from_address=from_address, from_name=from_name, subject=subject, html=email_text)
return "The verification email has been sent. Check your email inbox and spam folder for it."
#########################################################################################
# Verify an email link click
#########################################################################################
@anvil.server.callable(require_user=True)
@anvil.tables.in_transaction
def account_email_verify(token):
user = anvil.users.get_user()
email_change = app_tables.email_changes.get(token=token)
if not email_change:
return "That token is invalid. You can only click on the link once."
expiration = email_change['issued'] + datetime.timedelta(hours=24)
if expiration < datetime.datetime.now(datetime.timezone.utc):
return "That link has expired. Start the email change process again, and click the link within 24 hours."
if app_tables.users.get(email=email_change['email']):
return "That email already has an account in the system, your email has not been changed."
user['email'] = email_change['email']
email_change.delete()
return None