99 lines
3.7 KiB
Markdown
99 lines
3.7 KiB
Markdown
# 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:**
|
|
```python
|
|
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-card` wrapping:
|
|
- Email input (`#inviteEmailInput`)
|
|
- Send button (`#sendInviteBtn`)
|
|
- Status div (`#inviteStatus`)
|
|
- Only visible to commissioner
|
|
|
|
**Added JS functions:**
|
|
- `renderInviteSection()` — called from `renderLeague()`, shows/hides based on `is_commissioner`
|
|
- `sendInvite()` — 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
|
|
|
|
```bash
|
|
# 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:
|
|
```bash
|
|
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):
|
|
```bash
|
|
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
|
|
|
|
1. **Invite query param (`?invite=...`) is not consumed client-side** — the landing page at `index.html` doesn'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.
|
|
2. **Inline Sho'Nuff title/closing lists** — `send_invite_email()` has hardcoded arrays matching the reference files. If `shonuff-titles.py` or `shonuff-closings.py` change, server.py must be updated manually.
|
|
3. **No rate limiting** — commissioner can hit the endpoint rapidly. SMTP spam protection is at the mail relay level.
|
|
4. **No duplicate email detection** — same email can be invited multiple times.
|
|
5. **No invite history/audit trail** — invites are fire-and-forget, no DB record.
|