Owner section in Settings, and the email confirmation landing - #47
Open
ClaydeCode wants to merge 9 commits into
Open
Owner section in Settings, and the email confirmation landing#47ClaydeCode wants to merge 9 commits into
ClaydeCode wants to merge 9 commits into
Conversation
The jest preset already compiles .vue files but the mounting library was missing, so components could not be tested at all. The lockfile's version field also catches up with package.json, which just set-version rewrites.
Reading the confirmation token, naming the three address states and telling a 502 that stored the candidate apart from a real save failure are the parts of the coming Owner section that are easy to get subtly wrong, so they live where a test can reach them directly.
Both fields now read and write the owner's user row, which is where the address lives since freeshard#220. The email does not write through: it becomes a candidate the owner confirms from a mailed link, so the card also carries the pending state, resend and cancel. A 502 from PATCH /protected/users/me stores the candidate and only fails to send the mail, so it is reported as such rather than as a failed save. On a shard that cannot send mail at all the address is taken directly, and no resend control is offered. The About list drops Owner and Owner Email: they were the controller's copy of the same two values under the same labels, which is the duplication this card removes.
The controller mails the link as https://<shard-domain>/?confirm_email=<token>, so it arrives here and the last step is the UI's. The screen confirms only on a click, because a request the browser makes on its own would let a mail scanner spend the single-use token — the same reason there is no GET route. It is reached before the pairing check: the link is typically opened in whatever reads mail, often a browser this shard has never seen, which the redirect to /welcome would otherwise swallow. The token sits in the document query string rather than in the hash route, so it is read from window.location and dropped from the URL once it has been spent.
The two traps a future agent walks into are the 502 that stored the address anyway and the confirmation token sitting outside the hash route, so both are written down next to the auth flow they belong to.
…where useful The Settings refresh button reloaded only the controller profile, so a confirmation opened on a phone never showed up on the desktop that was waiting for it. After confirming, an unpaired browser is sent to the public page instead of the app grid, which needs a pairing it does not have.
Cancelling re-read the owner row afterwards, so a failed read reported the cancellation itself as failed when it had already gone through. The 204 says exactly what changed, so the card applies that instead.
- Tell the API's 502 from the proxy's. Traefik answers 502 whenever shard_core is down, where nothing was stored; only the API's own 502 carries a JSON detail, and only it means the candidate is on file. - Stop the pending hint from claiming a confirmation mail went out. After a reload the card cannot know whether one did, and today it usually did not. - Stop promising mail on a shard that cannot send any: the no-address prompt is the first thing a self-hosted owner reads. - Keep the name editable when only the settings read fails. - Drop the confirmation token from the URL when the screen opens rather than when it succeeds, so a failed or ignored link stops riding in the address bar, the history entry and any Referer. Log its status, not the request. - Offer a way off the confirmation screen in every state; without it a stale token parked the browser there with no in-app way back. - Await the owner reload behind the Settings refresh overlay. Tests: the 502 recovery, the resend that clears the warning, the reload on the confirm screen and the third address state were all passing against mutations of the code under them.
- OwnerSection becomes OwnerCard: it renders a b-card, and every sibling that does is named for one. - EditableText gets a hint slot, so callers stop rebuilding its grid from the outside to line text up under a field. - errorDetail moves to lib/http-error.js. It formats any axios failure and had nothing to do with the owner's address. - saveEmail hands its 502 branch to showStoredCandidate, which drops a try/catch nested inside a catch. - load() clears the delivery warning: after a resend from another device, a refresh was leaving a false "could not be sent" on screen. - The confirmation screen leads with what it can honestly say instead of saying "Done." and taking it back in the next clause, and all four places that talk about resending now call the button by its name. - agents.md: the 429 that stores nothing, and two counts the diff made stale. Tests read through the DOM the owner uses — pencil, type, submit — rather than EditableText's internals, and the route table is no longer reached through the shared router instance.
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.
Closes #44 (#44).
The owner's email had two homes in this UI showing two different values. FreeshardBase/freeshard#220 settled where it lives — the owner's user row,
users.email, verified by definition, withusers.pending_emailas the unverified candidate — and freeshard#221 shipped the API. This is the terminal side of that: one Owner card in Settings that owns both fields, and the screen the confirmation link lands on.The first half of the issue (removing the published address from the Public page and
Welcome.vue) already shipped as #45 / #46 and is not in here.What is in it
An Owner card at the top of Settings, bound to the owner's user row — not to the controller's
profile, not toidentity.PATCH /protected/users/memakes it a candidate, and the live value only changes when the owner opens the mailed link. The card renders the three states the model allows (no address, pending, set) plus the self-hosted variant whereGET /protected/settingsreportsemail_enabled: false, the address is taken directly, and no resend control is offered.OwnerandOwner Email. They were the controller's copy of the same two values under the same labels — the duplication the issue opens with.A confirmation screen at
/?confirm_email=<token>.window.location.search.$route.querynever sees it.One correction to the issue
§4 says
POST /public/users/confirm-emailanswers "204 on success, 400 otherwise". The merged endpoint answers 204 either way — anything else would let a caller probe for pending addresses — so the screen cannot report success or failure, only that the request went through. Its copy is hedged accordingly ("If the link was still valid…"), with the expiry and the resend named so an owner whose link had died knows what to do. Two reviewers checked this againstshard_coreonmainindependently. Worth a look, since it is a real difference from what the issue specifies.Recommended reading order
src/lib/owner-email.js,src/lib/http-error.js— the token, the three states, the 502 discriminatorsrc/components/OwnerCard.vue— the cardsrc/views/ConfirmEmail.vue,src/App.vue,src/router/index.js— the landingsrc/views/Settings.vue,src/components/EditableText.vue— wiring and the new hint slottests/unit/*.spec.js,agents.mdVerification
npm run lint,npm run test:unit(8 suites, 71 tests) andnpm run buildall green. Every new test was run against a deliberate mutation of the code beneath it; the ones that did not fail were rewritten until they did.@vue/test-utilsis added as a devDependency: the jest preset already compiled.vuefiles, but the mounting library was missing, so no component in this repo could be tested at all.Review panel
Four adversarial reviewers ran against the diff and the issue, each without my reasoning: general correctness, test adversary, DevEx/readability, security. No BLOCKING findings from correctness, DevEx or security. The test adversary raised four, all proved by mutation and all fixed.
Blocking — test adversary (all fixed in 72c49d6)
does not confirm on its ownasserted synchronously, so an auto-confirm one tick after mount passed$nextTick(() => this.confirm()).deliveryFailed = falsein the resend path was entirely unpinned — a successful resend could leave "could not be sent" up forever$route.name !== 'ConfirmEmail'guard inApp.vuewas untested; without it a reload on the confirm screen rejects withNavigationDuplicatedoutsidebeforeMount's try and strands the loading splashAdvisory, fixed
isDeliveryFailurekeyed on status 502 alone. Traefik answers 502 too whenever shard_core is down — where nothing was stored — and the card would then have claimed the address was saved and invented a pending one. It now requires the JSONdetailonly the API sends. (72c49d6)Promise.allmeant an unrelated/protected/settingsfailure blanked the whole card, including the name field. Now independent. (72c49d6)Referer. Stripped when the screen opens instead. (72c49d6)console.error(e)on the axios error expandede.config.data— the token — into devtools. Logs the status now. (72c49d6)OwnerSection→OwnerCard;errorDetailmoved out of the owner-email module;saveEmail's nested try/catch lifted intoshowStoredCandidate;EditableTextgained a hint slot so callers stop rebuilding its grid;load()clears a stale delivery warning; confirmation copy no longer says "Done." and retracts it; one name for the resend action everywhere;agents.mdcounts and the 429 sibling case. (cbbc2fa)Advisory, not taken
users.display_name, the Public page editsidentities.name). Both stay, since the issue names the field "Name" and they are genuinely different values; the card now carries a line saying so and pointing at Public View.Settings.vuehas no test file, so the removal of the two About fields and the refresh wiring are unverified there. Standing up a harness for that view means registering the global filters, a router and the PayPal SDK — a bigger change than this issue warrants. The$refcall it flagged is now guarded and awaited.🤖 Generated with Claude Code