Repository navigation
STU-387: Implement account access actions without fake deletion or allowance resets #93
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
8bfe0c0
4a42da6
bc1c505
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -50,6 +50,12 @@ import { | |
| PLUS_ALIAS_REJECTED_CODE, | ||
| trimEmail, | ||
| } from "./src/authMagicLink.js"; | ||
| import { | ||
| clearDisposableUiCaches, | ||
| createSendLinkController, | ||
| isDeleteAccountSupported, | ||
| performSignOut, | ||
| } from "./src/accountAccess.js"; | ||
| import { planCustomCodeVerification } from "./src/customCodeVerification.js"; | ||
| import { | ||
| deployOutcomeOfStreamResult, | ||
|
|
@@ -4507,10 +4513,88 @@ async function handleMagicLinkRequest() { | |
| } | ||
|
|
||
| function handleSignOut() { | ||
| clearSession() | ||
| clearSubscriptionCache() | ||
| updateAuthUI() | ||
| updateSubscriptionUI() | ||
| // Reset in-memory auth/subscription state first so the immediate re-render | ||
| // below reflects a signed-out identity/plan. | ||
| authState.email = null | ||
| authState.sessionToken = null | ||
| authState.isVerified = false | ||
| subscriptionState = createSubscriptionState({ isResolved: true }) | ||
| // Remove the session + subscription cache keys (scoped adapter) and then | ||
| // synchronously re-render every identity/plan surface. | ||
| performSignOut({ | ||
| storage: window.localStorage, | ||
| onIdentityChanged: updateAuthUI, | ||
| onPlanChanged: updateSubscriptionUI, | ||
| }) | ||
|
Comment on lines
+4524
to
+4528
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If a subscription request is already pending when the user signs out, its response still restores Greptile automatically discovered a related ticket stating that sign-out must clear the session and immediately update all identity and plan UI, which informed this comment. Source Used: Linear — Implement account access actions without fake deletion or allowance resets Prompt To Fix With AIThis is a comment left during a code review.
Path: app.js
Line: 4524-4528
Comment:
**Sign-out state can return**
If a subscription request is already pending when the user signs out, its response still restores `subscriptionState` and rewrites `ccc_subscription`. The generation path can then render and use the former paid tier while the user is signed out. Sign-out needs to invalidate or ignore responses that belong to the ended session.
Greptile automatically discovered a related ticket stating that sign-out must clear the session and immediately update all identity and plan UI, which informed this comment.
**Source Used:** Linear — [Implement account access actions without fake deletion or allowance resets](https://linear.app/stuart-gardoll/issue/STU-387/implement-account-access-actions-without-fake-deletion-or-allowance)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly. |
||
| } | ||
|
|
||
| // --- ACCOUNT ACCESS & DATA WIRING (STU-387) --- | ||
| // One "Send new link" controller guards against duplicate requests and reflects | ||
| // the actual response or failure into the row's status line. | ||
| const accessSendNewLink = createSendLinkController({ | ||
| sendLink: sendMagicLink, | ||
| getEmail: () => authState.email || "", | ||
| onState: renderSendNewLinkState, | ||
| }) | ||
|
|
||
| function renderSendNewLinkState(state) { | ||
| const btn = document.getElementById("send-new-link-btn") | ||
| const msg = document.getElementById("send-new-link-msg") | ||
| if (!btn || !msg) return | ||
| if (state.status === "pending") { | ||
| btn.disabled = true | ||
| btn.textContent = "Sending…" | ||
| msg.className = "acct-access-msg" | ||
| msg.textContent = "Sending a fresh sign-in link…" | ||
| return | ||
| } | ||
| if (state.status === "sent") { | ||
| btn.disabled = false | ||
| btn.textContent = "Send new link" | ||
| msg.className = "acct-access-msg" | ||
| msg.textContent = getMagicLinkResultMessage(state.data, authState.email || "") || "Link sent — check your email." | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The request sends to the email captured when it starts, but this line formats the success message using the current Greptile automatically discovered a related ticket stating that reauthentication must reflect the actual response and prevent duplicate requests, which informed this comment. Source Used: Linear — Implement account access actions without fake deletion or allowance resets Prompt To Fix With AIThis is a comment left during a code review.
Path: app.js
Line: 4555
Comment:
**Link result uses wrong account**
The request sends to the email captured when it starts, but this line formats the success message using the current `authState.email`. If the user signs out or changes accounts before the request finishes, the status claims the link went to a blank or different address, and that stale message can reappear in the next signed-in session. Preserve the request-time email in the result and clear this status when the session changes.
Greptile automatically discovered a related ticket stating that reauthentication must reflect the actual response and prevent duplicate requests, which informed this comment.
**Source Used:** Linear — [Implement account access actions without fake deletion or allowance resets](https://linear.app/stuart-gardoll/issue/STU-387/implement-account-access-actions-without-fake-deletion-or-allowance)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly. |
||
| return | ||
| } | ||
| // status === "error": surface the real failure, never a canned success. The | ||
| // propagated Error carries a user-safe reason (e.g. "HTTP 429", "HTTP 500") | ||
| // rather than raw server internals, and a rate-limit must not read identically | ||
| // to a server error. Fall back to the generic copy only when no reason escaped. | ||
| btn.disabled = false | ||
| btn.textContent = "Send new link" | ||
| msg.className = "acct-access-msg error" | ||
| const reason = state.error?.message | ||
| ? `Couldn't send a link right now. ${state.error.message}` | ||
| : "Couldn't send a link right now. Please try again." | ||
| msg.textContent = reason | ||
| } | ||
|
|
||
| async function handleSendNewLink() { | ||
| await accessSendNewLink() | ||
| } | ||
|
|
||
| function handleClearCache() { | ||
| // Scoped to the named disposable UI caches only — never usage, identity, | ||
| // credentials, or the active session, so it cannot hand out new runs. | ||
| const removed = clearDisposableUiCaches(window.localStorage) | ||
| const detail = removed.length | ||
| ? `Cleared disposable UI caches: ${removed.join(", ")}.` | ||
| : "Nothing disposable to clear." | ||
| showToast(`${detail} Your run count, identity and stored keys are kept.`, "info") | ||
| } | ||
|
|
||
| function handleDeleteAccount() { | ||
| // Truthful: no backend deletion contract exists in this slice, so this control | ||
| // only explains that deletion is unavailable. It performs no deletion and | ||
| // never resets usage, identity, credentials, or the session. | ||
| if (isDeleteAccountSupported()) return | ||
| const modal = document.getElementById("delete-unavailable-modal") | ||
| if (modal) openModal(modal, { trigger: document.activeElement }) | ||
| } | ||
|
|
||
| function closeDeleteUnavailableModal(event) { | ||
| if (event && event.target !== event.currentTarget) return | ||
| const modal = document.getElementById("delete-unavailable-modal") | ||
| if (modal) closeModal(modal) | ||
| } | ||
|
|
||
| function updateAuthUI() { | ||
|
|
@@ -4523,6 +4607,8 @@ function updateAuthUI() { | |
| if (guestUsage) guestUsage.classList.toggle('hidden', signedIn) | ||
| const emailEl = document.getElementById('auth-user-email') | ||
| if (emailEl) emailEl.textContent = authState.email || '' | ||
| const accessEmailEl = document.getElementById('access-signed-email') | ||
| if (accessEmailEl) accessEmailEl.textContent = authState.email || '' | ||
| updateGuestUsageCounter() | ||
| updateSubscriptionUI() | ||
| } | ||
|
|
@@ -6385,6 +6471,10 @@ window.openSignInModal = openSignInModal; | |
| window.closeSignInModal = closeSignInModal; | ||
| window.handleMagicLinkRequest = handleMagicLinkRequest; | ||
| window.handleSignOut = handleSignOut; | ||
| window.handleSendNewLink = handleSendNewLink; | ||
| window.handleClearCache = handleClearCache; | ||
| window.handleDeleteAccount = handleDeleteAccount; | ||
| window.closeDeleteUnavailableModal = closeDeleteUnavailableModal; | ||
| window.startCheckout = startCheckout; | ||
| window.openCustomerPortal = openCustomerPortal; | ||
| window.openPricingModal = openPricingModal; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔴 Late response restores signed-out plan
When
handleSignOutruns duringfetchSubscription, the pending response restores the old plan. The signed-out browser then exposes its allowance and model access.Learn more
fetchSubscriptionapplies its response and writes the subscription cache without checking that the session which started the request is still current.handleSignOutclears the current state, but it neither aborts nor invalidates that request. When the response arrives,subscriptionStatebecomes the former account's tier; updateSubscriptionUI then feeds that tier into allowance and model gating even though the user is signed out.Example: A Pro user opens the page and the subscription request stalls. They sign out before it returns. The Pro response then lands, and the guest session receives the Pro run limit and model choices instead of Free access.
Recommended fix: Add an auth-session generation or capture the initiating token/email in
fetchSubscription. Before applying any response, error, or cache write, verify that the captured session still matchesauthState; increment the generation on sign-out and session changes. AnAbortControllercan cancel pending work, but retain the identity check because cancellation can race with response completion.Was this helpful? React with 👍 or 👎 to provide feedback.