fix email invites
This commit is contained in:
1 parent
00e640d6dc
commit
3026204a99
4 files changed
+48
-9
No files matched your search
@@ -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.4"
|
||||
APP_VERSION = "2.5.5"
|
||||
|
||||
|
||||
def load_local_env(env_path: Path) -> None:
|
||||
@@ -44,9 +44,14 @@ class Config:
|
||||
MAIL_SERVER: str = os.getenv("MAIL_SERVER", "")
|
||||
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_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_PASSWORD: str | None = os.getenv("MAIL_PASSWORD") or None
|
||||
MAIL_DEFAULT_SENDER: str = os.getenv("MAIL_DEFAULT_SENDER", "noreply@myboker.org")
|
||||
MAIL_TIMEOUT: float = float(os.getenv("MAIL_TIMEOUT", "5"))
|
||||
|
||||
|
||||
class ProductionConfig(Config):
|
||||
|
||||
@@ -1,12 +1,31 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import socket
|
||||
|
||||
from flask import current_app
|
||||
from flask_mail import Message
|
||||
|
||||
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:
|
||||
msg = Message(
|
||||
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"),
|
||||
)
|
||||
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(
|
||||
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],
|
||||
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"{invite_url}\n\n"
|
||||
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"),
|
||||
)
|
||||
mail.send(msg)
|
||||
_send_message(msg)
|
||||
+5
-2
@@ -1204,6 +1204,7 @@ def invite_member(league_ref: str):
|
||||
from auth import generate_invite_token
|
||||
from emails import send_league_invite
|
||||
from flask import current_app
|
||||
from db_models import User
|
||||
from league_repositories import find_membership, find_user_by_email
|
||||
|
||||
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 "")
|
||||
base_url = current_app.config.get("APP_BASE_URL", "").rstrip("/")
|
||||
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:
|
||||
send_league_invite(email, league.name, invite_url, current_user_id() or "")
|
||||
flash(f"Invitation sent to {email}.", "success")
|
||||
send_league_invite(email, league.name, invite_url, inviter_email, role)
|
||||
flash(f"{role.title()} invitation sent to {email}.", "success")
|
||||
except Exception:
|
||||
flash("Failed to send invitation email. Check your mail configuration.", "error")
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
</div>
|
||||
{% endfor %}
|
||||
</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() }}">
|
||||
<p class="kicker" style="margin-bottom:10px;">Invite by email</p>
|
||||
<div class="session-create-row" style="align-items:flex-end;gap:10px;">
|
||||
@@ -177,6 +177,17 @@
|
||||
document.querySelectorAll('.modal-confirm-input').forEach(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>
|
||||
|
||||
|
||||
Reference in new issue
Block a user