fix email invites

This commit is contained in:
SowinskiBraeden committed 2026-06-27 10:15:55 -07:00
1 parent 00e640d6dc
commit 3026204a99
4 files changed
+48 -9

No files matched your search

+6 -1
View File
@@ -9,7 +9,7 @@ DATA_PATH = BASE_DIR / "data" / "entries.csv"
DEFAULT_DATABASE_URL = f"sqlite:///{BASE_DIR / 'data' / 'boker-dev.sqlite3'}" DEFAULT_DATABASE_URL = f"sqlite:///{BASE_DIR / 'data' / 'boker-dev.sqlite3'}"
ELIGIBLE_MIN_SESSIONS = 3 ELIGIBLE_MIN_SESSIONS = 3
APP_VERSION = "2.5.4" APP_VERSION = "2.5.5"
def load_local_env(env_path: Path) -> None: def load_local_env(env_path: Path) -> None:
@@ -44,9 +44,14 @@ class Config:
MAIL_SERVER: str = os.getenv("MAIL_SERVER", "") MAIL_SERVER: str = os.getenv("MAIL_SERVER", "")
MAIL_PORT: int = int(os.getenv("MAIL_PORT", "587")) MAIL_PORT: int = int(os.getenv("MAIL_PORT", "587"))
MAIL_USE_TLS: bool = os.getenv("MAIL_USE_TLS", "true").lower() in ("true", "1", "yes") MAIL_USE_TLS: bool = os.getenv("MAIL_USE_TLS", "true").lower() in ("true", "1", "yes")
MAIL_USE_SSL: bool = os.getenv(
"MAIL_USE_SSL",
"true" if MAIL_PORT == 465 else "false",
).lower() in ("true", "1", "yes")
MAIL_USERNAME: str | None = os.getenv("MAIL_USERNAME") or None MAIL_USERNAME: str | None = os.getenv("MAIL_USERNAME") or None
MAIL_PASSWORD: str | None = os.getenv("MAIL_PASSWORD") or None MAIL_PASSWORD: str | None = os.getenv("MAIL_PASSWORD") or None
MAIL_DEFAULT_SENDER: str = os.getenv("MAIL_DEFAULT_SENDER", "noreply@myboker.org") MAIL_DEFAULT_SENDER: str = os.getenv("MAIL_DEFAULT_SENDER", "noreply@myboker.org")
MAIL_TIMEOUT: float = float(os.getenv("MAIL_TIMEOUT", "5"))
class ProductionConfig(Config): class ProductionConfig(Config):
+25 -5
View File
@@ -1,12 +1,31 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from __future__ import annotations from __future__ import annotations
import socket
from flask import current_app from flask import current_app
from flask_mail import Message from flask_mail import Message
from extensions import mail from extensions import mail
def _mail_send_suppressed() -> bool:
state = current_app.extensions.get("mail")
return bool(getattr(state, "suppress", False))
def _send_message(msg: Message) -> None:
if not _mail_send_suppressed() and not current_app.config.get("MAIL_SERVER"):
raise RuntimeError("Mail server is not configured.")
previous_timeout = socket.getdefaulttimeout()
socket.setdefaulttimeout(float(current_app.config.get("MAIL_TIMEOUT", 5)))
try:
mail.send(msg)
finally:
socket.setdefaulttimeout(previous_timeout)
def send_password_reset(to_email: str, reset_url: str) -> None: def send_password_reset(to_email: str, reset_url: str) -> None:
msg = Message( msg = Message(
subject="Reset your myboker.org password", subject="Reset your myboker.org password",
@@ -19,19 +38,20 @@ def send_password_reset(to_email: str, reset_url: str) -> None:
), ),
sender=current_app.config.get("MAIL_DEFAULT_SENDER"), sender=current_app.config.get("MAIL_DEFAULT_SENDER"),
) )
mail.send(msg) _send_message(msg)
def send_league_invite(to_email: str, league_name: str, invite_url: str, invited_by_email: 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"
msg = Message( msg = Message(
subject=f"You've been invited to manage {league_name} on myboker.org", subject=f"You've been invited to join {league_name} on myboker.org",
recipients=[to_email], recipients=[to_email],
body=( body=(
f"{invited_by_email} has invited you to join {league_name} as a manager on myboker.org.\n\n" 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"Accept your invitation:\n\n"
f"{invite_url}\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." f"This link expires in 7 days. You'll need a myboker.org account with this email address to accept."
), ),
sender=current_app.config.get("MAIL_DEFAULT_SENDER"), sender=current_app.config.get("MAIL_DEFAULT_SENDER"),
) )
mail.send(msg) _send_message(msg)
+5 -2
View File
@@ -1204,6 +1204,7 @@ def invite_member(league_ref: str):
from auth import generate_invite_token from auth import generate_invite_token
from emails import send_league_invite from emails import send_league_invite
from flask import current_app from flask import current_app
from db_models import User
from league_repositories import find_membership, find_user_by_email from league_repositories import find_membership, find_user_by_email
league = require_league(league_ref, {"owner"}) league = require_league(league_ref, {"owner"})
@@ -1228,10 +1229,12 @@ def invite_member(league_ref: str):
token = generate_invite_token(league.id, email, role, current_user_id() or "") token = generate_invite_token(league.id, email, role, current_user_id() or "")
base_url = current_app.config.get("APP_BASE_URL", "").rstrip("/") base_url = current_app.config.get("APP_BASE_URL", "").rstrip("/")
invite_url = f"{base_url}{url_for('account.accept_invite', token=token)}" invite_url = f"{base_url}{url_for('account.accept_invite', token=token)}"
inviter = db.session.get(User, current_user_id() or "")
inviter_email = inviter.email if inviter is not None else "A league owner"
try: try:
send_league_invite(email, league.name, invite_url, current_user_id() or "") send_league_invite(email, league.name, invite_url, inviter_email, role)
flash(f"Invitation sent to {email}.", "success") flash(f"{role.title()} invitation sent to {email}.", "success")
except Exception: except Exception:
flash("Failed to send invitation email. Check your mail configuration.", "error") flash("Failed to send invitation email. Check your mail configuration.", "error")
+12 -1
View File
@@ -71,7 +71,7 @@
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
<form class="form-card" method="post" action="{{ url_for('leagues.invite_member', league_ref=league.url_ref) }}" style="padding:16px 20px 20px;border-top:1px solid var(--border);"> <form class="form-card" id="invite-member-form" method="post" action="{{ url_for('leagues.invite_member', league_ref=league.url_ref) }}" style="padding:16px 20px 20px;border-top:1px solid var(--border);">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<p class="kicker" style="margin-bottom:10px;">Invite by email</p> <p class="kicker" style="margin-bottom:10px;">Invite by email</p>
<div class="session-create-row" style="align-items:flex-end;gap:10px;"> <div class="session-create-row" style="align-items:flex-end;gap:10px;">
@@ -177,6 +177,17 @@
document.querySelectorAll('.modal-confirm-input').forEach(inp => { document.querySelectorAll('.modal-confirm-input').forEach(inp => {
inp.addEventListener('input', () => checkConfirmInput(inp)); inp.addEventListener('input', () => checkConfirmInput(inp));
}); });
const inviteForm = document.getElementById('invite-member-form');
if (inviteForm) {
inviteForm.addEventListener('submit', () => {
const btn = inviteForm.querySelector('button[type="submit"]');
if (!btn) return;
btn.disabled = true;
btn.textContent = 'Sending...';
inviteForm.setAttribute('aria-busy', 'true');
});
}
})(); })();
</script> </script>