Skip to content

feat(profile): show reputation badges and history - #27

Merged
mcfarhat merged 2 commits into
developfrom
feat/reputation-profile-badges
Sep 2, 2026
Merged

mcfarhat merged 2 commits into
developfrom
feat/reputation-profile-badges

Conversation

@KareemN1936

Copy link
Copy Markdown
Collaborator

Summary

  • Add a public profile API at GET /profiles/:username backed by existing users, latest reputation snapshots, contribution drafts, canon lore entries, and app vote history.
  • Derive MVP reputation levels/badges from the stored reputation score without changing voting weight or the scoring model.
  • Replace the placeholder profile page with a real profile view showing score, badge, contribution history, canon entries, and recent votes.

Trello

Verification

  • npm test --workspace=@hivelore/api (200 passed, 1 skipped because TEST_DATABASE_ADMIN_URL is not configured locally)
  • npm run typecheck --workspace=@hivelore/api
  • npm run typecheck --workspace=@hivelore/web
  • npm run lint --workspace=@hivelore/api
  • npm run lint --workspace=@hivelore/web
  • npm run build --workspace=@hivelore/api
  • npm run build --workspace=@hivelore/web

@KareemN1936

Copy link
Copy Markdown
Collaborator Author

Implementation note for Trello card https://trello.com/c/6Fprw569:

This PR builds the profile + badges layer on top of the existing reputation snapshot model rather than changing reputation scoring or voting weight.

Implemented:

  • public GET /profiles/:username endpoint;
  • latest reputation snapshot serialization with MVP badge levels;
  • contribution draft, canon lore entry, and recent vote history;
  • real /profile/[username] page replacing the placeholder.

Verified locally:

  • npm test --workspace=@hivelore/api (200 passed, 1 skipped because TEST_DATABASE_ADMIN_URL is not configured locally)
  • npm run typecheck --workspace=@hivelore/api
  • npm run typecheck --workspace=@hivelore/web
  • npm run lint --workspace=@hivelore/api
  • npm run lint --workspace=@hivelore/web
  • npm run build --workspace=@hivelore/api
  • npm run build --workspace=@hivelore/web

@github-actions

github-actions Bot commented Aug 30, 2026 •

Copy link
Copy Markdown

Vercel preview is ready:

https://hivelore-bdj31m4u9-kareemn1936s-projects.vercel.app

Commit: 52e0938

GET /profiles/:username is unauthenticated, but the profile query filtered
only on the user. contributionDrafts and loreEntries were selected with no
status filter, so any visitor could read another user's unsubmitted draft
titles and the world they were drafting in.

- contributionDrafts: restrict to ContributionStatus.SUBMITTED
- loreEntries: restrict to LoreStatus.PUBLISHED_CANON, so DRAFT, SUBMITTED,
  APPROVED_FOR_PUBLICATION and ARCHIVED stay private to the author

The existing test stub returned fixture rows regardless of the query, so a
missing filter could not surface. It now honours the nested `where` the way
Prisma would, and the fixture carries an unsubmitted draft and a non-canon
lore entry that the new test asserts are absent. Removing either filter
fails two tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@mcfarhat mcfarhat left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving with one fix pushed directly to your branch (4fdf70d), since you're the only one on this now and it was small.

The badge work is solid

Tests genuinely bite — I mutated the logic and each of these failed a test: shifting the Canon Keeper threshold above the tested score, swallowing InvalidHiveUsernameError, and changing nextScore. Validation and the 404 path are properly covered.

What I fixed — a privacy leak on a public endpoint

GET /profiles/:username has no auth hook, and the profile query filtered only on the user. Both relations were selected with no status filter:

contributionDrafts: { orderBy: …, select: { title, status, world … }, take: 10 }
loreEntries:        { orderBy: …, select: { title, status, slug  … } }

ContributionStatus is DRAFT | SUBMITTED, so any unauthenticated visitor could read another user's unsubmitted draft titles and which world they were drafting in. Same for non-canon loreEntries (DRAFT, SUBMITTED, APPROVED_FOR_PUBLICATION, ARCHIVED).

Fix:

  • contributionDrafts → where: { status: 'SUBMITTED' }
  • loreEntries → where: { status: 'PUBLISHED_CANON' }

Why the tests didn't catch it — worth knowing for next time

The stub's findUnique returned the fixture rows regardless of the query, so a missing where could never surface: the mock answered the same whether you filtered or not. The fixture also only contained SUBMITTED/PUBLISHED_CANON rows, so there was nothing private to leak.

I changed the stub to honour the nested where the way Prisma does, and added an unsubmitted draft plus a non-canon lore entry to the fixture, with a test asserting neither appears in the response. Removing either filter now fails two tests — verified by mutation both ways.

That's the general lesson: a mock that ignores the query can only test your assumptions, not your code. When the stub answers identically for every query, absence-of-filter is invisible.

Gates

Quality and Vercel Preview both pass. Full api suite: 20 failures locally both before and after my change — all environmental (no local Postgres), zero regression from the fix.

@mcfarhat
mcfarhat merged commit 3b5c743 into develop Sep 2, 2026
2 checks passed
@mcfarhat
mcfarhat deleted the feat/reputation-profile-badges branch September 2, 2026 12:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants