Public viewable binders - #308
Conversation
Public profile (custom handle) exposing owner-shared binders, viewable anonymously and discoverable in-app. Dedicated unauthenticated /api/public namespace with whitelist serializers so purchase price / cost / P&L cannot leak; market values shown only on owner opt-in. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
11 TDD tasks: data model + migrations, handle validation, whitelist serialization, unauthenticated public API, owner controls, per-binder toggle, leaderboard discovery, frontend pages/routes + token-less client, owner UI, cache/rate-limit. Backend unittest + frontend vitest throughout. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
- Add is_public field to BinderUpdate schema (Optional[bool] = None) - Add is_public field to BinderResponse schema (bool = False) - Include is_public in _binder_response() constructor - Persist is_public updates in update_binder() - Add BinderPublicToggleTests test class Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…lient Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… frontend) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds PublicProfile and PublicBinderView pages consuming the Task 8 publicClient, a shared formatEur(value) helper (returns null for hidden/non-numeric values so callers can conditionally render), and wires /u/:handle and /u/:handle/binder/:binderId as siblings in the top-level Routes, before the /* catch-all, so they render outside ProtectedRoutes for anonymous visitors. Co-Authored-By: Claude <noreply@anthropic.com>
…rboard links Add GET /api/profile/ to hydrate the public-profile columns (they live on User, not the settings key/value store), plus client.js helpers, a Settings.jsx "Public Profile" section (handle input with debounced availability check, public/show-values toggles, copy link), a per-binder "Share publicly" toggle in Binders.jsx, and a public-profile link on Leaderboard rows that have a handle. Co-Authored-By: Claude <noreply@anthropic.com>
Add Cache-Control: public, max-age=300 to both public GET endpoints (get_public_profile, get_public_binder) via a test-safe defaulted response: Response = None param, so Task 4's direct-call unit tests (no response arg) keep passing. Rate limiting: no per-route @limiter.limit decorator added. main.py's existing Limiter(default_limits=["60/minute"]) + SlowAPIMiddleware already applies globally to these routes, and importing that limiter into api/public.py would create an import cycle (main.py imports api.public). Documented via code comment instead. Co-Authored-By: Claude <noreply@anthropic.com>
…iants Public binder view returned cards in arbitrary DB order and omitted the variant, so cards were unordered and variant prints were indistinguishable (and mispriced when values were shown). - _binder_cards: order by added_at desc (matches owner's binder view) and eager-load card/set/collection_item to avoid N+1 - _serialize_card: expose variant (from the linked collection item) and use it for effective_market_price; summary total_value now variant-correct - PublicCard schema gains variant; PublicBinderView renders VariantPills - tests: variant presence, variant-absent default, added_at ordering Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The public binder listed cards newest-added-first (matching the owner's app view), but collectors expect card-number order. Sort by set, then natural card number (1, 2, 10 — not 1, 10, 2), then variant so same-number prints group. - extract natural_card_number_key into services/card_numbers.py (shared); api/sets.py now imports it instead of a private duplicate - public_profile._binder_cards sorts in Python via that key - test asserts 1, 2, 10 ordering regardless of add order Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
public,max-age=300 pinned a stale browser copy for up to 5 minutes, so owner edits (unshare, value toggle) and card-order/variant fixes appeared not to take effect until the cache expired. Drop to public,max-age=30,must-revalidate so changes surface within seconds while still blunting load bursts. Dedupe the two header assignments into _set_public_cache and pin the value with tests (set on success, absent on 404). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PublicBinderView imported ../components/VariantPills, which was never added on this branch - build has been failing standalone. VariantPills was unified into CardStateIndicators and merged upstream as part of v1.24.0 (PR Git-Romer#295), so pull that component plus its cardVariants.js dependency straight from upstream/main instead of reintroducing the old deploy-only component. Also brings in groupCardsByPrint (grouped, stacked-tile rendering) already proven on the deploy branch. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
This may be a challenge for instances hosted behind some other auth mechanism. Right now I am using forward auth via Authentik. I was thinking of adding proper OIDC support but haven't gotten to it yet. This may impact that. Will have a look later this week. |
|
Hi @hiddenbanana 👋 Thank you very much for this PR! Public profiles and shareable binders are a great addition, and your implementation gave me a strong foundation to build on. After testing it, I made several follow-up adjustments to align it with the rest of the application: • Added an admin-controlled global switch, disabled by default. The final version passed 276 backend tests, 64 frontend tests, PostgreSQL migration tests, Docker builds, desktop/mobile browser workflows, and two independent review passes. The PR is now ready from my side. Thanks again for contributing this feature. I really appreciate the time and work you put into it! 🙌 |
|
Amazing 🔥 |
|
just pulled 1.26.0 to my deploy... youve done some tweaks i was honestly considering :D the stacking looks awesome and the legend is awesome :D |
|
@engelben, you were right to flag forward-auth setups here. PokéCollector treats the public routes as anonymous, but Authentik still sees and protects those requests before they reach the app. I added a follow-up in #315 with the exact routes needed for the public pages, API data, card images, trainer sprites, frontend bundles, and fallback assets. It includes separate Authentik examples for single-application and domain-level forward auth, plus a checklist to confirm that collection, settings, and admin routes remain protected. This gives existing Authentik installations a safe setup for public profiles. Native OIDC would still be separate work. |
Really happy to hear that 😄 The stacking took a few rounds to get right, but it makes mixed variants much easier to understand without filling the card with more labels. The legend ties the badges together nicely too. Funny that you were already considering some of the same changes. Your original binder implementation made them much easier to add. |
|
can only add the same - stacking is a super idea. Also thanks for the forward auth additions |
Summary
/u/audit-trainer) with reserved-name, invalid-character, collision, migration, and concurrent-claim protection./u, public profile pages, and public collection-binder pages.1.26.0.Public response behavior
public, max-age=0, must-revalidate.no-store.Authorizationheader, including when an expired token exists in local storage.Validation
git diff --checkDeliberately out of scope