1
Fork 0
mirror of https://github.com/thegeneralist01/archivr synced 2026-10-09 12:55:00 +02:00

feat(collections): public collections, per-collection auth, UX improvements (#34)

* feat(core): add requires_auth to collections; include name in entry-collection memberships

- Add `requires_auth INTEGER NOT NULL DEFAULT 1` column to the
  collections DDL and as an idempotent ALTER TABLE migration in
  initialize_schema (archive DB), not initialize_auth_schema.
- CollectionRecord and CollectionSummary gain `requires_auth: bool`.
- create_collection() and update_collection() accept the new field.
- get_entry_collection_memberships() now returns collection name as the
  third tuple element; EntryCollectionMembership gains a `name` field
  so the sidebar can show human-readable names instead of raw UIDs.

* feat(server): conditional auth for public collections; add requires_auth + original_url to API

- CreateCollectionBody gains requires_auth (default true).
- PatchCollectionBody gains requires_auth: Option<bool>.
- get_collection_handler: load record first, then skip auth.require_auth()
  when record.requires_auth == false so public collections are accessible
  to unauthenticated callers; caller_bits falls back to ROLE_GUEST (1)
  so only visibility_bits=3 entries are returned to guests.
- Collection JSON response includes requires_auth and each entry now
  includes original_url for use by the public collection page.
- list_collections_handler keeps require_auth (management UI).

* feat(frontend): public collection page at /c/:archiveId/:collUid

- Detect PUBLIC_COLL_ROUTE at module load time (like PREVIEW_ROUTE) and
  return <PublicCollectionPage> before any auth checks so unauthenticated
  users can view public collections without hitting the login gate.
- PublicCollectionPage fetches via getCollection() and renders the
  server-filtered entry list (no client-side bitmask filtering - the
  server already applies caller_bits=GUEST for unauthenticated requests).
  Entry titles link to original_url when present; fall back to plain
  text when original_url is null.
- api.js createCollection() gains requiresAuth param (default true),
  sent as requires_auth in the request body.
- Storybook story covers WithEntries, Empty, and LoadError states.

* feat(frontend): collections view improvements

- addVis in the 'Add entry' form now syncs to the selected collection's
  default_visibility_bits via useEffect on collDetail, so the default
  matches the collection's configured entry visibility.
- Rename 'Default visibility' label to 'Entries\' default visibility'
  in both the detail pane and the create form to distinguish it from
  the new collection-level access setting.
- Add 'Require authentication to view' checkbox in the detail pane
  backed by a PATCH to requires_auth; reads collDetail?.requires_auth
  with fallback to the list-level selected record.
- Create form gains a matching requires_auth checkbox (default: true),
  passed as 5th arg to createCollection().

* feat(frontend): context rail collection improvements

- Show collection name (c.name) instead of raw UID in the sidebar
  Collections section; names now come from the updated
  EntryCollectionMembership API response.
- Fix horizontal overflow on long collection names: coll-name gains
  overflow:hidden + text-overflow:ellipsis + white-space:nowrap +
  min-width:0; coll-row gets overflow:hidden.
- Single-entry 'Add to collection' UI: dropdown + button inside the
  Collections rail section lets users add the current entry to any
  non-default collection without multi-selecting. After add, the
  membership list refreshes automatically.
- Collections section now shows even when entryCollections is empty,
  as long as non-default collections exist (so the add form is
  accessible for un-membered entries).
- Bulk 'Add to collection' now uses the target collection's
  default_visibility_bits instead of hardcoded 2 (Users only).
- Both bulk and single-entry dropdowns filter out slug='_default_'
  to match the backend rejection in add_entry_to_collection_handler.
- Collections list is now fetched on archiveId change (not just on
  bulk mode entry) so it is available for single-entry mode too.

* build(frontend): update static assets

* feat(frontend): public collection link UX + app-styled public page

CollectionsView:
- When a collection has requires_auth=false, show a read-only URL input
  and Copy button below the auth checkbox so the public link is
  immediately discoverable. The input auto-selects on focus so manual
  copy always works. Copy button tries navigator.clipboard.writeText
  first; falls back to execCommand('copy') for HTTP deployments where
  the Clipboard API is unavailable in non-secure contexts.

PublicCollectionPage:
- Rewritten to use the app's CSS classes and variables instead of
  bare inline styles, so it visually matches the main archive UI.
  Dark topbar (.pub-coll-topbar) with brand + collection name, paper
  background body, entry list via .coll-entries-list / .coll-entry-row /
  .coll-entry-info / .coll-entry-kind — the same classes used in the
  authenticated Collections view.

styles.css:
- .coll-public-link-row / -wrap / -input / .coll-copy-btn for the
  new link field in CollectionsView detail pane.
- .pub-coll-* classes for the public page layout and typography.

* feat(core): add get_collection_by_slug; scope search to active collection

- get_collection_by_slug(): new function mirroring get_collection_by_uid
  but matching on slug, used to resolve the _default_ collection when no
  ?collection param is supplied.
- SearchEntriesQuery gains collection_id: Option<i64>. When set, the
  search SQL adds an EXISTS subquery that checks collection_entries cef
  for both membership (cef.collection_id = ?) and visibility bits in
  that specific collection — preventing cross-collection visibility
  leaks where an entry is public in one collection but private in the
  current one. Without collection_id the original cross-collection
  visibility fallback is kept.

* feat(server): collection-scoped entries/search with uniform auth gate

All entry listing and search now route through the active collection:

list_entries (?collection=<uid>|main|<omitted>):
- Resolves the target collection; omitted or 'main' resolves to _default_.
- Checks requires_auth on that collection; gates auth conditionally.
- Returns list_entries_for_collection() — same EntrySummary shape.

search_entries_handler:
- Same collection resolution + conditional auth as list_entries.
- Sets search_query.collection_id so SQL scopes membership + visibility
  to the specific collection, not cross-collection fallback.

list_collections_handler:
- Dropped require_auth() — collection summaries (name/slug/uid/
  requires_auth/default_visibility_bits) are public metadata needed for
  the guest collection-switcher dropdown.

Tests:
- list_collections_requires_auth → list_collections_is_public (200).
- list_entries_requires_auth and search coverage still pass.

* feat(frontend): integrate collection switching into main Archive view

Replaces the standalone /c/:archiveId/:collUid public page with a
unified main-view approach where all collection logic lives at /.

URL param:
- ?collection=<uid> selects a collection; omitted or 'main' = default.
- 'main' is normalized to null in parseLocation() so the dropdown shows
  'All entries' and the URL stays clean.

Collection switcher (Topbar):
- Dropdown always visible (guests need it to navigate public collections).
- Non-default collections only (All entries = no param = _default_).
- Guest selecting an auth-required collection calls onSignInClick().
- handleCollectionChange checks both named and _default_ requires_auth
  before proceeding, redirecting guests to login if needed.

listCollections fetched for all users (guests too) since the endpoint
is now public; used to populate the switcher without auth.

Public-session mode (authenticated state, no currentUser):
- Auth gate: fetchArchives() + fetchEntries() with collection param;
  401 falls through to login, 200 proceeds as guest.
- auth:expired suppressed when !currentUser.
- fetchEntryDetail skipped; ContextRail shows entry summary + sign-in prompt.
- ContextRail selection effect skips tag/collection API calls.
- runs/tags not fetched in guest mode.
- Child row expansion disabled in EntryRow (hasChildren = false).

api.js:
- fetchEntries/searchEntries both thread ?collection=<uid> to server.

Deleted: PublicCollectionPage.jsx, PublicCollectionPage.stories.jsx,
copy-link UI from CollectionsView, pub-coll-*/copy-link CSS.

* build(frontend): update static assets

* feat(core): add is_entry_publicly_accessible; checks entry+parent vs public collections

* feat(server): allow guests to fetch detail/children/artifacts for public entries

* feat(frontend): guest collection dropdown filtering; public entry detail without auth wall

* build(frontend): update static assets

* test(server): public entry detail/artifact/children contract for guests

* feat(server): filter auth-required collections from guest list_collections response
This commit is contained in:
TheGeneralist 2026-07-24 20:16:17 +02:00 • committed by GitHub
parent 1af920eb63
commit e1ee05bd41
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 1218 additions and 185 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -4,8 +4,8 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Archivr</title>
<script type="module" crossorigin src="/assets/index-D_BVhPjQ.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-5jBL9-i7.css">
<script type="module" crossorigin src="/assets/index-B67momER.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DicH9MNh.css">
</head>
<body>
<div id="root"></div>