Tiered access: guests browse, accounts upload (1/day), verified researchers get 10/day - #212
Merged
Merged
Conversation
…ta plane The four upload endpoints (upload-inference-chunk, finalize-upload, upload-dicom-slice, finalize-dicom) accepted anonymous POSTs with no rate limit - a guest with curl could write 2 GB files into the sessions tree. The AI assistant's gate returned None unconditionally, so signing out was an unlimited tier and plan_store.check_assistant was dead code. The only real gate on the whole data plane was /run-inference. - @require_auth on the four upload endpoints. - _ai_gate delegates to plan_store.check_assistant: signed out gets a 401, a spent allowance a 402 with the same body shape /run-inference uses. The sidebar already handles both statuses; no frontend change needed. - The upload fetches never sent the session cookie; add credentials: "include" to all six call sites so signed-in uploads keep working once the server refuses anonymous ones. - The pre-upload effect gets an explicit isAuthenticated check instead of relying on every setSelectedItems writer sitting behind ensureAccount. New tests/functional/test_guest_gating.py pins the boundary (401s for guests, chunk upload still works signed in, 402 at the assistant allowance); ci.yml runs it and gains scikit-image/pandas/trimesh so api_blueprint imports in the backend job.
Free drops from 3 daily scans to 1 - the simple-signup tier is a taste, not a workload - and Pro drops from the invented 50 to the 10/day the verified-researcher tier grants (Team follows Pro per member). Other limits are untouched. The daily-limit refusal and the upgrade dialog learn singular grammar so a 1-scan plan never renders '1 scans a day'. Card copy follows: Free says '1 scan a day', Pro says '10 scans a day'. Backend and frontend limit mirrors, their tests, and the plan-gating fixtures all move to the new numbers.
Three nullable free-text columns on user_account - organization, occupation, role_description - with an additive migration (7d4f8c2a9b1e). PATCH /auth/me accepts any subset (empty string clears back to NULL, wrong type is a 400), the public dict and the account export carry them, and Settings -> Profile gains a 'Verified researcher profile' group with three commit-on-blur fields and the copy explaining what a complete profile unlocks. Nothing reads the fields yet; the tier check that will arrives with the promotion commit. Functional tests cover trim/clear/type-refusal and the widened export shape; frontend tests cover the PATCH field names and the unlock copy.
Password signups were permanently unverified - email_verified_at existed but nothing ever set it outside OAuth. Add the flow, cloned from the password-reset pattern: an email_verification_token table (hashed token, single-use, superseded by a newer request, 24 h window - a stale link can only finish its own job, unlike a reset), POST /auth/send-verification (signed-in, rate-limited, reports honestly whether mail left) and POST /auth/verify-email (public - the link may open in a browser that has never seen the site). Registration sends the first link best-effort; an unconfigured mailer prints it to the server log, which is the dev flow. Frontend: the account carries emailVerified; Settings -> Profile shows the state with a Resend button; /verify-email redeems the link on load and explains an expired one. Boot purges spent tokens alongside reset tokens; the new unit file runs in CI's auth step. Unit tests cover issue/redeem/single-use/expiry/supersede; functional tests the endpoint gates; frontend tests the resend row and the landing page's three states.
limits_for_user computes the tier instead of anything writing the plan column: a free account with email_verified_at set and all three profile fields filled in resolves to pro (10 scans/day), reported as its plan by /me/usage and every 402 body, so the usage page and the upgrade dialog name it consistently. No trigger to miss, no backfill, and clearing the profile drops the account back on its own. The stored column stays 'free' and self-service plan writes stay closed. The Pro card now tells that story - 'For verified researchers', price 'Free', 'verify email + complete profile' - and its button reads 'Verify to unlock' for non-admins instead of 'Coming soon'. The settings hero names the effective plan from /me/usage. gatingPlan mirrors the promotion for the upload page's advisory checks. Unit tests pin promotion, each missing ingredient, and the untouched column; a functional test walks the whole journey (register -> profile -> verify -> usage says pro, /me/plan still 403).
The deploy manual is what the person running bdmap1 actually follows, so the mail section now covers verification email (which gates the 10/day tier), points at the SES values used in production, and states the two properties a deployer needs to know: the credential is handed over privately and never lives in this repository, and leaving SMTP unset degrades gracefully - links go to the server log, password signups stay on 1/day, OAuth accounts are unaffected.
andyy-yang
force-pushed
the
andy/guest-hardening
branch
from
September 1, 2026 02:10
5baa253 to
b36b763
Compare
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.
TLDR
The tiered-access system, built and verified end to end locally before pushing: guests browse (uploads and the assistant now require an account — enforced server-side, where previously the entire data plane was open to anonymous callers), signed-up accounts get 1 scan/day, and verified researchers — verified email + completed profile — are automatically promoted to Pro at 10 scans/day. Six commits, reviewable one by one.
Major decisions
limits_for_userresolves free → pro whenemail_verified_at+ all three profile fields are present. No trigger to miss, no backfill, self-reversing if the profile is cleared; the stored plan column staysfreeand/me/planstill refuses self-service changesWhat changed (per commit)
@require_authonupload-inference-chunk/finalize-upload/upload-dicom-slice/finalize-dicom;_ai_gatedelegates toplan_store.check_assistant(401 signed out / 402 spent allowance, same body shape as/run-inference); upload fetches now send the session cookie; explicit auth check in the pre-upload effect; newtest_guest_gating.pyrun by CI (backend job gains scikit-image/pandas/trimesh so the blueprint imports)organization/occupation/role_descriptiononuser_account(additive migration7d4f8c2a9b1e); PATCH/auth/meaccepts them; Settings → Profile gains a "Verified researcher profile" group with commit-on-blur fieldsemail_verification_tokentable (migration9e6a1b5d4c2f);POST /auth/send-verification(signed-in, rate-limited, reports honestly whether mail left) andPOST /auth/verify-email(public — the link may open anywhere); registration sends the first link best-effort;/verify-emailpage redeems on load, built on the reset card's structure; Settings shows Verified/Resendlimits_for_userpredicate;/me/usageand 402 bodies report the effective plan; Pro card reads "For verified researchers / Free / verify email + complete profile", button "Verify to unlock"; plan page names the effective planTesting
jy168@duke.edu); the emailed link verified the account through the deployed-style flow. No simulated results.free,/me/plan403 → signed-in chunk upload 200 → resend reports already-verified.compileallclean; frontend — typecheck 0 errors, 47 files / 314 tests, build OK. Merge-tree clean against currentmain.Verified correct (no change needed):
/run-inference's existing auth+plan gate; dataset browse/view endpoints stay public; admin unlimited resolution; OAuth linking; Live Rooms.Deferred (deliberate): ownership checks on session-artifact reads (needs a user id on job records — follow-up); per-IP rate limits on uploads; account-type label cleanup.
Deployer notes
alembic upgrade headis mandatory (two revisions). The SES SMTP block is optional and arrives privately — see the README's step 4b. Without it the site runs and verification links go to the gunicorn log.Reviewer checklist
curl -X POST /api/upload-inference-chunk→ 401; assistant asks to sign in/me/planstill 403 for non-admins; admin flows unchanged