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'}"
|
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):
|
||||||
|
|||||||
@@ -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
@@ -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")
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user