fix(auth): evig owns its own admin + docs(legal): Verein founding documents - #270
Merged
Merged
Conversation
evig separated from Revamp-IT on 2026-07-24, but three Revamp-IT accounts (andreas@, veronica@, daniel@revamp-it.ch) were still hardcoded in SUPER_ADMIN_EMAILS. isSuperAdmin() resolves that list by e-mail alone, bypassing the database entirely, so each of them held full access to evig's sensitive sections (users, finances, settings) — and the demotion guard in PATCH /api/admin/users/[id]/permissions explicitly refuses to demote anyone on the list, making them un-removable through the UI. - SUPER_ADMIN_EMAILS reduced to the owner only. It is a bootstrap FLOOR so evig can't lock itself out; users.is_super_admin stays the SSOT and is how every other super admin is granted. - Super-admin is now checked BEFORE the is_staff gate in canAccessSection/canAccessSensitive/getAccessibleSections, so a cleared is_staff flag can't lock the owner out of their own admin. - STAFF_EMAIL_DOMAIN derives from ORG.emailDomain (evig.ch); the legacy Revamp-IT domains move to LEGACY_STAFF_EMAIL_DOMAINS so existing logins keep working until the Layer B infra cutover. - Docs corrected: staff has been DB-granted (never domain-derived) since the permissions-v2 hardening, but .claude/CLAUDE.md still documented "anyone with @revamp-it.ch email is staff", which was simply false. Regression guard added: the removed Revamp-IT addresses must return false from isSuperAdmin(). Verified: typecheck clean, lint 0 errors, 156 auth/permission/user tests pass. The 25 pre-existing failures in src/config/__tests__ reproduce identically on main and are untouched by this change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Draft Statuten + Gründungsprotokoll for founding evig as a gemeinnütziger Verein with seat in Zürich, plus the operational checklist. The statutes are drafted so that (a) the tax-exemption application to the Kantonales Steueramt Zürich can be filed on the first attempt, and (b) a later spin-out of the commercial arm into a GmbH needs NO amendment. Exemption-critical clauses (ESTV Kreisschreiben Nr. 12, § 61 lit. g StG): - Art. 2: idealistic Zweck, explicit "kein Erwerbszweck", economic activity admitted only as "Mittel zum Zweck und nie Selbstzweck", and directed at the Allgemeinheit rather than members (Uneigennützigkeit). - Art. 3: assets dedicated exclusively and irrevocably to the purpose. - Art. 17: Vermögensanfall to another tax-exempt Swiss legal person; any distribution to members excluded. Structural clauses: - Art. 4: the Verein may found/hold participations, held as Vermögensanlage and subordinate to the purpose, with NO Geschäftsführung by the Verein — which is both what KS 12 requires and what keeps the founder clear of the self-employment ban. This is what makes the GmbH split amendment-free. - Art. 10 Abs. 2: the board must have >= 3 members before any compensation or employment of a board member may be resolved, so founding stays possible with 2 while independence exists exactly when it matters. - Art. 12: unpaid by default, effective expenses against receipt only (Pauschalspesen excluded — they get requalified as salary), mandatory Ausstand on one's own compensation, disclosure in the annual report. - Art. 14: publication of statutes, board, annual report and accounts in a form that keeps changes traceable; and no Spendenbescheinigungen or claims of recognised gemeinnützigkeit before the Verfügung exists — matching ORG.legalForm = 'in Gründung' as the SSOT. Formal requirements per the Handelsregisteramt Zürich Merkblätter are noted inline (min. 2 founders, original handwritten signatures, election acceptance recordable in the minutes). Verified: lint:umlauts clean, no "ß" (Swiss German rule). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…all 8 locales The founding documents were repo-only. They are now a public page, in every language the site speaks, reachable from the Transparenz hub and the sitemap. Honesty first: evig is not yet founded, so STATUTEN_STATUS.state drives an unmissable draft banner. Publishing a draft as the governing statutes of an association that does not legally exist would be a false claim; publishing it AS a draft is the transparency the statutes themselves promise (Art. 14). SSOT split: - src/config/statuten.ts — structure: article order, numbers, section grouping - messages/<locale>.json — strings only, one coherent body per article - src/lib/legal/ — parses bodies into paragraph/list blocks - src/components/legal/ — renders them Paragraph numbers and list letters are GENERATED by the renderer, never read from a translated string, so all 8 locales show identical structure. That also sidesteps the repo's fragile-array i18n trap (arrays have no per-element fallback). German is declared authoritative via STATUTEN_AUTHORITATIVE_LOCALE and every translated locale carries a notice card saying so. Verified: lint, lint:umlauts, typecheck, test:i18n green; all 8 locales render 200 with 18 articles each; i18n audits show zero new leaks and zero new missing keys vs main. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Two related pieces of separating evig from Revamp-IT: taking sole ownership of the platform's admin, and the paperwork to make evig a legal entity.
1.
fix(auth)— remove Revamp-IT super adminsSUPER_ADMIN_EMAILSstill hardcoded andreas@, veronica@ and daniel@revamp-it.ch.isSuperAdmin()resolves that list by e-mail alone, bypassing the database, andsrc/auth.tsORs it into the session — so each held full access to evig'susers,finanzenandsettings. The permissions API also refuses to demote anyone on the list, so they were un-removable through the UI.SUPER_ADMIN_EMAILSreduced to the owner as a documented bootstrap floor;users.is_super_adminis the SSOT and is how every other super admin is granted — the "anyone can be admin once I approve them" model that permissions-v2 already implements.is_staffgate incanAccessSection/canAccessSensitive/getAccessibleSections, so a cleared flag can't lock the owner out.STAFF_EMAIL_DOMAINderives fromORG.emailDomain(evig.ch); legacy Revamp-IT domains move toLEGACY_STAFF_EMAIL_DOMAINSso existing logins keep working until the Layer B infra cutover. This path is cosmetic (welcome-email template) and grants nothing..claude/CLAUDE.mdstill claimed "Anyone with @revamp-it.ch email is staff", which has been false since the permissions-v2 hardening.falsefromisSuperAdmin().Production database — audited
Those three have no accounts on prod, so the e-mail bypass was the only exposure. Prod super admins are exactly one: the owner.
georgy.butaev@revamp-it.ch{*}e2e-admin@revamp-it.ch{*}shop@revamp-it.ch{}No database change was required. (
e2e-admin@revamp-it.chis the admin persona for the prod route-inventory gate —scripts/e2e-inventory-prod.sh, credentials inAUTH_TEST_ADMIN_EMAIL. It is load-bearing, not an orphan; the.testaddresses inci.ymlbelong to the local E2E job.)2.
docs(legal)— founding documentsdocs/legal/— draftSTATUTEN.md,GRUENDUNGSPROTOKOLL.md, and aREADME.mdchecklist for founding evig as a gemeinnütziger Verein in Zürich.Drafted so the tax-exemption application can be filed on the first attempt, and so spinning the commercial arm into a GmbH later needs no statute amendment:
kein Erwerbszweck, economic activity admitted only asMittel zum Zweck und nie Selbstzweck, directed at the Allgemeinheit rather than members (Uneigennützigkeit per KS 12).ORG.legalForm = 'in Gründung'as the SSOT.Formal requirements from the Handelsregisteramt Zürich Merkblätter are noted inline: minimum two founders, original handwritten signatures, election acceptance recordable in the minutes.
3. The founding documents are now public — in all 8 languages
The statutes existed only in
docs/legal/. They are now a real page at/transparenz/statuten, linked from the Transparenz hub and in the sitemap,rendered in de / en / es / fr / it / ja / ko / ru.
Published as a draft, deliberately.
STATUTEN_STATUS.state = 'draft'drives anunmissable banner. Presenting these as the governing statutes of an association that
does not legally exist yet would be a false claim; publishing them as a draft is the
transparency Art. 14 promises — a constitution should be readable before it is adopted,
not only after.
SSOT split, per the repo's i18n rule (structure in config, strings in messages):
src/config/statuten.tsmessages/<locale>.jsonsrc/lib/legal/statuten-body.tssrc/components/legal/StatutenBody.tsxParagraph numbers and list letters are generated by the renderer, never read from a
translated string — so all 8 locales show byte-identical structure, and it sidesteps the
repo's fragile-array trap (locale arrays have no per-element fallback). German is declared
authoritative via
STATUTEN_AUTHORITATIVE_LOCALE; every translated locale carries a noticecard saying only the German wording binds.
Verified: all 8 locales return 200 with 18 articles each; a structural parity check confirms
identical top-level keys, sections, articles and paragraph/list shapes against German.
Verification
npm run typecheck— cleannpm run lint— 0 errors (48 pre-existing warnings)npm run lint:umlauts— clean; no eszett (Swiss German rule)src/config/__tests__reproduce identically on cleanmainand are untouched here.Verification of this third part
npm run lint— exit 0 ·npm run lint:umlauts— clean ·npm run typecheck— cleannpm run test:i18n— 3 suites, 16 tests pass (the structure gate CI enforces)i18n-leaks: 554 total, identical tomain— the translations add zero German leaksi18n-audit: missing-key counts identical tomain— zero new missing keysi18n-hardcoded: +4 vs main. One was a German phrase in a code comment (fixed). The otherthree are heuristic false positives on three-word legal phrases (
Entrata in vigore,Entrée en vigueur, and the French hero subtitle flagged as Spanish); the translations arecorrect and were not distorted to satisfy the heuristic. These three audits already fail on
mainand are not wired into CI.Still open (not blockers)
Two questions for the authorities before anything is filed, both recorded in
docs/legal/README.md: whether Zurich'sBranchenregelungfor Ausweis-N work permits is still in force (source is a 2017 AWA factsheet), and whether the Handelsregister accepts an Ausweis N under Art. 24a HRegV.🤖 Generated with Claude Code