feat(sites): drive main site presentation from its db document - #159
Merged
Merged
Conversation
Declares which categories of main-site configuration are read from the site document on the publicUrl host rather than from env vars. Defaults to an empty list, so no existing install changes behaviour. The four category names are constrained by an enum so a typo refuses to start. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nA8Pb1y3ytneydAwDTU23
A site document whose host+path matches publicUrl is the "main site document". getMainSitePresentation merges its presentation fields over config.* according to mainSiteFromDb; reqSite() is untouched and still returns undefined on that host, so identity and trust rules are unchanged by construction. The public-info builder moves next to the env baseline constants so the two cannot drift, and the constants are kept because ui/vite.config.ts imports them for the dev server and must not reach into mongo. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nA8Pb1y3ytneydAwDTU23
The /api/sites/_* presentation endpoints take their values from the main site resolver instead of the env-only constants. The constants stay exported: app.ts and ui/vite.config.ts still import them, and the resolver returns them as its env baseline. Also fixes reducedPersonalInfoAtCreation missing from getPublicSiteInfo, which made the field unreachable from the login page on every site even though site-public declares it and login.vue reads it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nA8Pb1y3ytneydAwDTU23
…rved getSiteExtraParams called getSiteByUrl directly, with no publicUrl exclusion, while the /api/sites/_* endpoints call reqSite. On a host that is both the publicUrl host and carries a site document, the served HTML asked for /api/sites/<document-hash>/_theme.css and got the env CSS back under max-age=31536000, immutable: the hash did not describe the bytes served under it, so an env theme change never busted the cache and a change to the ignored document busted it for nothing. The logic moves to sites/spa-params.ts where it can be tested directly — in dev, /login is served by vite rather than by this middleware, so the HTTP path cannot exercise it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nA8Pb1y3ytneydAwDTU23
The mail path resolved its site with getSiteByHost, which has no publicUrl exclusion, so a document on the main host already drove the sender and contact address while every other consumer read env. It now goes through the main site resolver, and the theme and sender become independent: the document's theme used to apply only when mails.from was also set. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nA8Pb1y3ytneydAwDTU23
GET /api/sites/:id and the showAll list gain mainSiteWarnings: one entry per never-honoured field the document carries, one per category stored but absent from MAIN_SITE_FROM_DB. A boot check logs the same report and raises an internalError when the document carries an inert field. Nothing is refused: the admin form round-trips the whole document, so a write barrier would reject an idempotent save, and the only non-UI writer (portals) cannot send those fields anyway. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nA8Pb1y3ytneydAwDTU23
PATCH runs toggleMainSite whenever isAccountMain is truthy, rewriting every other site of the owner to onlyOtherSite. The admin form sends the whole document back on each save, so on a main document carrying isAccountMain this re-fired on every save. The side effect is skipped on the main document; the request itself still succeeds, so no caller breaks. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nA8Pb1y3ytneydAwDTU23
The sites list badges the main site document and surfaces mainSiteWarnings alongside the colour warnings. Its edit page gains a banner explaining what the document does and does not drive, and lists the warnings. The auth sections stay visible: the form round-trips them, so hiding would conceal the very values the warnings refer to. Only isAccountMain is withheld, being the one field whose write reaches beyond the document. Also strips the computed mainSiteWarnings from the patch body: the patch schema is additionalProperties:false, so leaving it in would have made every save of the main document fail with a 400. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nA8Pb1y3ytneydAwDTU23
Locators tightened after a first run against a cold vite compile: the section assertion gets the same generous timeout as the banner, and isAccountMain is matched by label rather than by text because the deprecated authMode field quotes "Site principal du compte" in its own label. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nA8Pb1y3ytneydAwDTU23
Records the three resolution paths that disagreed on the publicUrl host, the immutable-cache bug that followed, the MAIN_SITE_FROM_DB category list, and why a write barrier was rejected in favour of reporting. The dev fixtures gain a main site document so the path is exercised locally; MAIN_SITE_FROM_DB is empty by default, so it stays inert until set in .env. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nA8Pb1y3ytneydAwDTU23
… spec initMongo() hands out a process-wide client that every unit spec shares; this spec was the only one calling closeMongo() in afterAll. Nothing noticed until master added orphan-avatars.unit.spec.ts, which sorts right after main-site and calls initMongo() again: the client was already closed, reconnecting it threw MongoNotConnectedError, and the rest of the unit project (13 tests) never ran. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nA8Pb1y3ytneydAwDTU23
… designs Five hunks outlived the designs they were written for during this branch and are unreferenced in the final state: - the VJSF context key mainSiteFromDb, left from a per-section-notes UI that the banner replaced. Nothing read it, which also made the uiConfig entry feeding it dead, so both go. - escapeHtml, module-private in app.ts before it moved to spa-params.ts - lighterTheme, exported for main-site.ts before buildMainPublicSiteInfo moved next to it - isMainSiteUrl and getMainSiteDoc in the #services barrel, which every consumer reaches through ./sites/service.ts instead - the MainSitePresentation pass-through re-export, plus three constants used only inside main-site.ts No behaviour change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nA8Pb1y3ytneydAwDTU23
main-site.ts had grown a parallel implementation of what the ordinary site path already does: buildMainPublicSiteInfo alongside getPublicSiteInfo, and a second hash cache with its own key scheme alongside getPublicSiteInfoHash / getThemeCssHash. That forced every consumer to branch, leaving 11 "ordinary : main" ternaries across router.ts and spa-params.ts. The merge now returns an EffectiveSite — Site with an optional owner and a main flag — and every consumer renders it with the same functions as a real site: const site = await reqSite(req) ?? await getEffectiveMainSite() res.send(getPublicSiteInfo(site)) main-site.ts drops to the merge plus the warnings; the ternaries are gone. Logo precedence is resolved inside the merge so the shared renderer stays dumb, and the contributing categories are folded into the synthetic _id so the shared hash caches still key correctly. Ordinary sites are unaffected: their public info is byte identical, hash included, verified against the previous implementation. The main site's own payload gains the keys the shared shape always emitted, so defaultPublicSiteInfoHash moves once — a single cache miss on upgrade. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nA8Pb1y3ytneydAwDTU23
DELETE /api/test-env cleaned the auth and contact rate limiters but not the mail one, whose window is the only one that outlives a run: the dev server uses NODE_ENV=development, so the limiter takes the production default of 500 mails per recipient per day rather than the small test.cjs window. Shared fixture addresses burn a few points per run, so after enough runs admin@test.com hit 509/500 and stayed blocked for the rest of the day. The symptom lands far from the cause — 2fa.api.spec.ts failing on `waitForMail timeout` while the mail was in fact dropped by the limiter, with the reason only visible in the server log. mails-rate-limit.api.spec.ts is unaffected: it pre-fills its own bucket inside each test, with a unique recipient, after this cleanup runs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nA8Pb1y3ytneydAwDTU23
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.
Adds the notion of a main site document — a site whose
host+pathmatchespublicUrl— and lets it drive the main site's presentation through a new opt-inMAIN_SITE_FROM_DBcategory list (theme,title,mails,registration), falling back to env config per category.Why: an install whose
publicUrlhost also carried a site document had two writable sources of config for the same host, and three resolution paths disagreeing about which won — so a portal on the main domain silently lost its theme, while its document still drove the served<title>and the transactional mail sender.reqSite()is deliberately unchanged and still returnsundefinedon that host, soisAdmin = !user.host,getUserByEmailscoping andadminModegating are untouched.authMode,authProviders,applications,isAccountMainandownerare never read from the document; they are reported as ignored (mainSiteWarningson the sites API, a boot check, an admin banner) rather than refused, because the admin form round-trips the whole document and a write barrier would reject an idempotent save.The merge returns an ordinary
EffectiveSite, so the main host renders through the samegetPublicSiteInfo/getThemeCss/ hash functions as any other site — there is no second rendering path.Defaults to
[], so no existing install changes behaviour on upgrade. Seedocs/architecture/main-site-config.md.Also fixes three pre-existing bugs surfaced by the same dual-source problem:
THEME_CSS_HASHtaken from the document while/api/sites/:hash/_theme.cssserved the env CSS — the hash did not describe the bytes served under it, cachedimmutablefor a yearreducedPersonalInfoAtCreationwas missing fromgetPublicSiteInfo, so the setting never reached the login page on any sitetoggleMainSitere-fired on every full-document save of the main document, rewriting the owner's other sitesHeads-up: the
reducedPersonalInfoAtCreationfix is not scoped to the main site — any existing secondary site storingtruewill start hiding the name fields on its signup form. Enabling thethemecategory also propagates the theme to every other data-fair service on that host, since they all read/api/sites/_hashes.defaultPublicSiteInfoHashchanges once, costing a single_public.jscache miss per install; ordinary sites' payloads and hashes are byte identical.