Skip to content

fix(auth): evig owns its own admin + docs(legal): Verein founding documents - #270

Merged
github-actions[bot] merged 3 commits into
mainfrom
worktree-fix-superadmin-evig-ownership
Aug 6, 2026
Merged

fix(auth): evig owns its own admin + docs(legal): Verein founding documents#270
github-actions[bot] merged 3 commits into
mainfrom
worktree-fix-superadmin-evig-ownership

Conversation

@catomean

@catomean catomean commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

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 admins

SUPER_ADMIN_EMAILS still hardcoded andreas@, veronica@ and daniel@revamp-it.ch. isSuperAdmin() resolves that list by e-mail alone, bypassing the database, and src/auth.ts ORs it into the session — so each held full access to evig's users, finanzen and settings. The permissions API also refuses to demote anyone on the list, so they were un-removable through the UI.

  • SUPER_ADMIN_EMAILS reduced to the owner as a documented bootstrap floor; users.is_super_admin is 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.
  • Lockout guard: super-admin is now checked before the is_staff gate in canAccessSection / canAccessSensitive / getAccessibleSections, so a cleared flag can't lock the owner out.
  • STAFF_EMAIL_DOMAIN derives from ORG.emailDomain (evig.ch); legacy Revamp-IT domains move to LEGACY_STAFF_EMAIL_DOMAINS so existing logins keep working until the Layer B infra cutover. This path is cosmetic (welcome-email template) and grants nothing.
  • Docs corrected — .claude/CLAUDE.md still claimed "Anyone with @revamp-it.ch email is staff", which has been false since the permissions-v2 hardening.
  • Regression guard: the removed addresses must return false from isSuperAdmin().

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.

email is_staff is_super_admin staff_permissions
georgy.butaev@revamp-it.ch t t {*}
e2e-admin@revamp-it.ch t f {*}
shop@revamp-it.ch f f {}

No database change was required. (e2e-admin@revamp-it.ch is the admin persona for the prod route-inventory gate — scripts/e2e-inventory-prod.sh, credentials in AUTH_TEST_ADMIN_EMAIL. It is load-bearing, not an orphan; the .test addresses in ci.yml belong to the local E2E job.)

Worth your decision, not mine: that smoke account holds {*}, which grants the sensitive sections (users, finanzen, settings) on production, with its password in GitHub secrets. It needs broad access because the gate walks every admin route — but a dedicated non-wildcard permission set covering exactly those routes would shrink the blast radius. Left as-is deliberately.


2. docs(legal) — founding documents

docs/legal/ — draft STATUTEN.md, GRUENDUNGSPROTOKOLL.md, and a README.md checklist 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:

  • Art. 2 — idealistic Zweck, explicit kein Erwerbszweck, economic activity admitted only as Mittel zum Zweck und nie Selbstzweck, directed at the Allgemeinheit rather than members (Uneigennützigkeit per KS 12).
  • Art. 4 — the Verein may found and hold participations, held as Vermögensanlage, subordinate to the purpose, with no Geschäftsführung by the Verein. This is simultaneously what KS 12 requires and what keeps the founder clear of the self-employment ban. It is what makes the GmbH split amendment-free.
  • Art. 10 Abs. 2 — board needs at least 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, since they get requalified as salary); mandatory Ausstand on one's own compensation; disclosure in the annual report.
  • Art. 14 — publication of statutes, board, report and accounts in a form that keeps changes traceable; no Spendenbescheinigungen or claims of recognised gemeinnützigkeit before the Verfügung exists, matching ORG.legalForm = 'in Gründung' as the SSOT.
  • Art. 17 — Vermögensanfall to another tax-exempt Swiss legal person; distribution to members excluded (mandatory for exemption).

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 an
unmissable 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):

Layer Owns
src/config/statuten.ts article order, numbers, section grouping, adoption state, source URL
messages/<locale>.json strings only — one coherent body per article
src/lib/legal/statuten-body.ts parses a body into paragraph / lettered-list blocks
src/components/legal/StatutenBody.tsx renders them

Paragraph 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 notice
card 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 — clean
  • npm run lint — 0 errors (48 pre-existing warnings)
  • npm run lint:umlauts — clean; no eszett (Swiss German rule)
  • 156 auth / permission / user tests pass
  • Full suite: 7734 passed. The 25 failures in src/config/__tests__ reproduce identically on clean main and are untouched here.

Verification of this third part

  • npm run lint — exit 0 · npm run lint:umlauts — clean · npm run typecheck — clean
  • npm run test:i18n — 3 suites, 16 tests pass (the structure gate CI enforces)
  • All 8 locales render 200 with 18 articles; hub link and sitemap entry confirmed
  • i18n-leaks: 554 total, identical to main — the translations add zero German leaks
  • i18n-audit: missing-key counts identical to main — zero new missing keys
  • i18n-hardcoded: +4 vs main. One was a German phrase in a code comment (fixed). The other
    three 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 are
    correct and were not distorted to satisfy the heuristic. These three audits already fail on
    main and 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's Branchenregelung for 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

catomean and others added 2 commits August 5, 2026 18:58
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>
@catomean catomean changed the title fix(auth): remove Revamp-IT super admins from evig, DB is the SSOT fix(auth): evig owns its own admin + docs(legal): Verein founding documents Aug 5, 2026
…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>
@catomean
catomean marked this pull request as ready for review August 6, 2026 09:04
@github-actions
github-actions Bot merged commit 20f6129 into main Aug 6, 2026
7 checks passed
@github-actions
github-actions Bot deleted the worktree-fix-superadmin-evig-ownership branch August 6, 2026 09:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant