diff --git a/config.py b/config.py
index 77448bb..420cd80 100644
--- a/config.py
+++ b/config.py
@@ -9,7 +9,7 @@ DATA_PATH = BASE_DIR / "data" / "entries.csv"
DEFAULT_DATABASE_URL = f"sqlite:///{BASE_DIR / 'data' / 'boker-dev.sqlite3'}"
ELIGIBLE_MIN_SESSIONS = 3
-APP_VERSION = "2.5.14"
+APP_VERSION = "2.5.16"
def load_local_env(env_path: Path) -> None:
diff --git a/emails.py b/emails.py
index 1830f4c..5e68be0 100644
--- a/emails.py
+++ b/emails.py
@@ -1,6 +1,7 @@
#!/usr/bin/env python3
from __future__ import annotations
+from html import escape
import socket
from flask import current_app
@@ -26,15 +27,95 @@ def _send_message(msg: Message) -> None:
socket.setdefaulttimeout(previous_timeout)
+def _html_email(title: str, intro: str, cta_label: str | None = None, cta_url: str | None = None, code: str | None = None, note: str | None = None) -> str:
+ app_url = current_app.config.get("APP_BASE_URL", "https://myboker.org").rstrip("/")
+ cta_html = ""
+ if cta_label and cta_url:
+ cta_html = f"""
+
+ |
+ {escape(cta_label)}
+ |
+
+
+
+ If the button does not work, copy and paste this link:
+ {escape(cta_url)}
+ |
+
+ """
+
+ code_html = ""
+ if code:
+ code_html = f"""
+
+ |
+ {escape(code)}
+ |
+
+ """
+
+ note_html = ""
+ if note:
+ note_html = f"""
+
+ | {escape(note)} |
+
+ """
+
+ return f"""
+
+
+
+
+
+
+
+ |
+ myboker.org
+ |
+
+
+
+ {escape(title)}
+ |
+
+
+ | {escape(intro)} |
+
+ {code_html}
+ {cta_html}
+ {note_html}
+
+ |
+ This email was sent by myboker.org. If you did not request this, you can safely ignore it.
+ |
+
+
+ |
+
+
+
+"""
+
+
def send_password_reset(to_email: str, reset_url: str) -> None:
+ body = (
+ f"You requested a password reset for your myboker.org account.\n\n"
+ f"Reset your password:\n\n"
+ f"{reset_url}\n\n"
+ f"This link expires in 1 hour. If you did not request this, you can ignore this email."
+ )
msg = Message(
subject="Reset your myboker.org password",
recipients=[to_email],
- body=(
- f"You requested a password reset for your myboker.org account.\n\n"
- f"Click the link below to set a new password:\n\n"
- f"{reset_url}\n\n"
- f"This link expires in 1 hour. If you did not request this, you can ignore this email."
+ body=body,
+ html=_html_email(
+ "Reset your password",
+ "We received a request to reset the password for your myboker.org account.",
+ cta_label="Reset password",
+ cta_url=reset_url,
+ note="This link expires in 1 hour.",
),
sender=current_app.config.get("MAIL_DEFAULT_SENDER"),
)
@@ -42,13 +123,20 @@ def send_password_reset(to_email: str, reset_url: str) -> None:
def send_email_verification_code(to_email: str, code: str) -> None:
+ body = (
+ "Verify your myboker.org account with this code:\n\n"
+ f"{code}\n\n"
+ "This code expires in 15 minutes. If you did not create an account, you can ignore this email."
+ )
msg = Message(
subject="Your myboker.org verification code",
recipients=[to_email],
- body=(
- "Verify your myboker.org account with this code:\n\n"
- f"{code}\n\n"
- "This code expires in 15 minutes. If you did not create an account, you can ignore this email."
+ body=body,
+ html=_html_email(
+ "Verify your email",
+ "Enter this verification code to finish creating your myboker.org account.",
+ code=code,
+ note="This code expires in 15 minutes. Do not share it with anyone.",
),
sender=current_app.config.get("MAIL_DEFAULT_SENDER"),
)
@@ -57,14 +145,22 @@ def send_email_verification_code(to_email: str, code: str) -> None:
def send_league_invite(to_email: str, league_name: str, invite_url: str, invited_by_email: str, role: str) -> None:
role_label = role.strip().lower() if role else "member"
+ body = (
+ f"{invited_by_email} has invited you to join {league_name} as a {role_label} on myboker.org.\n\n"
+ f"Accept your invitation:\n\n"
+ f"{invite_url}\n\n"
+ f"This link expires in 7 days. You'll need a myboker.org account with this email address to accept."
+ )
msg = Message(
subject=f"You've been invited to join {league_name} on myboker.org",
recipients=[to_email],
- body=(
- f"{invited_by_email} has invited you to join {league_name} as a {role_label} on myboker.org.\n\n"
- f"Accept your invitation:\n\n"
- f"{invite_url}\n\n"
- f"This link expires in 7 days. You'll need a myboker.org account with this email address to accept."
+ body=body,
+ html=_html_email(
+ "You have a league invite",
+ f"{invited_by_email} invited you to join {league_name} as a {role_label}.",
+ cta_label="Accept invite",
+ cta_url=invite_url,
+ note="This invitation link expires in 7 days.",
),
sender=current_app.config.get("MAIL_DEFAULT_SENDER"),
)
diff --git a/routes/account.py b/routes/account.py
index 16d951a..d82fb22 100644
--- a/routes/account.py
+++ b/routes/account.py
@@ -117,6 +117,7 @@ def register():
email = normalize_email(form["email"])
password = request.form.get("password", "")
confirm_password = request.form.get("confirm_password", "")
+ existing_user = find_user_by_email(email)
if not email or "@" not in email:
flash("Enter a valid email address.", "error")
@@ -124,10 +125,18 @@ def register():
flash("Password must be at least 8 characters.", "error")
elif password != confirm_password:
flash("Passwords do not match.", "error")
- elif find_user_by_email(email):
+ elif existing_user and existing_user.disabled_at is None:
flash("An account already exists for that email.", "error")
else:
- user = create_user(email, password)
+ if existing_user:
+ user = existing_user
+ user.password_hash = hash_password(password)
+ user.disabled_at = None
+ user.email_verified_at = None
+ user.email_verification_code_hash = None
+ user.email_verification_sent_at = None
+ else:
+ user = create_user(email, password)
db.session.flush()
return redirect(_start_email_verification(user, safe_next_url(url_for("leagues.new"))))