3.7 KiB
3.7 KiB
Email Invite Implementation — Jul 9, 2026
What was built
Commissioners can invite players via email from the league settings page. The invite sends a Sho'Nuff-styled HTML email with the league's invite code and a signed JWT link.
Files changed
server.py (backend)
New models:
InviteRequest(Pydantic):{email: str}
New function:
send_invite_email(to_email, league_name, commish_name, invite_code, signed_link)builds and sends HTML email via the same SMTP pipeline (mail.germainebrown.com:2525)
New endpoint:
POST /api/leagues/{league_id}/invite— commissioner-only, validates league is in draft, not full, then generates a signed JWT invite token (7-day expiry) and sends the email.
Signed token payload:
jwt.encode({
"league_id": league_id,
"invite_code": league["invite_code"],
"exp": datetime.utcnow() + timedelta(days=7),
"iat": datetime.utcnow(),
}, JWT_SECRET, algorithm="HS256")
Signed link: https://shark.iamgmb.com/?invite={token}
league.html (frontend)
Added HTML:
<div id="inviteByEmailSection">with amber-bordered.settings-cardwrapping:- Email input (
#inviteEmailInput) - Send button (
#sendInviteBtn) - Status div (
#inviteStatus)
- Email input (
- Only visible to commissioner
Added JS functions:
renderInviteSection()— called fromrenderLeague(), shows/hides based onis_commissionersendInvite()— validates email (non-empty, has @ and .), POSTs to/api/leagues/{id}/invite, shows success/error, clears input on success
Email template
- Dark ocean background (
#0a1628), amber headers, Sho'Nuff signature - Prominent invite code in amber monospace (28px, 4px letter-spacing)
- "Accept Invitation" CTA button (amber gradient)
- Fallback invite code instructions
- Random Sho'Nuff title + closing quote (inline lists in server.py)
- Base64 signature image from
/root/.hermes/references/shonuff-image-b64.txt - BCC to
g@germainebrown.com
Testing
# Register a commish
POST /api/auth/register {"email": "test@example.com", "password": "testpass123", "display_name": "TestCommish"}
# Create league
POST /api/leagues {"name": "Test League", "max_players": 6}
# Send invite
POST /api/leagues/{id}/invite {"email": "g@germainebrown.com"}
Server restart pitfalls
If the old process holds port 8083:
fuser -k 8083/tcp
sleep 1
fuser 8083/tcp # should print nothing
# then restart
Verify route registered by checking OpenAPI schema or hitting it (expect 401 without auth, not 405):
python3 -c "
import json, urllib.request
resp = urllib.request.urlopen('http://localhost:8083/openapi.json')
data = json.loads(resp.read())
for p in sorted(data.get('paths', {}).keys()):
if 'invite' in p:
print(p, ':', list(data['paths'][p].keys()))
"
A 405 "Method Not Allowed" means the old server is running. Kill it and restart.
Known limitations
- Invite query param (
?invite=...) is not consumed client-side — the landing page atindex.htmldoesn't read the signed token or pre-fill the invite code. Recipients must use the plain invite code manually. The token exists in the URL but the frontend join flow doesn't process it yet. - Inline Sho'Nuff title/closing lists —
send_invite_email()has hardcoded arrays matching the reference files. Ifshonuff-titles.pyorshonuff-closings.pychange, server.py must be updated manually. - No rate limiting — commissioner can hit the endpoint rapidly. SMTP spam protection is at the mail relay level.
- No duplicate email detection — same email can be invited multiple times.
- No invite history/audit trail — invites are fire-and-forget, no DB record.