Add ability to self-serve password recovery if the user has their username and a valid card ID/PIN pair.

This commit is contained in:
Jennifer Taylor
2026-08-20 22:20:56 +00:00
parent dd565d11a4
commit 7ccbcd385a
4 changed files with 86 additions and 27 deletions

View File

@@ -75,10 +75,10 @@ def viewlogin() -> Response:
return Response(render_template("account/login.html", **{"title": "Log In", "show_navigation": False}))
def recover_display(username: str, token: Optional[str]) -> Response:
def recover_display(username: str, token: Optional[str], card_number: Optional[str]) -> Response:
return Response(render_template(
"account/recover.html",
**{"title": "Recover Password", "show_navigation": False, "token": token, "username": username},
**{"title": "Recover Password", "show_navigation": False, "token": token, "card_number": card_number, "username": username},
))
@@ -89,36 +89,71 @@ def recover() -> Response:
token = request.form.get("token", "")
password1 = request.form["password1"]
password2 = request.form["password2"]
card_number = request.form.get("card_number", "")
pin = request.form.get("pin", "")
# Now, make sure this account recovery token is valid.
# Figure out what style of recovery it is.
if token:
# Now, make sure this account recovery token is valid.
userid = g.data.local.user.from_recovery(token)
if userid is None:
error("Recovery token is invalid or expired!")
return recover_display(username, token, card_number)
# And make sure the user is valid and matches this recovery token.
user = g.data.local.user.get_user(userid)
if user is None:
error("Recovery token is invalid or expired!")
return recover_display(username, token, card_number)
# Be a little lenient with username spelling here.
if user.username.lower() != username.lower():
error("Recovery token is not for this account!")
return recover_display(username, token, card_number)
elif card_number:
# First, try to convert the card to a valid E004 ID
try:
cardid = CardCipher.decode(card_number)
except CardCipherException:
error("Invalid card number!")
return recover_display(username, token, card_number)
# Now, see if this card ID exists already
userid = g.data.local.user.from_cardid(cardid)
if userid is None:
error("The specified card is not for this account!")
return recover_display(username, token, card_number)
# Now, make sure this card and user are linked
user = g.data.local.user.get_user(userid)
if user is None:
error("The specified card is not for this account!")
return recover_display(username, token, card_number)
# Be a little lenient with username spelling here.
if user.username.lower() != username.lower():
error("The specified card is not for this account!")
return recover_display(username, token, card_number)
# Now, see if the pin is correct
if not g.data.local.user.validate_pin(userid, pin):
error("The entered PIN does not match the PIN on the card!")
return recover_display(username, token, card_number)
else:
userid = None
if userid is None:
error("Recovery token is invalid or expired!")
return recover_display(username, token)
# And make sure the user is valid and matches this recovery token.
user = g.data.local.user.get_user(userid)
if user is None:
error("Recovery token is invalid or expired!")
return recover_display(username, token)
# Be a little lenient with username spelling here.
if user.username.lower() != username.lower():
error("Recovery token is not for this account!")
return recover_display(username, token)
return recover_display(username, token, card_number)
# Now, make sure that the passwords match
if password1 != password2:
error("Passwords do not match each other!")
return recover_display(username, token)
return recover_display(username, token, card_number)
# Now, make sure passwords are long enough
if len(password1) < 6:
error("Password is not long enough!")
return recover_display(username, token)
return recover_display(username, token, card_number)
# Now, update the account.
g.data.local.user.update_password(userid, password1)
@@ -132,7 +167,7 @@ def recover() -> Response:
@account_pages.route("/recover/<recovery>")
@loginprohibited
def viewrecover(recovery: Optional[str]) -> Response:
return recover_display("", recovery)
return recover_display("", recovery, "")
def register_display(card_number: str, username: str, email: str) -> Response:

View File

@@ -13,6 +13,9 @@
</dl>
</form>
</div>
<div class="section">
Forgot your password? <a href="{{ url_for('account_pages.viewrecover') }}">Recover it!</a>
</div>
<div class="section">
Don't have an account? <a href="{{ url_for('account_pages.viewregister') }}">Create one!</a>
</div>

View File

@@ -6,14 +6,29 @@
<form action="{{ url_for('account_pages.recover') }}" method=post>
{% if token %}
<input type="hidden" name="token" value="{{ token }}" />
{% else %}
<p>
Enter the card number and PIN of a card that you've previously linked
to your account. Any linked card will work as long as you also enter
the correct PIN and the username of the account the card is linked to.
</p>
{% endif %}
<dl>
<dt>Username</dt>
{% if token %}
{% else %}
<dt>Card Number (found on back of e-AMUSEMENT card)</dt>
<dd><input type="text" name="card_number" value="{{ card_number }}" autocomplete="off" /></dd>
<dt>PIN</dt>
<dd><input type="password" name="pin" autocomplete="off" /></dd>
<dt>&nbsp;</dt>
<dd></dd>
{% endif %}
<dt>Existing Username</dt>
<dd><input type="text" name="username" value="{{ username }}" /></dd>
<dt>Desired Password</dt>
<dd><input type="password" name="password1" /></dd>
<dd><input type="password" name="password1" autocomplete="new-password" /></dd>
<dt>Desired Password (again)</dt>
<dd><input type="password" name="password2" /></dd>
<dd><input type="password" name="password2" autocomplete="new-password" /></dd>
<dt></dt>
<dd><input type="submit" value="update password" /></dd>
</dl>

View File

@@ -4,11 +4,17 @@
<h2>Register New Account</h2>
<form action="{{ url_for('account_pages.register') }}" method=post>
<p>
To register an account on this network you will need to have played at least
one credit on a game linked to this network. If you have not done so you cannot
register an account. Enter the card number and PIN of the card you have used to
play on this network.
</p>
<dl>
<dt>Card Number (found on back of e-AMUSEMENT card)</dt>
<dd><input type="text" name="card_number" value="{{ card_number }}" /></dd>
<dd><input type="text" name="card_number" value="{{ card_number }}" autocomplete="off" /></dd>
<dt>PIN</dt>
<dd><input type="password" name="pin" /></dd>
<dd><input type="password" name="pin" autocomplete="new-password" /></dd>
<dt>&nbsp;</dt>
<dd></dd>
<dt>Desired Username</dt>
@@ -16,9 +22,9 @@
<dt>Email Address (used for password and pin resetting)</dt>
<dd><input type="text" name="email" value="{{ email }}" /></dd>
<dt>Desired Password</dt>
<dd><input type="password" name="password1" /></dd>
<dd><input type="password" name="password1" autocomplete="new-password" /></dd>
<dt>Desired Password (again)</dt>
<dd><input type="password" name="password2" /></dd>
<dd><input type="password" name="password2" autocomplete="new-password" /></dd>
<dt></dt>
<dd><input type="submit" value="create account" /></dd>
</dl>