Combined/dev image links org mgmt#197
Draft
vharmain wants to merge 43 commits into
Draft
Conversation
Sports sites can now carry an :images collection (url, alt-text, copyright,
description) edited through a new "Kuvat" tab. Images live in an external
image bank; LIPAS stores only the CC BY 4.0 metadata.
Access is gated by a new :site/edit-images privilege. The :images-manager
role grants it without :site/save-api, so its holders can only save revisions
whose diff is limited to :images; enforced in backend/core/check-permissions!
by comparing the incoming doc against the persisted one (ignoring event-date
and search-meta noise).
Initial rollout: Loimaa (city-code 430) assigns the role to designated users.
Later cities either add the privilege to an existing broader role or get a
role assignment. :images surfaces in Public API v2 (not v1, which is frozen).
Elasticsearch mapping declares :images as {:enabled false} — existing strict
indices must be recreated or have the field added before deploy, otherwise
saves with :images populated will fail.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…fallback
Per the external image-links requirement spec ("Täydentävä
vaatimusmäärittely"), a bare URL is no longer accepted:
- :alt-text and :copyright (source/owner) are now required with at least
one non-empty translation; enforced in the malli schema (validates the
internal save API and surfaces in v2 OpenAPI) and in the edit dialog,
whose save button stays disabled until URL + alt-text + copyright are
filled.
- Image URLs must be https:// — http images would be blocked as mixed
content on the https-served LIPAS anyway. Single source of truth in
lipas.schema.sports-sites.images/valid-url?, shared by schema and UI.
- Failed image loads (broken links) no longer render the browser's
broken-image icon: the dialog preview and the hover popper swap in a
neutral text placeholder via on-error.
- Schema descriptions and docs/site-images.md now spell out the API
consumer obligations: embed/hotlink images from the source URL, no
caching or re-hosting, so source-side takedowns (GDPR/copyright)
propagate immediately. Updating the official API Terms of Use remains a
legal/process task outside this repo.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Previously :site/save-api holders (e.g. city-managers) could write :images through the regular save endpoint even though the UI never offered it to them — pilot containment relied on the UI alone. check-permissions! now enforces both directions of the diff against the persisted revision: - :images changed (or present on a new site) -> :site/edit-images required - anything else changed -> :site/save-api required, as before Saves that merely round-trip unchanged images (the UI posts the full document) need no images privilege, so regular editing of sites that already have images keeps working. Rolling the feature out to a new municipality remains a pure role assignment: grant :images-manager (on top of existing editor rights) with the municipality's city-code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two bugs hid persisted images from the UI even though the API returned them: - The ::display-site sub builds its map field-by-field and never included :images, so the read-only tab always rendered an empty table — and view-images? gating made the tab invisible to users without :site/edit-images even when the site had images. - The images editor caches its table state from :value once at mount (r/with-let), so entering edit mode after viewing the empty read-only tab kept the stale empty state. Key the component by lipas-id, mode and value so state re-derives whenever the source changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Connect organizations to the data they produce and make org state immutable and auditable, without touching the per-request auth path or the matcher core. P1 — Immutable org store + ownership link: - `org` becomes an append-only revision table + `org_current` view + GIN index (migration 20260604120000), seeded from legacy orgs with members backfilled from account roles. `org.clj` rewritten to revisions; membership lives in the org document. - Site `:owner-org-id`/`:edit-grants` (document) + `acting_org_id` revision column (20260604120001); ES indexes `search-meta.owner-org-id`/`editor-org-ids`. - Owner-enum hard-lock on org-owned sites. P2 — Org-derived permissions: - New `:org-editor` role; `select-role` `:org-id` uses set-intersection (a site can have several editor orgs); `site-roles-context` emits the editor-org set; `wrap-es-query` gains an org branch. - `derive-org-roles`/`derive-user-org-roles` project membership -> roles at login (auth/enrich-org-roles), never persisted. Structural ceiling: only catalog templates expand. P3 — Self-service: - Catalog edit (lipas-admin), a separate catalog-validated org-admin invite endpoint with a custom org-invitation email, member role/template/remove ops, cross-org site edit-grants, and a requested->approved take-over workflow (org_takeover_request, 20260604120002 + lipas.backend.org-takeover). No invitation table: invites reuse add-user! + the existing magic link. All migrations are reversible; verified via REPL and the test harness (roles, org incl. take-over + 4 permission-bound safety tests, handler, ptv, enrichment, search-mapping) with no regressions. UI deferred. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Build the opt-in org-management UI on top of the P1–P3 backend, plus the three read endpoints the dashboard phases need. Frontend (lipas.ui.org + site page): - ::can? capability sub gating every new control by the UX-plan matrix - tab restructure: Overview / Members / Roles & templates / Our sites / PTV / History - Members: unified invite-member (email + org-role + catalog template multi-select), member table on :org-role/:templates with tooltip chips and inline role/template edits - Roles & templates: read-only catalog for org-admins, editable for lipas-admin - Overview: type + ownership-rule (lipas-admin only) - Our sites: owned/editable filters, per-site "who can edit" drawer, cross-org grants, claim-ownership, folded-in bulk-ops - History timeline; site-page "Editing rights" tab (org-owned sites only) - orgs list: enriched cards + lipas-admin take-over approval queue - surface :owner-org-id/:edit-grants via ::display-site Backend (thin reads): - GET /orgs/:id/sites?filter=owned|editable (core/org-sites, ES terms) - GET /sites/:lipas-id/editors (core/site-editors) - GET /orgs/:id/history (org/get-history, revision diffs) Tests: org-sites/site-editors/history added to org-test (all green; roles-test + handler-test unaffected). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Part of the ongoing feat/org-management work. This commit captures the current branch state and adds the following on top: Permission-ceiling hardening (lipas-admin only, enforced + tested): - Role-template catalog: strict malli schema (every spec must name a real, catalog-assignable role) replaces the previous :map-of :keyword :any on /actions/update-org-role-templates; org/validate-catalog! guards REPL and internal callers too. FE sanitizes half-filled specs before saving. - /actions/update-org now pins :type and :ownership to current values unless the caller has :users/manage, so an org-admin can edit name/contact/PTV but cannot widen their own take-over ceiling (marshall re-defaulting made a plain key-drop unsafe, hence pin-to-current). - HTTP-level authz tests: catalog endpoint (admin 200 / org-admin 403 / regular 403) and org-admin :type/:ownership edits are no-ops while lipas-admin's persist. Roles & templates editor: - Surfaces per-template member-assignment counts and warns before saving away a template that members still depend on. Member-invite UX (single email-based flow for all admins): - New org-scoped POST /actions/check-is-existing-user returning ONLY a boolean (GDPR-safe; gated [org-scope-from-body :org/manage], no directory exposure, no email harvesting). Drives inline "existing user / new invite" status and an adaptive button label. - invite-org-member! now emails in both branches: magic-link invitation for new accounts, "added to organization" notification for existing ones. - Dropped the lipas-admin-only directory autocomplete + redundant :user-id path (the source of the duplicate "Sähköposti" label); result toast is accurate via the response's :new-account?. Also includes in-progress org-management surfaces carried on this branch (bulk operations, takeover, dashboard, shared role-spec editor) and the design/spec docs under docs/. Verified: cljs compiles clean; roles/org/handler test namespaces green; new endpoint + both email paths exercised via REPL. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sports-site form (Omistajuus): - Owner-org autocomplete on new sites for owner-capable users (pre-filled when exactly one ownable org), and on any site for lipas-admins; clearing it falls back to the legacy Omistaja select. Non-admins can't change ownership on existing sites (take-over flow only). - Selecting an org locks :owner to the org-type enum (org-type->owner moved to lipas.data.owners so FE + backend share it). - Backend gate: POST /sports-sites authorizes a claimed :owner-org-id via owner-org-assignment-authorized? (requires :site/create-edit on that org); legacy saves untouched. Org view: - History tab is now admin-only (lipas-admin or org-admin): FE :org/view-history capability + org-history route tightened to :org/manage. - Audit-log diffs resolve member user-ids to emails (not raw UUIDs). - Terminology: Kohteemme -> Kohteet, Oikeudet ja omistus -> Käyttöoikeudet (en/se aligned); Kohteet is now the first and default tab, with its data loaded on org open. Tests: owner-org assignment authz, history authz (member -> 403), and member-email resolution in the audit log. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Org instructions (member guidance): - New "Ohjeet" tab (first), localized fi/se/en shown as language sub-tabs; org-admin/lipas-admin edit, members read-only. Stored in the org payload under :instructions; added to the malli org schema, marshall/unmarshall, and the update-org! whitelist (members/catalog still preserved). Audit diff records "Ohjeet päivitetty". New capability :org/edit-instructions. Who-can-edit panel (Kohteet tab) redesign: - Replaced the confusing "Muokkausoikeus myönnetty:" stack with one scannable list answering "who can edit this site", each row tagged by reason (Omistaja / Jaettu+revoke / Aktiviteetti / Suora oikeus), plus a clear "share edit access" action. New i18n keys; old grantee/legacy labels retired. Legacy direct-permission users (Q2, design-spec §6 step 4): - core/site-editors now populates :legacy-users. db/users-with-permissions-matching pre-filters accounts via jsonb containment on account.permissions (both city-code/city_code key spellings), then the exact check-privilege confirms; admins excluded. GIN index account_permissions_gin (jsonb_path_ops) added by migration to keep the selective path fast. Verified in REPL: candidate result == exhaustive naive scan, single-digit-ms. Tests: org-instructions round-trip + org-admin route; site-editors legacy-users (direct user found, admin excluded, candidate == naive scan). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Each access-reason chip (Omistaja / Jaettu / Aktiviteetti / Suora oikeus) now explains in a tooltip how that access is granted. Wrapped the chip in a MUI Tooltip (revoke action on the shared chip still works). New i18n keys in fi/en/se. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Merge the three working docs (design-spec, ux-plan, ux-followup) into a single docs/wip/org-management.md reflecting what's actually built on this branch: site-form ownership UI + owner-enum lock + assignment authz, org instructions (Ohjeet) tab, terminology + tab order, admin-only history with UUID→email resolution, the who-can-edit redesign, and the legacy-editor lookup (candidate pre-filter + GIN index). Records endpoints, capabilities, verification (incl. the legacy-editor REPL benchmark), and open items (default tab, GDPR email decision pending PM/DPO). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Surface each site's per-revision edit history (timestamp + editor email) in the org Kohteet expand drawer, alongside the existing who-can-edit list. Useful for the org members maintaining the data. - Lightweight per-revision query (event-date/author-id/status, no documents) so it stays cheap on long-lived sites; authors resolved to emails in one batched account lookup (mirrors org/get-history). - New POST /actions/site-edit-history, visible to any authenticated user (org members included), like /actions/site-editors. - Lazy-fetched on row expand, cached per lipas-id; display capped to the 50 most recent revisions with the true total in the header. Emails are shown to all org members for now; swapping to username/reveal is a one-line change once PM/DPO decides (see org-management.md §10.2). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Record the future enhancement discussed: org-admins currently have no reachable claim trigger (both entry points are lipas-admin-only), and claim scope is fixed to the org's ownership rule. Captures the two follow-ups reusing the existing org_takeover_request state machine: surface the org-admin bulk-request trigger, and per-site claims for wrong-owner corrections / private->municipality transfers. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The ptv-organizations migration (20250707) called org/create-org, which on this branch was changed to append to the new org revision schema (org_id/document). That schema isn't created until the later org-event-log migration (20260604120000), so a from-scratch migration run (CI, fresh deploy) failed at "column org_id of relation org does not exist". Existing DBs were unaffected only because the migration was already marked complete. Freeze the migration to insert directly into the single-row org schema that existed when it was written (a plain sql/insert!, matching the old create-org). The org-event-log migration folds these seeded rows into the new revision schema. Verified end-to-end: full migration chain now runs clean on a fresh database, org_current shows all three PTV orgs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Naming: the read/query org endpoints were noun-only and didn't read as actions. Rename them to verb-first, consistent with the command endpoints (create-/update-/grant-/approve-/…), updating handler routes and every FE call site + test: current-user-orgs -> get-current-user-orgs all-orgs -> get-all-orgs org-members -> get-org-members org-sites -> get-org-sites org-sites-for-bulk -> get-org-sites-for-bulk org-history -> get-org-history org-takeover-preview -> preview-org-takeover org-takeover-requests -> list-org-takeover-requests site-editors -> get-site-editors site-edit-history -> get-site-edit-history jdbc: convert the new account-name resolver in core to next.jdbc (execute! + as-unqualified-kebab-maps) instead of legacy clojure.java.jdbc. org-takeover's approve! still uses clojure.java.jdbc for its write txn on purpose: it shares the re-entrant with-db-transaction of db/upsert-sports-site! (same pattern as bulk-ops mass-update); switching it to next.jdbc would break that nested-transaction reuse. Verified: bb test-ns org-test + bulk-operations-test green (39 tests, 171 assertions); CLJS compiles clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
"Määritetyt roolit" → "Käyttöoikeudet", mirroring the Käyttöoikeudet tab's existing translations across all three locales (se "Åtkomsträttigheter", en "Access rights") for consistent terminology. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Native-speaker feedback: "lunastaa" reads wrong for claiming sites into an
org. Switch all Finnish claim/take-over strings to the "siirrä omistukseen"
(transfer to ownership) phrasing, short forms; request noun -> "siirtopyyntö".
Covers the i18n labels and the two hardcoded toasts in org events.
Swedish was inconsistent (mixed "anspråk" and "ta över"); unify on "överför"
(transfer) to match. English ("claim"/"take-over") left as-is per request.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Record the agreed simplification and its refactor plan. Downstream both fields
already reduce to the same flat {:role :org-id} set matched by one rule, so the
split only ever existed at the assignment layer; a single per-member :roles list
(drawn from #{:admin} ∪ catalog keys) is simpler for admins to reason about.
- §3.2/§3.5/§4.1: unified model — one :roles list; reserved engine :admin role
(→ org-admin, not catalog-stored, so catalog edits can't strip admins);
:org/member baseline conferred by membership itself.
- §2.5/§6.2/§6.6/§6.7/§7/§8/§9: bounded delegation, members UI, endpoints,
migration and audit updated to the unified model.
- §13: concrete refactor checklist by layer + self-contained reshape migration
(no org/* calls, per §8's from-scratch lesson) + sequencing.
- Also refreshed §7/§6.8 endpoint names to the verb-prefixed forms from the
earlier naming review (get-/preview-/list-), which the doc still listed as nouns.
No code change yet — design doc only; still pre-production so safe to do.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The plan described the invite form's collapse but not the new-member-specific decisions the unification forces. Make them explicit in §6.6 + §13: - roles select defaults to empty = plain member (the old "member" default goes away; baseline is implicit), and :roles [] is a valid submission - :admin is grantable at invite time (first-admin creation in one step) - the existence probe + account creation + magic-link email path is unchanged; only the role payload collapses to :roles - §13.8: invite test cases for empty roles and admin Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Granular, resumable implementation plan (file paths, current→new transforms, sequencing, REPL/test verification) for the §13 refactor. Self-contained companion to org-management.md so the work can be picked up from a clean context. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Collapse the org-member authority split (an admin/member :org-role plus a
:templates data-grant list) into a single :roles list per member, drawn from
#{"admin"} ∪ the org's role-template catalog keys.
- org.clj: "admin" is a reserved engine role (reserved-roles, not in the
deletable catalog) projecting org-admin; org-user is the membership baseline
projected unconditionally (membership ⟺ :org/member). member->roles expands the
single list via reserved + catalog, dropping anything else (the ceiling).
validate-assignment! checks :roles ⊆ #{admin}∪catalog (ex-type
:roles-outside-catalog); set-member-org-role!/set-member-templates! merged into
set-member-roles!; get-org-users and the history diff switch to :roles.
- schema/org.cljc: members entry -> {:user-id … :roles [..]}.
- handler.clj: invite body takes :roles; merged endpoint
/actions/set-org-member-roles; map :roles-outside-catalog/:invalid-catalog -> 400.
- migration 20260608120000-org-roles-unify: self-contained jsonb reshape (no
org/* calls, per the ptv-organizations from-scratch lesson), idempotent —
members already in :roles shape are skipped so a re-run never wipes them to [].
- FE org views/events: one "Roolit" multi-select (Ylläpitäjä + catalog) replaces
the org-role dropdown + templates select; ::set-member-roles. i18n
:roles/:role-admin/:grants-admin in fi/se/en.
- tests: org-test fixtures/endpoints -> :roles, plus new cases for baseline,
reserved admin, ceiling, catalog-cannot-strip-admin, the merged endpoint, and
invite :roles []/["admin"].
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…in UI Org-scoped roles (org-admin/org-user/org-editor) now come ONLY from org membership, projected at login. Direct account roles remain the default plane for everything else (admin, type/city/site/ptv/floorball/activities/itrs/analysis managers) — those have no org concept and stay account-level. An org's existence is itself the opt-in to org management; no separate flag. - roles.cljc: org-admin/org-user -> :assignable false (org-editor already was), so they no longer appear in the admin role editor. :assignable has no backend consumers; this only gates the FE dropdown. - migration 20260608130000-org-roles-account-cleanup: strips org-admin/org-user/ org-editor from account.permissions.roles (now redundant with membership). Self-contained (no org/* calls). Defensive: an org role is dropped only when current membership covers all its org-ids; any uncovered one is kept + logged, so access can never be silently revoked. Idempotent; down rebuilds from membership. Safe because org-event-log backfilled membership for every account org role (verified: all 48 tuples reproduced, no missing/empty org-ids, flattened effective access identical across down/up for all 36 affected users). - Remove dead alpha org admin UI: org-dialog + add-user-to-org-dialog and their orphan subs; org-select + the :org-id context case in the shared role editor; the now-unused react require. Kept admin ::orgs/::org/::get-orgs — they still feed org-name display in user/subs context-value-name. - Plane labels: admin permissions card -> "Suorat käyttöoikeudet" (+ pointer to Organisaatiot); org members tab -> :members-plane-note (fi/se/en). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Centralize "who may change a site's :owner-org-id and :edit-grants" in the core layer as pure predicates, authorized against the STORED revision — never the submitted body. Fixes a privilege-escalation hole (#18) and a revoke asymmetry (#19) found in branch review. Root cause: the generic save path authorized edits using site-roles-context of the request body (whose :org-id = owner-org-id ∪ edit-grants), so an org-editor could grant themselves :site/save-api on any site by injecting their org into :edit-grants. And revoke-site-edit lacked the ownership check grant-site-edit had. - core.clj: new predicates lipas-admin?, owns-site-org?, ownership-change-authorized? (new-site claim by an org you can create-edit on; existing-site change is lipas-admin only), edit-grant-change-authorized? (lipas-admin or owning-org admin). - upsert-sports-site!: authorize content edits against the stored site; reject unauthorized changes to :owner-org-id/:edit-grants. Permission-before-existence ordering preserved (a posted-but-missing lipas-id still 403s before 404). - grant-site-edit!/revoke-site-edit!: both gate via edit-grant-change-authorized!, so the two endpoints share one rule; removed the bespoke handler-side check and the redundant owner-org-assignment-authorized? middleware gate (it couldn't see stored state and falsely rejected legacy city/type editors of org-owned sites). - tests: exhaustive predicate unit tests (business-logic-test) + handler regression tests for the escalation and grant/revoke authz (org-test). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up to the branch review; fixes the remaining medium/low items. Backend: - update-org: pin :ptv-data to current for non-admins (org-admins could otherwise overwrite PTV org-id/creds via the details endpoint, bypassing the :users/manage-only PTV config route). [#20] - org-takeover approve!/deny!: require a 'requested' status and flip it with a guarded UPDATE (WHERE status='requested') so a decided request can't be re-approved and concurrent approvals can't double-claim. New :invalid-takeover-state → 409 mapping. [#23] - invite-org-member!: email send is now best-effort (try/catch, logged) and returns :email-sent? — a delivery failure no longer fails an already-committed membership change. [#24] - validate-catalog!: reject a context-scoped role spec missing its required context key (e.g. ptv-manager without :city-code), which would project an org-wide grant; present-but-empty is fine (matches nothing). [#29] - org-event-log migration: require lipas.backend.db.utils for jsonb (de)serialize, matching its sibling migrations (from-scratch safety). [#22] Frontend: - editing-rights-panel: also dispatch ::get-site-edit-history so the edit-history section on the sports-site tab is populated (was always empty). [#21] - org events ::get-user-orgs/::get-org-users: :on-failure ::todo → ::failure (user-visible error); removed the dead ::todo/::TODO handlers. [#25] - roles editor: :clearOnBlue → :clearOnBlur typo. [#26] - remove dead our-sites subs/events orphaned by the bulk-ops rewrite (::our-sites, ::our-sites-filter, ::org-editable-sites, ::set-our-sites-filter, the :editable fetch). [#28] Tests: catalog missing/empty-context cases; takeover re-approve/deny rejection. 108 backend tests, 400 assertions green; cljs compiles clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bring the consolidated design doc in line with what's now on the branch: - Role unification is implemented (banner + §13 reframed from "plan" to a "DONE" record); org-scoped roles are membership-only (:assignable false, §3.5). - New §4.5 documents the ownership/edit-grant business rule (core predicates authorized against the stored site) — the post-review security hardening. - §6.10 rewritten: save authz is in core; the "deferred caveat" is resolved. §10 item 8 marked resolved. - §8 lists the org-roles-unify and org-roles-account-cleanup migrations; account org-roles are now stripped (not a pending follow-up). - §6.7 documents catalog required-context validation; §6.2 notes the relabelled "Suorat käyttöoikeudet" plane and removed alpha add-user-to-org dialog. - §7 endpoints: save row (all authz in core), grant/revoke (owning-org rule), takeover approve/deny state guard (409). §11.4 lists the added tests. - §10 gains the org-site result-cap limitation (tracked). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Each org card in the Organisaatiot list now shows how many sites the org owns, via a single ES terms aggregation over search-meta.owner-org-id (core/org-owned-site-counts) merged into the get-current-user-orgs response as :site-count. The agg is exact (single-shard) and cheap (~1ms, independent of site count), so it folds into the list load with no new endpoint. Tests: org-owned-site-counts-test (agg correctness, grants excluded, matches org-sites :total) and current-user-orgs-site-count-test (endpoint annotation, 0 default). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The org-added email (existing-account branch of invite-org-member!) now carries a one-click magic login link instead of a plain login-page URL — the invitee lands already authenticated, with a fresh token already carrying the new org role. Mirrors send-permissions-updated-email!, which already mints a magic link for the analogous "permissions changed" event. The link domain is env-specific by construction (frontend login-url from window origin, backend-validated against the LIPAS domain whitelist), so no domain handling is needed here. Token reuses the standard 7-day magic-link lifetime; tighten later across the board if desired. Test: org-admin-invite-existing-account-magic-link-test asserts the token-bearing link in both plain and HTML bodies. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Catalog grant text and the role form now describe roles and their
privileges from i18n data instead of hardcoded per-role cases:
- role-grant-text looks up a role's :role-descriptions entry, falling back
to its translated name, so no catalog role ever renders as "Tuntematon
oikeus" (previously only org-editor/ptv/activities were handled; city-,
type-, site-manager etc. fell through).
- role-spec-editor now lists the selected role's privileges inline
("Sisältää käyttöoikeudet:"), driven by a new :privilege-descriptions
i18n map covering all privileges — so a LIPAS admin no longer has to
remember what each role grants. Shared primitive, so it appears in both
the org catalog editor and the admin user-management form.
- :site/save-api description reworded to "Osittaisten muutosten
tallentaminen (esim. aktiviteetit, salibandy, ITRS)" — it is the
aspect-editor save grant, not a duplicate of create-edit.
- Renamed the "Reittien lisäys" preset to "Tyyppi" (key :routes -> :type);
it narrows on facility types. No org used the old key.
- Removed the now-unused :grants-org-editor/-ptv/-activity/-unknown keys.
Privilege keys are namespaced (:site/create-edit); tongue drops a
namespaced leaf's namespace, so the i18n map munges "/" -> "." and the
lookup mirrors it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
sanitize-catalog now collapses exact-duplicate role-specs (distinct) alongside dropping half-filled rows, so a template can never carry the same grant twice (it would render twice and grant nothing extra) — the gunk seen on the Laukaa "Muokkaaja" template. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The save-endpoint gate (check-permissions!) now accepts :site/create-edit OR :site/save-api, so a general editor needs no separate save-api grant. Removed :site/save-api from the `basic` privilege set; it remains only on the aspect-specific editor roles (activities/floorball/itrs) that may persist a partial edit WITHOUT being general editors. Behavior is unchanged: the OR admits exactly the same users as before (verified — city-manager still saves via create-edit, activities-manager still via save-api, scoping and denial intact). No migration needed: privileges are derived from role keys at check-time, never persisted (confirmed across all 2346 accounts incl. legacy permissions — zero store any expanded privilege). Tests green: roles-test, business-logic-test, org-test, handler-test, api v1/v2 handler-test, bulk-operations-test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- organizations.md: replace the outdated, too-technical pre-branch doc with a high-level overview of org management as it works now (two permission planes, role catalog as ceiling, self-describing roles, ownership + shared editing, claims, members, history, PTV). - org-management.md: drop the plan-vs-implemented / DONE framing and the §13 change-record; write the spec as a present-state description. Correct to current code: reserved :admin role lives outside the catalog (membership view-baseline + reserved admin are the only non-catalog authorities); lipas.roles/basic no longer carries :site/save-api (save gate accepts create-edit OR save-api); add self-describing role/privilege descriptions and catalog dedup/sanitation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fixes all 32 findings from the multi-agent code review (2026-06-09) plus one found in live testing (F33). Per-finding details, test evidence, and remaining follow-ups: docs/wip/pr193-review-findings.md. Security / authorization: - Save path checks :site/create-edit against stored AND submitted docs (scope escape closed); server carries :owner-org-id/:edit-grants forward when absent; :acting-org-id no longer forgeable via save - Bulk contact updates authorize against DB state and route through core/upsert-sports-site! (single enforcement point, no ES clobber) - Edit-history author emails gated behind :users/manage - Org-admin field ceiling moved into the org business layer Functional: - Inviting an existing member returns 409 instead of wiping their roles - Org members with catalog-projected roles can create sites in the UI - Who-can-edit drawer evaluates catalogs through the roles engine - Catalog-granted PTV managers receive audit notifications - Owner org name denormalized into ES search-meta and shown to all viewers; "other"-type orgs can own sites; fresh event-date on grant/takeover revisions; members tab works on fresh orgs - Org members see the org sites list read-only; bulk actions gated by :site/create-edit; list-fetch errors rendered instead of swallowed Efficiency / cleanup: - Takeover approve! batch-loads revisions; owned-site count uses a count-only ES query; zero-org users skip the counts aggregation; deduped member upsert, uuid coercers, account-name resolver, org emails, admin list subs; deleted legacy test-only member path and dead org UI events; backend normalizes catalog role-specs Tests: ~26 new regression tests, each verified to fail on the old behavior. Full suite: 523 tests, 11835 assertions, 0 failures. DEPLOY NOTE: requires a full Elasticsearch reindex (strict mapping gained search-meta.owner-org-name; saves fail without it). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The ownership-rule queries in lipas.backend.org-takeover build :->> / :-> HoneySQL forms, but the pg-ops registration that makes those render as infix operators was only loaded as a side effect of lipas.wfs.core. In processes that never load wfs (the deployed server), HoneySQL fell back to function-call rendering — ->>(document, 'name') — which PostgreSQL parses as an operator on a record: "operator does not exist: ->> record". Dev REPL and the test suite load broadly, masking the load-order dependency; reproduced via the uberjar with only the takeover ns loaded. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- F34: site-roles-context derives :activity from the site's type (by-type-code) unioned with document UTP keys, so activities-managers can bootstrap UTP data on fresh typed sites and their orgs appear in the who-can-edit drawer (reviewer case: Bikeland on cycling routes) - F35: org type "sports-federation" renamed to "association" (fi "Yhdistys"); idempotent migration rewrites existing org revisions - F36: org Kohteet list refetches on every page/tab entry instead of serving cached app-db state (new sites appear without browser refresh; ~1s ES indexing lag remains inherent) - F37: "Organisaatiot" navbar crumb is always a link back to the org list (navbar sub-page header now links for parameterized routes) - F38: GDPR - site edit history shows timestamp + coarse role label (Yllapito/Kunta/Organisaatio/Muu, derived at read time) to non-admins; person identifiers only for :users/manage Gate: full suite 526 tests / 11828 assertions / 0 failures, cljs compile clean, browser smoke 5/5 PASS. Tracker updated (docs/wip/pr193-review-findings.md). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
site-editors gains :legacy-activity-users: individuals whose direct roles grant :activity/edit but not :site/create-edit (previously they could edit a site's UTP data — including typed-but-empty sites since F34 — while staying invisible in the transparency view). The drawer tags them with the same Aktiviteetti label as activity-editor orgs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PM request (Uuvi case): an association's estate is enumerated, not rule-shaped — the rule narrows the candidate pool and a checkbox picker draws the exact boundary. Spec: docs/organizations.md "Claiming a curated subset". - Requests snapshot the claimed lipas-ids (curated subset validated against the rule's matches → 400 :invalid-selection otherwise; full match list when no subset given) and approve! applies EXACTLY the stored set — closing the drift where approval re-ran the rule and silently claimed sites that appeared after the request. No migration: the existing org_takeover_request.lipas_ids column is repurposed. - Already-owned skip, idempotency, owner-enum lock unchanged; sites deleted between request and approval are skipped and reported. - Preview dialog: per-site checkboxes (default all checked), header select-all/none with indeterminate state, name filter, "N / M valittu" summary, confirm disabled on empty selection; the approval queue shows the stored snapshot read-only. Stepper claim button disabled while the dialog is open (click-through hardening). - 6 new integration tests incl. empirically-proven drift closure. Full suite: 532 tests / 11869 assertions / 0 failures. Browser smoke 5/5 PASS. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The org Muutoshistoria resolved revision authors with emails unconditionally — an org admin saw LIPAS staff members' personal emails. get-history now branches like site edit history (F38): :users/manage callers keep :author-name (email); org admins get :author-role, the coarse current-role label rendered with the same i18n keys. Member references inside change summaries keep emails in both modes — they are the org's own members, already visible to every member on the Jäsenet tab. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The deftranslations macro read EDN files invisibly to shadow-cljs, so editing a translation file did not invalidate the cached compilation output of lipas.i18n.fi/se/en and release builds shipped stale translations. The ^:dev/always hint sat on the macro namespace, which shadow's cache check never consults for the consuming namespaces. Read EDN through shadow.resource/slurp-resource when expanding for CLJS, registering each file as a compilation input of the consuming namespace so both release and watch builds recompile exactly when an EDN file changes. The JVM path keeps plain slurp and resolves nothing from shadow, so the backend never loads the CLJS compiler. Also drop the now-redundant ^:dev/always and remove five ice-hall top-level keys whose EDN files were deleted long ago (resurrected by a merge), silencing the missing-file warnings on every build. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Release (Luovu omistajuudesta): an org admin can give up ownership of selected owned sites — authority-shedding, so self-service with no approval queue. New ownership-change-authorized? arm (owner-org admin may clear, never gain), preview-release/release! mirroring approve! (one tx, one ES bulk, acting_org_id, skip+report non-owned), endpoints preview-org-release/release-org-sites (:org/manage), and a selection-bar action + impact dialog in Kohteet. Release clears :edit-grants with the owner and offers an optional :owner relabel (the org-type lock vanishes), making release → re-claim the v1 transfer path. Contested claims: take-over previews (live-rule and stored-request) now carry current-owner-org-id/-name per row; the claim dialog flags such rows with a warning chip naming the current owner plus a bold summary bullet, so a claim that transfers sites away from another org is visible to the requester and approver. Decision: visibility suffices, no separate consent flow (spec §10.8). Both close-dialog events now keep the preview through MUI's exit fade (clearing it mid-fade flipped the still-mounted content to the loading state, reading as a phantom reopen). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fetch-grid runs a geo_shape filter with no sort, so Elasticsearch returns the matching grid cells in arbitrary (segment-layout-dependent) order. The test asserted on (first result), which flaked when the order flipped between which of the two in-radius cells came first (passed locally / on most CI runs, failed on others with the adjacent 250mN667150E38600 cell first). Assert the full set of returned grd_ids instead - deterministic, order-independent, and stronger (verifies both seeded cells are returned within the radius). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fresh throwaway integration branch off the rebased feat/org-management (now on latest master: gis rewrite, s3 removal, test-infra speedups, diversity flake fix) with feat/site-image-links woven in for lipas-dev. Semantic merge decisions (re-applied from the prior combined merge, since they live only in the combined branch, not either feature): - core.clj: org-management's upsert-sports-site! flow is the skeleton; image-links rules woven in: 4-arity check-permissions! (extra :site/edit-images OR branch when images-only?) + separate check-image-permissions! (any :images change needs :site/edit-images) + images-only-diff?. `stored` fetched RAW via db/get-sports-site (not core/get-sports-site) so the images-only diff isn't poisoned by enrich-activities adding :geometries. - map/views.cljs: editing-rights tab = :value 7, images tab = :value 8. - subs.cljs: surface both org ownership keys and :images on the site page. - test_utils/fix-generated-site strips :images (like :owner-org-id / :edit-grants) so generated images don't make non-images-role saves flaky by seed. Throwaway: never merge to master; review happens on the two feature PRs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
d3891cb to
210c405
Compare
The canonical lipas-id schema was [:int {:min 0}] (allowed 0), while the
jobs payload schema required pos-int?. lipas-ids come from a Postgres
sequence and are always >= 1; every save enqueues an analysis job
validated as pos-int?, so a site with lipas-id 0 could never be saved.
The looser front gate let mg/generate produce :lipas-id 0, which passed
the sports-site schema but failed the job schema at enqueue time -> 500
(seed-dependent flake in HTTP-save tests, e.g. the images-manager test).
- Tighten the canonical schema to [:int {:min 1}].
- Repoint the ad-hoc [:lipas-id :int] / [:vector :int] / [:set [:int
{:min 0}]] usages at the canonical #'sports-sites-schema/lipas-id
(jobs payload, bulk-operations, ptv handler/workbench, ptv schema,
users schema, frontend map route) instead of re-spelling it.
- Update lipas-id-test: 0 is now invalid, matching the contract the jobs
layer always enforced.
Two deliberate exceptions:
- ptv/ai.clj keeps inline :int: it feeds malli.json-schema/transform for
Gemini structured output, where a var ref becomes a JSON-schema $ref
that Gemini's schema subset doesn't support (verified).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 53ee6cd)
deftranslations expands concurrently for lipas.i18n.fi/se/en under shadow-cljs's parallel release compile. Each expansion did a bare (requiring-resolve 'shadow.resource/slurp-resource), so on a cold build (no warm cache) the calls raced on the half-loaded shadow.resource namespace and threw "Attempting to call unbound fn: #'shadow.resource/slurp-resource", failing the CI "Build frontend" job. It only surfaced on this branch (+ combined): master/image-links use the old plain-slurp deftranslations with no shadow.resource dependency, and a warm local cache or a compilation order where shadow.resource loads first (combined's extra namespaces) hides the race. Reproduced reliably locally with a cold .shadow-cljs cache. Resolve the var once through a delay so the require runs a single, serialized time. The delay never derefs on the JVM (the macro takes the plain-slurp branch there), so the backend never loads shadow/cljs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> (cherry picked from commit fe10402)
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.
No description provided.