Skip to content

Commit 64d5cb1

Browse files
committed
docs(plans): harvest and record more opus plans
Signed-off-by: Xe Iaso <me@xeiaso.net>
1 parent ff89f87 commit 64d5cb1

File tree

2 files changed

+232
-0
lines changed

2 files changed

+232
-0
lines changed
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
# Plan: Add Patreon OAuth Login to Sponsor Panel
2+
3+
## Context
4+
5+
The sponsor-panel (`cmd/sponsor-panel/`) currently only supports GitHub OAuth login. Patreon patrons have no way to access sponsor benefits (Discord invite, team invitations, logo submissions). This change adds Patreon as a second authentication provider so patrons get feature parity with GitHub Sponsors.
6+
7+
**Design decisions:**
8+
9+
- User-token based verification (no saasproxy dependency)
10+
- Separate identities (no account linking between GitHub and Patreon)
11+
- Patreon API v2 via direct HTTP calls; `patreon-go` library used only for OAuth URL constants
12+
- OAuth scopes: `identity`, `identity[email]`, `campaigns.members`
13+
- Team invite: Patreon $50+ users see the same card and input a GitHub username
14+
15+
---
16+
17+
## Step 1: Database Migration
18+
19+
**File:** `cmd/sponsor-panel/migrations.go`
20+
21+
Add `migration002` constant with idempotent DDL:
22+
23+
- `ALTER TABLE users ALTER COLUMN github_id DROP NOT NULL`
24+
- `ADD COLUMN IF NOT EXISTS patreon_id TEXT UNIQUE`
25+
- `ADD COLUMN IF NOT EXISTS provider TEXT NOT NULL DEFAULT 'github'`
26+
- Drop `users_login_key` unique constraint, replace with `UNIQUE INDEX (provider, login)`
27+
- Add `idx_users_patreon_id` index
28+
29+
Update `runMigrations()` to execute `migration002` after `migrationSchema`.
30+
31+
---
32+
33+
## Step 2: Update User Model
34+
35+
**File:** `cmd/sponsor-panel/models.go`
36+
37+
- Change `User.GitHubID` from `int64` to `*int64` (nullable)
38+
- Add `PatreonID *string` and `Provider string` fields
39+
- Update all `SELECT`/`Scan` calls in `getUserByID`, `upsertUser` to include new columns
40+
- Add `upsertPatreonUser(ctx, pool, user)` — same pattern as `upsertUser` but upserts by `patreon_id`, sets `provider='patreon'`
41+
42+
---
43+
44+
## Step 3: Add Patreon OAuth Config
45+
46+
**File:** `cmd/sponsor-panel/main.go`
47+
48+
New flags (all optional — service still works GitHub-only if omitted):
49+
50+
- `--patreon-client-id`
51+
- `--patreon-client-secret`
52+
- `--patreon-redirect-url`
53+
- `--patreon-campaign-id` (to match pledge against)
54+
55+
Add to `Server` struct:
56+
57+
```go
58+
patreonOAuth *oauth2.Config // nil if not configured
59+
patreonCampaignID string
60+
```
61+
62+
In `main()`, conditionally create `oauth2.Config` using `patreon.AuthorizationURL` and `patreon.AccessTokenURL` from `gopkg.in/mxpv/patreon-go.v1`.
63+
64+
Register new routes:
65+
66+
```
67+
/login/patreon → server.patreonLoginHandler
68+
/callback/patreon → server.patreonCallbackHandler
69+
```
70+
71+
---
72+
73+
## Step 4: Patreon OAuth Handlers (new file)
74+
75+
**File:** `cmd/sponsor-panel/patreon_oauth.go` (new)
76+
77+
### `patreonLoginHandler`
78+
79+
Mirrors `loginHandler` in `oauth.go`: generate state, set CSRF cookie, redirect to `s.patreonOAuth.AuthCodeURL(state)`. Returns 404 if `s.patreonOAuth == nil`.
80+
81+
### `patreonCallbackHandler`
82+
83+
1. Validate state cookie (same CSRF pattern as GitHub)
84+
2. Exchange code via `s.patreonOAuth.Exchange(ctx, code)`
85+
3. Call Patreon API v2 identity endpoint:
86+
```
87+
GET https://www.patreon.com/api/oauth2/v2/identity
88+
?include=memberships.campaign
89+
&fields[user]=full_name,vanity,email,image_url
90+
&fields[member]=patron_status,currently_entitled_amount_cents
91+
```
92+
4. Parse JSON:API response to extract user identity and membership data
93+
5. Find membership matching `s.patreonCampaignID`
94+
6. Build `SponsorshipData` JSON: `{is_active, monthly_amount_cents, tier_name}`
95+
7. Call `upsertPatreonUser()` with `provider="patreon"`, login = vanity or full_name
96+
8. Create session (same `user_id` in gorilla/sessions cookie)
97+
9. Redirect to `/`
98+
99+
### Response types (define in same file):
100+
101+
- `patreonIdentityResponse` — JSON:API envelope with user data + included memberships
102+
- `patreonMember` — membership attributes (patron_status, currently_entitled_amount_cents) + campaign relationship
103+
104+
---
105+
106+
## Step 5: Update Login Template
107+
108+
**File:** `cmd/sponsor-panel/templates/login.templ`
109+
110+
Change signature to `templ Login(patreonEnabled bool)`. Add a "Login with Patreon" button (with Patreon SVG icon) conditionally rendered when `patreonEnabled` is true, linking to `/login/patreon`.
111+
112+
---
113+
114+
## Step 6: Update Dashboard for Provider Awareness
115+
116+
**File:** `cmd/sponsor-panel/templates/dashboard.templ`
117+
118+
- Add `Provider string` to `UserProps`
119+
- In `SponsorshipCard`, when user is not a sponsor: show Patreon link for `provider == "patreon"`, GitHub Sponsors link otherwise
120+
121+
**File:** `cmd/sponsor-panel/dashboard.go`
122+
123+
- `loginPageHandler`: pass `s.patreonOAuth != nil` to `templates.Login()`
124+
- `dashboardHandler`: set `UserProps.Provider` from `user.Provider`
125+
126+
---
127+
128+
## Step 7: Generate & Build
129+
130+
1. `go tool templ generate` (regenerate `*_templ.go` files)
131+
2. `go build ./cmd/sponsor-panel/`
132+
3. `npm test` (`go test ./...`)
133+
134+
---
135+
136+
## Files Modified
137+
138+
| File | Change |
139+
| --------------------------------------------- | ----------------------------------------------- |
140+
| `cmd/sponsor-panel/migrations.go` | Add migration002 |
141+
| `cmd/sponsor-panel/models.go` | Update User struct, add upsertPatreonUser |
142+
| `cmd/sponsor-panel/main.go` | Add flags, Server fields, routes |
143+
| `cmd/sponsor-panel/patreon_oauth.go` | **New** — Patreon login/callback handlers |
144+
| `cmd/sponsor-panel/oauth.go` | No changes (existing GitHub flow untouched) |
145+
| `cmd/sponsor-panel/dashboard.go` | Pass patreonEnabled and Provider |
146+
| `cmd/sponsor-panel/templates/login.templ` | Add Patreon button |
147+
| `cmd/sponsor-panel/templates/dashboard.templ` | Add Provider to props, conditional sponsor link |
148+
149+
## Existing Code to Reuse
150+
151+
- `generateState()` in `oauth.go:20` — reuse for Patreon CSRF state
152+
- `SponsorshipData` struct in `models.go:27` — same JSON format for both providers
153+
- `User.IsSponsorAtTier()` in `models.go:35` — works provider-agnostically
154+
- Session management via `getSessionUser()` in `oauth.go:634` — no changes needed
155+
- `patreon.AuthorizationURL` / `patreon.AccessTokenURL` from `gopkg.in/mxpv/patreon-go.v1`
156+
- All feature handlers (`inviteHandler`, `logoHandler`) — work unchanged since they only check `IsSponsorAtTier()`
157+
158+
## Verification
159+
160+
1. **Build**: `go build ./cmd/sponsor-panel/` compiles without errors
161+
2. **Tests**: `npm test` passes
162+
3. **GitHub flow unchanged**: Login with GitHub still works identically
163+
4. **Patreon login**: With Patreon OAuth credentials set, clicking "Login with Patreon" redirects to Patreon, callback creates user with `provider=patreon`
164+
5. **Tier gating**: A Patreon user pledging $50+/month sees team invite card; $1+ sees logo submission and Discord
165+
6. **No Patreon config**: When Patreon flags are omitted, login page only shows GitHub button, `/login/patreon` returns 404
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Replace GitHub Sponsor iframe with Styled Sponsor Card
2+
3+
## Context
4+
5+
The homepage (`lume/src/index.jsx` line 40) currently embeds a GitHub Sponsors iframe. This should be replaced with a custom-styled card linking to Patreon, GitHub Sponsors, and the Sponsor Panel (sponsors.xeiaso.net). The card design should echo the sponsor-panel app's warm Gruvbox aesthetic.
6+
7+
## File to modify
8+
9+
- `lume/src/index.jsx` — the only file that needs changes
10+
11+
## Design reference (from `cmd/sponsor-panel`)
12+
13+
- Cards: `rounded-2xl`, border, surface bg, `overflow-hidden`, subtle shadow
14+
- Gradient accent line: 2px at top, orange-to-pink for warm variant
15+
- Card titles: `font-serif`, semibold
16+
- Buttons: `rounded-xl`, colored bg, hover lift (`hover:-translate-y-px`), white text
17+
18+
## Plan
19+
20+
### 1. Add small icon components before the default export
21+
22+
Copy SVG paths from `lume/src/donate.jsx` (GitHubIcon, PatreonIcon) scaled to 20x20. Add a star icon for the Sponsor Panel link.
23+
24+
### 2. Add `SponsorCard` component
25+
26+
Structure:
27+
28+
```
29+
<div> — card container (rounded-2xl, border, bg-bg-2, dark:bg-bgDark-2, shadow-sm, overflow-hidden, max-w-xl, mx-auto, my-6)
30+
<div> — gradient accent line (2px, orange→purple, with dark mode variant via dark:hidden/hidden dark:block)
31+
<div> — content area (px-6 py-5 text-center)
32+
<h3> — "Support My Work" with heart SVG icon
33+
<p> — brief description (text-sm, muted color)
34+
<div> — flex row of 3 button-links (flex-wrap, gap-3, centered)
35+
<a> Patreon — bg-orange-light / dark:bg-orangeDark-light
36+
<a> GitHub Sponsors — bg-fg-0 / dark:bg-purpleDark-light
37+
<a> Sponsor Panel — bg-purple-light / dark:bg-blueDark-light
38+
```
39+
40+
### 3. Replace the iframe (line 40) with `<SponsorCard />`
41+
42+
## Key implementation details
43+
44+
**Gradient accent line:** Use two `<div>`s with `dark:hidden` / `hidden dark:block` to swap light/dark gradients, since inline `style` can't respond to `prefers-color-scheme`:
45+
46+
- Light: `linear-gradient(90deg, #d65d0e, #b16286)` (orange→purple)
47+
- Dark: `linear-gradient(90deg, #fe8019, #d3869b)` (bright orange→pink)
48+
49+
**Button link style overrides:** The site's base `<a>` styles (in `lume/src/styles.css` line 61-63 and `hack.css` line 65-71) apply link colors, underline, border-bottom, and visited styles. Each button `<a>` needs:
50+
51+
- `no-underline border-0` to remove text decoration and bottom border
52+
- `text-white hover:text-white hover:bg-[color]` to override link/hover colors
53+
- `visited:text-white visited:hover:text-white visited:hover:bg-[color]` to override visited states
54+
55+
**Color tokens available** (from `lume/tailwind.config.js`):
56+
57+
- `bg-orange-light`, `dark:bg-orangeDark-light`, `hover:bg-orange-dark`
58+
- `bg-purple-light`, `dark:bg-blueDark-light`, `hover:bg-purple-dark`
59+
- `bg-fg-0`, `dark:bg-purpleDark-light`, `hover:bg-fg-1`
60+
- `text-fg-0`, `dark:text-fgDark-1`, `text-fg-3`, `dark:text-fgDark-3`
61+
62+
## Verification
63+
64+
1. Run `npm run dev` and check the homepage in both light and dark mode
65+
2. Verify all three links open correctly in new tabs
66+
3. Confirm the card is responsive (shrinks gracefully on mobile)
67+
4. Check that button hover states work (lift effect, color change, no link underline artifacts)

0 commit comments

Comments
 (0)