Skip to content

Feat/auth image collections [OWTR #1] - #548

Open
JohanHjelsethStorstad wants to merge 87 commits into
mainfrom
feat/auth-image-collections
Open

Feat/auth image collections [OWTR #1]#548
JohanHjelsethStorstad wants to merge 87 commits into
mainfrom
feat/auth-image-collections

Conversation

@JohanHjelsethStorstad

@JohanHjelsethStorstad JohanHjelsethStorstad commented Jul 27, 2026

Copy link
Copy Markdown
Member

Image system rewrite: special/dynamic collections + double-level visibility

This is PR # 1 in the one week till realese series. These should be merged sequentially

Rewrites the image service from a single flat "images" service into a
sub-service with per-domain implementations, and puts every dynamic image
collection behind a two-level visibility system (who may see it, who may
administrate it) with a full admin UI.

main..HEAD is 82 commits / 374 files. This is a large refactor — the sections
below are ordered roughly by how much reviewer attention they need.


1. Image service: one service → sub-service + implementations

The old src/services/images/{actions,operations,schemas,auth,types,collections/}
is deleted and replaced by:

Path Role
images/subservice/ The generic image/collection sub-operations every implementer builds on (uploadImage, updateCollection, destroyCollection, readPageOfImagesInCollection, …)
images/subservice/special/implement.ts implementSpecialCollection() — how a domain service claims one SpecialCollection and gets typed operations for it
images/dynamic/ Ordinary user-created collections (the ones with visibility)
images/standard/ The STANDARDIMAGES collection + readStandardImage / regeneration-from-config
images/specialPanels/ Assembles every special collection's panel operations for the frontend

Domain services now own their own image collection rather than reaching into a
shared one — users (PROFILE_IMAGES), ombul (OMBULCOVERS), committees
(COMMITTEELOGOS), flairs (FLAIRIMAGES).

Schema: Flair.image and Committee.logoImage now point at Image instead
of CmsImage. Article/ArticleCategory/NewsArticle moved out of the CMS
schema file to reflect that they are implementations, not CMS primitives.

Standard images are no longer seeded rows that can drift: each is declared in
StandardImageConfig with a source file in the new top-level standard_store/,
and readStandardImage regenerates it from config if it is missing or has
escaped the standard collection. The SpecialCmsImage enum shrank accordingly
(FRONTPAGE_LOGO, NOT_FOUND, LOADER_IMAGE, nav/footer buttons … are gone —
those are standard images now).

Store (src/services/store/src/lib/store/) became a factory,
implementStore(), so each service gets a namespaced store with its own allowed
extensions — and deleting an image now actually deletes the files.

2. Double-level visibility

Visibility is attached twice to every ImageCollection
(visibilityRegularId / visibilityAdminId).
implementDoubleLevelVisibilityOperations() gives an owning service a
readDoubleLevelMatrix plus updateRegularLevel / updateAdminLevel, each with
its own authorizer and an ownership check that a passed visibilityId really is
that owner's level.

New authorizer RequireLevelFromDoubleLevelVisibility (level: REGULAR | ADMIN,
with an optional bypass permission — IMAGE_ADMIN for images).

Invariant: the admin level must always be a sub-visibility of the regular
level; an administrator who cannot see what they administrate is a broken state.
Enforced via isSubVisibility on the matrix the update would produce, so it is
checked before anything is written and either level can still be updated alone.

Note one non-obvious consequence: an empty admin level means everyone
administrates, which is not a subset of a narrowed regular level. So on a fresh
collection the admin level must be narrowed before the regular level can be.

beforeRun (framework): enforcing this needed a hook, so .implement() gained
an optional beforeRun?: BeforeRun<…> taking the same args as authorizer /
ownershipCheck. It runs after auth and ownership, before the operation, and
throws to abort. This is the general place for implementer invariants a
sub-operation cannot state on its own — ownershipCheck stays for "does this
implementer own the resource".

Bug fix: visibilityOperations.update called omegaOrderOperations.readCurrent({})
without bypassing auth. That order is only a placeholder stored on ACTIVE
conditions, but requiring the caller to also hold OMEGA_ORDER_READ — not a
default or membership permission — meant essentially nobody could save a
visibility change
. Now { bypassAuth: true }, matching every other internal
readCurrent call.

3. Frontend

  • VisibilityAdmin — new editor for one matrix: requirements (ANDed) each
    holding conditions (ORed), ACTIVE vs ORDER per condition, group/order pickers.
    CollectionAdmin mounts it twice, once per level.
  • DoubleLevelVisibilityDescription — human-readable "Kan se / Kan administrere",
    shown on the collection page. Generic over any double-level service, grouped with
    VisibilityAdmin under _components/Visibility/.
  • CollectionAdmin runs one authorizer per action (upload-one, upload-many,
    update, destroy, update-regular, update-admin) instead of gating the whole panel
    on updateCollection. The count of auth checks now matches the count of actions.
  • The visibility matrix is fetched server-side in page.tsx and threaded down. If
    that read fails it becomes null and the visibility button is simply hidden
    (rather than 404-ing the page); the remaining checks fall closed against an
    unsatisfiable placeholder, so the IMAGE_ADMIN bypass still works.
  • ImageListImagePanel, which serves both special and dynamic collections;
    CollectionCard no longer forces being a link (new CollectionCardLink);
    new StandardImageServer / StandardImageClient; new ClientData provider
    replaces DefaultPermissions and the image-selection contexts.
  • ImageUploader is now just a Form. Callers decide on popup vs inline —
    committee logos and ombul covers render it beside the current image, EditOverlay
    is no longer used for special-collection uploads, and title is caller-supplied.
  • Flair image editing moved out of the reusable Flair component into a button in
    the /admin/flairs list. (Flair had briefly become a client component receiving
    a Session class instance as a prop, which crashes RSC serialization.)
  • Fixes: invisible "add part" buttons on the article editor (secondary matched the
    page background), and collection cards collapsing horizontally in the CMS image
    editor (missing flex-shrink: 0).

4. Shared utils

Pulled out of services into src/lib/groups/ so they can be reused and so none of
them throw:

  • inferGroupName, checkGroupValidity, groupOptions (orderOptions / findGroup).
  • checkGroupValidity no longer throws — it returns
    { valid: true, group } | { valid: false }. The throwing behaviour lives in
    assertGroupValidity in the service layer, which all 5 existing call sites use.
  • orderOptions deduplicates the identical order-range logic that VisibilityAdmin
    and UserList each had.
  • describeMatrix moved to auth/visibility/ next to checkVisibility /
    isSubVisibility.

5. Seeding

Migrated to defineSeedOperation (context-aware, so seeders stop hand-threading
prisma/session). Fixes a standard-image race on seed, stops logging expected
NOT-FOUNDs during upsert, and makes the OmegaWeb migration use the new image system.

6. Tests

New tests/services/visibility.test.ts (29 tests) and
tests/services/dynamicImages.test.ts (21 tests) — 50 total, all passing.

  • Unit coverage of checkVisibility (AND across requirements, OR within one,
    ACTIVE vs ORDER, empty = everyone) and isSubVisibility.
  • visibilityOperations create/update/destroy: update replaces rather than
    appends, ORDER conditions keep their order, cascade on destroy.
  • A standalone double-level implementation built directly on
    implementDoubleLevelVisibilityOperations with no owning domain model — its
    implementationParams are just the two visibility ids. Covers both level updates,
    the per-level authorizers, the ownership check (wrong level / another owner's
    visibility → DISSALLOWED), and the sub/super invariant in both directions.
  • Dynamic images, focused on authorization: regular level gates reading, admin level
    gates updating/destroying, IMAGE_ADMIN bypasses both, the paging filter hides
    collections the session may not see, showOnlyCollectionsSessionAdministrates
    filters on the admin level, and special collections stay unreachable through the
    dynamic service.

JohanHjelsethStorstad and others added 30 commits April 16, 2026 11:30
…tter reflect that they are not cms but implementation, just like /services folder does

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

@JohanHjelsethStorstad JohanHjelsethStorstad changed the title Feat/auth image collections Feat/auth image collections [OWTR #1] Jul 27, 2026
@JohanHjelsethStorstad

Copy link
Copy Markdown
Member Author

I suggest someone to user-test this that the visibility authoring makes sense

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.

2 participants