Repository navigation
Conversation
…tion
Creating an invitation link was ungated -- `create_invitation_link` carried a
TODO saying as much -- so any logged-in account could mint unlimited 100-use
links. It is now gated on MIN_KARMA_INVITE_USER, following the same shape as
the upload and site-creation gates, with the superuser exempt as in
`services/sites.py`.
The invite reward was dead code: `pay_reward_for_invitation` had no caller, so
INVITE_NEW_USER_REWARD and INVITE_NEW_USER_MAX_REWARDED were promises nothing
kept, and `sent_new_user_invitataions` was never written. (The rules-applied
ratchet was green throughout, because it looks for readers, not reachable
callers.) The reward is now paid when a signup actually produces an account.
To make that possible, `try_consume_invitation_link_by_uuid` -- which returned
a bool and so hid the inviter from `open_account` -- splits into a read-only
`resolve_usable_invitation_link` and a separate `consume`. That split also
fixes the ordering: the old code decremented the quota before checking the
email, handle and verification code, so every rejected signup cost the inviter
a slot. Nothing is spent now until `crud.user.create` has succeeded.
Two further corrections inside `pay_reward_for_invitation`:
* Return early when the inviter is the superuser, who funds the reward, so
the shared daily link cannot book superuser-to-superuser transfers.
* Count every invitation that brought someone in, rewarded or not, checking
the cap against the pre-increment value -- otherwise a spell of the admin
being broke would silently hand people extra rewarded slots.
Redemption also checks expiry now, which the old consume path did not: it
tested only the quota, so an expired link still opened accounts. Both that
path and the serialized `valid` field read one `is_usable_invitation_link` in
`model_utils.py`, so what a caller is shown is what redemption enforces.
INITIAL_USER_COINS goes from 0 to 50, resolving the TODO in `rules.py`: a new
account could not afford any coin cost, so no amount of invite reward reached
the person actually being invited. The link quota and lifetime move out of
`crud_invitation_link.py` into `rules.py` alongside the other numbers.
The `/invitation-links/daily` endpoint is deliberately untouched, and is still
unauthenticated -- the karma gate does not bind while anyone can fetch a
100-use link from it. Turning it off is a separate change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7WGABXVRz5MYheF6vT3js
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Two things were quietly not true about inviting users.
Nobody was gated.
POST /invitation-links/only required a valid JWT, andcreate_invitation_linkcarried a# TODO we didn't check if this user is allowed to invite new users— so any logged-in account could mint unlimited links, each good for 100 signups.The invite reward was dead code.
pay_reward_for_invitationhad no caller.INVITE_NEW_USER_REWARDandINVITE_NEW_USER_MAX_REWARDEDread as promises the site makes and enforced nothing, andsent_new_user_invitataionswas never written to. The rules-applied ratchet stayed green the whole time, because it looks for readers of a rule, not reachable callers — worth knowing about that check's blind spot.What changed
Karma gate. Link creation now requires
MIN_KARMA_INVITE_USER = 100, the same shape as the upload and site-creation gates. The superuser is exempt, followingservices/sites.py— otherwise the admin (karma 0) could not mint links at all, which matters more once/dailyis gone.Reward at redemption.
try_consume_invitation_link_by_uuidreturned abool, which hid the inviter fromopen_account. It splits into a read-onlyresolve_usable_invitation_linkand a separateconsume.open_accountvalidates the link up front and, only oncecrud.user.createhas succeeded, spends the quota and pays the inviter.That split also fixes an ordering bug: the old code decremented the quota before checking the email, handle and verification code, so every rejected signup cost the inviter a slot.
Two corrections inside
pay_reward_for_invitationbeyond wiring it up:Expiry is now enforced at redemption. The old consume path checked only the quota, so an expired link still opened accounts. Both that path and the serialized
validfield now read oneis_usable_invitation_linkinmodel_utils.py, next tois_live_answer/is_live_article— what a caller is shown is what redemption enforces.INITIAL_USER_COINS0 → 50, resolving the TODO inrules.py. A new account could not afford any coin cost, so no amount of invite reward reached the person actually being invited. Link quota and lifetime also move out ofcrud_invitation_link.pyintorules.pyalongside the other numbers.Deliberately not in this PR
GET /invitation-links/dailyis untouched and still unauthenticated. The karma gate does not really bind while anyone can fetch a 100-use link from it; turning that endpoint off is a separate change. Notetests/app/api/api_v1/test_users.py:42sources its signup link from/daily, so retiring it means touching that test.sent_new_user_invitataionskeeps its typo — it is now actually written to, so this is the cheapest moment to rename it, but that needs a migration.The
Invitationmodel andcrud/crud_invitation.pyremain dead (referenced only by tests).Testing
tests/app/api/,tests/app/crud/and the unit tests.tests/app/api/api_v1/test_invitation_links.pycovers: karma gate below/at threshold, rejected signup leaves quota intact, successful signup consumes one and pays the inviter (invitee starts with 50 coins), daily link pays nobody.🤖 Generated with Claude Code
https://claude.ai/code/session_01H7WGABXVRz5MYheF6vT3js