feat: add public sponsors API for desktop app - #57
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughA new Next.js API route is added at ChangesSponsors Public Feed Route
Estimated code review effort🎯 2 (Simple) | ⏱️ ~8 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/app/api/v1/sponsors/route.ts`:
- Around line 37-47: The sponsors feed mapping in the GET handler can throw
because `new URL(s.avatar, origin)` is applied directly to every item, so a
single malformed `avatar` value from `sponsors-store.ts` can fail the whole
response. Update the `items` mapping in the sponsors route to resolve `avatar`
defensively per sponsor, catching invalid URL cases and falling back to the
original `s.avatar` value or `undefined` instead of throwing.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: e33d9758-d93c-4d1c-9dbc-e0edfb2bdf32
📒 Files selected for processing (1)
src/app/api/v1/sponsors/route.ts
| const items = sponsors.map((s) => ({ | ||
| id: s.id, | ||
| name: s.name, | ||
| url: s.url, | ||
| since: s.since, | ||
| note: s.note, | ||
| // Resolve relative avatar paths (uploaded avatars) to absolute URLs; leave | ||
| // already-absolute external URLs (e.g. GitHub CDN) untouched. | ||
| avatar: s.avatar ? new URL(s.avatar, origin).toString() : undefined, | ||
| tier: s.tier, | ||
| })); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
A single malformed avatar URL will 500 the entire feed.
new URL(s.avatar, origin) throws on an invalid input. Internal paths from resolveAvatar are always well-formed, but row.avatarUrl is an externally/admin-supplied value (per sponsors-store.ts); one bad entry would reject the whole GET and break the endpoint for all sponsors. Resolve defensively per-item and fall back to the raw value (or undefined).
🛡️ Proposed defensive resolution
+function toAbsoluteAvatar(avatar: string | undefined, origin: string): string | undefined {
+ if (!avatar) return undefined;
+ try {
+ return new URL(avatar, origin).toString();
+ } catch {
+ return undefined;
+ }
+}
+
export async function GET(req: NextRequest) {
const origin = req.nextUrl.origin;
const sponsors = await getPublicSponsors();
const items = sponsors.map((s) => ({
id: s.id,
name: s.name,
url: s.url,
since: s.since,
note: s.note,
// Resolve relative avatar paths (uploaded avatars) to absolute URLs; leave
// already-absolute external URLs (e.g. GitHub CDN) untouched.
- avatar: s.avatar ? new URL(s.avatar, origin).toString() : undefined,
+ avatar: toAbsoluteAvatar(s.avatar, origin),
tier: s.tier,
}));📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const items = sponsors.map((s) => ({ | |
| id: s.id, | |
| name: s.name, | |
| url: s.url, | |
| since: s.since, | |
| note: s.note, | |
| // Resolve relative avatar paths (uploaded avatars) to absolute URLs; leave | |
| // already-absolute external URLs (e.g. GitHub CDN) untouched. | |
| avatar: s.avatar ? new URL(s.avatar, origin).toString() : undefined, | |
| tier: s.tier, | |
| })); | |
| function toAbsoluteAvatar(avatar: string | undefined, origin: string): string | undefined { | |
| if (!avatar) return undefined; | |
| try { | |
| return new URL(avatar, origin).toString(); | |
| } catch { | |
| return undefined; | |
| } | |
| } | |
| export async function GET(req: NextRequest) { | |
| const origin = req.nextUrl.origin; | |
| const sponsors = await getPublicSponsors(); | |
| const items = sponsors.map((s) => ({ | |
| id: s.id, | |
| name: s.name, | |
| url: s.url, | |
| since: s.since, | |
| note: s.note, | |
| // Resolve relative avatar paths (uploaded avatars) to absolute URLs; leave | |
| // already-absolute external URLs (e.g. GitHub CDN) untouched. | |
| avatar: toAbsoluteAvatar(s.avatar, origin), | |
| tier: s.tier, | |
| })); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/app/api/v1/sponsors/route.ts` around lines 37 - 47, The sponsors feed
mapping in the GET handler can throw because `new URL(s.avatar, origin)` is
applied directly to every item, so a single malformed `avatar` value from
`sponsors-store.ts` can fail the whole response. Update the `items` mapping in
the sponsors route to resolve `avatar` defensively per sponsor, catching invalid
URL cases and falling back to the original `s.avatar` value or `undefined`
instead of throwing.
What
Adds a public, unauthenticated
GET /api/v1/sponsorsendpoint that feeds the desktop app's About → Sponsors section. Mirrors the public sponsor wall on the site but is shaped for an external HTTP client.Details
amountCentsis never exposed./api/sponsor-avatar/...) are resolved against the request origin so the desktop client can load them without knowing the site origin; already-absolute external URLs (e.g. GitHub CDN) are left untouched.*) — payload is public and read-only; the desktop webview fetches cross-origin.OPTIONSpreflight handled.public, max-age=300, s-maxage=300(5 min); sponsor changes are infrequent.getPublicSponsors()fromsponsors-store.Response shape
{ "items": [{ "id", "name", "url", "since", "note", "avatar", "tier" }], "count": N }Summary by CodeRabbit
New Features
Bug Fixes
Performance