Skip to content

feat(share): add access roles (view, comment, edit), link expiry, password protection, and share revocation (#1527) - #1534

Draft
AyushDubey23 wants to merge 5 commits into
opengeos:mainfrom
AyushDubey23:feat/share-access-roles-hygiene
Draft

feat(share): add access roles (view, comment, edit), link expiry, password protection, and share revocation (#1527)#1534
AyushDubey23 wants to merge 5 commits into
opengeos:mainfrom
AyushDubey23:feat/share-access-roles-hygiene

Conversation

@AyushDubey23

@AyushDubey23 AyushDubey23 commented Jul 29, 2026

Copy link
Copy Markdown

Summary

This PR addresses issue #1527 by extending project sharing on share.geolibre.app beyond basic visibility (unlisted, public, private). It introduces explicit access roles (view, comment, edit), link hygiene options (link expiry and password protection), active share management, and share revocation directly from ShareProjectDialog.


Key Changes

  1. Access Roles (ShareRole):

    • Added support for 3 access roles:
      • view: Read-only access (open, pan/zoom, toggle layer visibility, feature identify popups, export image).
      • comment: View capabilities plus adding & replying to comments.
      • edit: Full application editing capabilities (default).
    • Designed to align with collaboration session participant permission vocabulary ("view" | "comment" | "edit").
    • Added parseShareRole and shareRoleFromLocation helpers to parse role parameters from share URLs (e.g. ?role=view).
  2. Link Hygiene (Expiry & Password Protection):

    • Link Expiry: Optional link expiry (never, 24h, 7d, 30d).
    • Password Protection: Optional password field when creating a share. Per scope requirements, passwords are authenticated server-side (verifySharePassword) and never stored or passed in the URL.
    • Active Share Management & Revocation:
      • fetchProjectShares: Retrieves active share links for the authenticated user.
      • revokeShare: Deletes an existing active share link.
  3. UI Enhancements (ShareProjectDialog.tsx):

    • Added dropdown selectors for Access Role (View, Comment, Edit) and Link Expiry (Never, 24 hours, 7 days, 30 days).
    • Added an optional Password protection input field.
    • Added an Active Shares tab to list active user shares with role badges, expiration info, password protection indicators, and a one-click Revoke action.
  4. I18n & Test Coverage:

    • Added translation keys to en.json under share.*.
    • Updated tests/share-geolibre.test.ts to test uploading with roles/expiry/passwords, listing active shares, revoking shares, and password verification.
    • Updated tests/project-url.test.ts to test role parsing.

Verification

  • Unit Tests: Ran node --import tsx --test tests/share-geolibre.test.ts (28/28 passed) and tests/project-url.test.ts (12/12 passed).
  • Typecheck & Build: Verified via npm run typecheck (tsc -b && vite build) with zero errors.

Closes #1527

Summary by CodeRabbit

  • New Features

    • Share Project dialog now includes a create/manage experience for existing shared links, with copy actions and revocation for active links.
    • Shared link access roles can be set to view, comment, or edit, with configurable expiration (never, 24h, 7d, 30d).
    • Shared links can be password-protected, and access role can be derived from share URLs with password verification.
  • Bug Fixes

    • More reliable loading and refreshed status for active links, including clearer per-action success/error handling during creation, copy, and revoke.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: cd2bdc22-b4c2-4642-a8d4-4e7ea238d738

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Share links now support access roles, expiry options, optional passwords, active-link listing, copying, revocation, and password verification. The share dialog provides create/manage tabs, while URL helpers parse share roles and tests cover the new APIs.

Changes

Share link access and management

Layer / File(s) Summary
Share contracts and access APIs
apps/geolibre-desktop/src/lib/share-geolibre.ts, apps/geolibre-desktop/src/lib/project-url.ts
Adds role and expiry types, configurable upload fields, active-share retrieval, revocation, password verification, and URL role parsing.
Dialog creation and management flow
apps/geolibre-desktop/src/components/layout/ShareProjectDialog.tsx, apps/geolibre-desktop/src/i18n/locales/en.json
Adds role, expiry, and password controls plus active-share listing, URL copying, per-share revocation, loading states, and localized labels.
Role and share workflow validation
tests/project-url.test.ts, tests/share-geolibre.test.ts
Tests valid and invalid role parsing, configured upload requests, share listing, revocation, and password verification behavior.

Estimated code review effort: 4 (Complex) | ~45 minutes

Suggested reviewers: giswqs, craun718

Poem

A rabbit clicks “Share” with a hop,
Adds a password, expiry stop.
Roles bloom: view, comment, edit,
Old links vanish when we quit.
“Copy!” squeaks the bunny bright—
Manage links with carrots light.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant ShareProjectDialog
  participant share-geolibre
  participant ShareAPI
  User->>ShareProjectDialog: Configure role, expiry, and password
  ShareProjectDialog->>share-geolibre: uploadProjectToShare(options)
  share-geolibre->>ShareAPI: POST project share
  ShareAPI-->>share-geolibre: Share result
  share-geolibre-->>ShareProjectDialog: Upload result
  ShareProjectDialog->>share-geolibre: fetchProjectShares(token)
  share-geolibre->>ShareAPI: GET /api/shares
  ShareAPI-->>share-geolibre: Active shares
  share-geolibre-->>ShareProjectDialog: Active share list
  User->>ShareProjectDialog: Revoke a share
  ShareProjectDialog->>share-geolibre: revokeShare(shareId)
  share-geolibre->>ShareAPI: DELETE /api/shares/{shareId}
  ShareAPI-->>ShareProjectDialog: Revocation result
Loading
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR covers roles, expiry, passwords, listing, and revocation, but explicit permission enforcement when opening shared projects is not evident. [#1527] Add enforcement of view/comment/edit permissions when a project opens from a share link, not just role parsing and UI controls.
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: share link roles, expiry, password protection, and revocation.
Out of Scope Changes check ✅ Passed The changes stay focused on share-link access controls, share dialog UX, translations, helpers, and tests; no unrelated scope is apparent.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

🔍 Cloudflare PR preview

Item Value
Site https://3822cf1e.geolibre-preview.pages.dev
Demo app https://3822cf1e.geolibre-preview.pages.dev/demo/
Commit 38aca65

@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

🔍 GitHub Pages PR preview

Item Value
Site https://opengeos.org/pages-preview/GeoLibre/pr-1534/
Demo app https://opengeos.org/pages-preview/GeoLibre/pr-1534/demo/
Commit 38aca65

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/geolibre-desktop/src/components/layout/ShareProjectDialog.tsx`:
- Around line 106-116: Update loadActiveShares to track fetch failures in a
dedicated error state, following the existing revokeError pattern, instead of
setting active shares to an empty array in the catch block. Surface that error
through the dialog’s existing UI while preserving setLoadingShares(false) in
finally and the successful shares flow.
- Around line 443-464: Wire the Manage tab’s existing i18n keys into
ShareProjectDialog: at
apps/geolibre-desktop/src/components/layout/ShareProjectDialog.tsx:443-464, use
t() for passwordProtected, expires, visibility, and role, and pass the active
language to toLocaleDateString(); at :477-489, add the revoke button aria-label
using revokingId, t("share.revoking"), and t("share.revoke").

In `@apps/geolibre-desktop/src/lib/share-geolibre.ts`:
- Around line 272-289: Update the role and visibility fallback values in the
share-item mapping to fail closed: change the unknown or missing role fallback
in the role normalization to "view" and the visibility fallback in visibility
normalization to "private". Preserve the existing accepted-value checks and all
other fields in the returned object.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b92d0e7b-c5ac-4bc4-803e-a8c47cf0b81f

📥 Commits

Reviewing files that changed from the base of the PR and between 874b024 and 14df595.

📒 Files selected for processing (6)
  • apps/geolibre-desktop/src/components/layout/ShareProjectDialog.tsx
  • apps/geolibre-desktop/src/i18n/locales/en.json
  • apps/geolibre-desktop/src/lib/project-url.ts
  • apps/geolibre-desktop/src/lib/share-geolibre.ts
  • tests/project-url.test.ts
  • tests/share-geolibre.test.ts

Comment thread apps/geolibre-desktop/src/components/layout/ShareProjectDialog.tsx Outdated
Comment thread apps/geolibre-desktop/src/components/layout/ShareProjectDialog.tsx
Comment thread apps/geolibre-desktop/src/lib/share-geolibre.ts Outdated
@giswqs

giswqs commented Jul 29, 2026

Copy link
Copy Markdown
Member

/claude-review

@giswqs

giswqs commented Jul 29, 2026

Copy link
Copy Markdown
Member

/claude-review

Comment on lines +18 to +37
import type { ShareRole } from "./share-geolibre";

/**
* Parses a share role string ("view", "comment", "edit") into a valid ShareRole or null.
*/
export function parseShareRole(value: unknown): ShareRole | null {
if (value === "view" || value === "comment" || value === "edit") {
return value;
}
return null;
}

/**
* Reads a share access role from the current `window.location` query string if present (?role=view, ?role=comment, ?role=edit).
*/
export function shareRoleFromLocation(): ShareRole | null {
if (typeof window === "undefined") return null;
const params = new URLSearchParams(window.location.search);
return parseShareRole(params.get("role") || params.get("shareRole"));
}

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.

Quality: the import for ShareRole is inserted mid-file (line 18), sandwiched between a stale duplicate JSDoc block (lines 9-17, which documents projectUrlFromLocation but now sits above parseShareRole/shareRoleFromLocation) and the real projectUrlFromLocation doc comment at line 39. This looks like a copy/paste artifact from adding the new exports. Move the import to the top of the file with the other imports and delete the duplicate comment block.

More significantly (functional, medium confidence): shareRoleFromLocation/parseShareRole are exported here but grepping the repo shows they're never called anywhere outside this file and its tests — not in useProjectUrlLoader.ts, useEmbedApi.ts, or any component that loads a shared project. So even though ShareProjectDialog lets a user pick view/comment/edit when creating a share, nothing in the client actually reads ?role= back out and restricts editing/commenting when a shared link is opened. As written, the "access role" feature only affects what's sent to the server on creation and is purely decorative in the desktop/web app itself.

Suggested change
import type { ShareRole } from "./share-geolibre";
/**
* Parses a share role string ("view", "comment", "edit") into a valid ShareRole or null.
*/
export function parseShareRole(value: unknown): ShareRole | null {
if (value === "view" || value === "comment" || value === "edit") {
return value;
}
return null;
}
/**
* Reads a share access role from the current `window.location` query string if present (?role=view, ?role=comment, ?role=edit).
*/
export function shareRoleFromLocation(): ShareRole | null {
if (typeof window === "undefined") return null;
const params = new URLSearchParams(window.location.search);
return parseShareRole(params.get("role") || params.get("shareRole"));
}
import type { ShareRole } from "./share-geolibre";
/**
* Parses a share role string ("view", "comment", "edit") into a valid ShareRole or null.
*/
export function parseShareRole(value: unknown): ShareRole | null {
if (value === "view" || value === "comment" || value === "edit") {
return value;
}
return null;
}
/**
* Reads a share access role from the current `window.location` query string if present (?role=view, ?role=comment, ?role=edit).
*/
export function shareRoleFromLocation(): ShareRole | null {
if (typeof window === "undefined") return null;
const params = new URLSearchParams(window.location.search);
return parseShareRole(params.get("role") || params.get("shareRole"));
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Import/comment part done in 38aca65 — the ShareRole import moved to the top with the others and the stale duplicate JSDoc block is gone.

The second part is correct and I'm leaving this thread open for @giswqs rather than fixing it here: parseShareRole/shareRoleFromLocation are only referenced by their own tests. Enforcing view/comment on link open needs an app-wide read-only mode that doesn't exist yet — the only "view-only" concept in the repo is collaboration.mode, which is a live-session notion, not a share-link one. So this needs a design decision (new feature vs. dropping the helpers until the enforcement lands), not a review-comment patch.

Comment thread apps/geolibre-desktop/src/lib/share-geolibre.ts Outdated
fetchImpl?: typeof fetch;
}

export async function verifySharePassword(

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.

Bug/incomplete feature (medium-high confidence): verifySharePassword is exported and unit-tested but is never called from anywhere in the app (ShareProjectDialog.tsx, useProjectUrlLoader.ts, useEmbedApi.ts — none reference it). fetchProjectFromUrl/useProjectUrlLoader fetch the raw project JSON directly and have no branch for a 401/403 "password required" response, so there's currently no UI path where a recipient of a password-protected share link is ever prompted to enter the password — they'd just see the generic "could not fetch the project" error from fetchProjectFromUrl. Password protection appears to be wired up only on the creation side (sending password in uploadProjectToShare), not the consumption side.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Confirmed, and left unfixed in 38aca65 on purpose — leaving this thread open for @giswqs.

verifySharePassword really is unreferenced outside its tests, and fetchProjectFromUrl has no 401/403 "password required" branch, so a recipient of a protected link gets the generic fetch error. Wiring the consumption side isn't a minimal fix: the POST <shareUrl>/access contract it assumes has to be confirmed against share.geolibre.app, and useProjectUrlLoader needs a new password-prompt state. Same call as the role helpers (#discussion_r3675757119) — groundwork for a follow-up, or drop it until the server side exists.

Comment thread apps/geolibre-desktop/src/lib/share-geolibre.ts Outdated
Comment thread apps/geolibre-desktop/src/components/layout/ShareProjectDialog.tsx Outdated
Comment thread apps/geolibre-desktop/src/components/layout/ShareProjectDialog.tsx Outdated
Comment thread apps/geolibre-desktop/src/components/layout/ShareProjectDialog.tsx Outdated
@github-actions

Copy link
Copy Markdown
Contributor

All inline comments posted. Here's the final summary.

Code review

Bugs

  • verifySharePassword is fully implemented and unit-tested but never called from any consumption path (useProjectUrlLoader.ts, useEmbedApi.ts, ShareProjectDialog.tsx) — there's no UI flow that prompts for a password when opening a password-protected share link, so the feature is effectively dead client-side code. Medium-high confidence.
  • shareRoleFromLocation/parseShareRole are likewise exported and tested but never consumed anywhere that restricts app behavior — a view/comment role selected at share-creation time isn't enforced when the link is opened. Medium confidence.
  • fetchProjectShares's fallback viewerUrl construction concatenates item.projectUrl into a query string without encodeURIComponent, producing malformed URLs if the raw value contains reserved characters. Medium confidence.
  • No AbortController/signal wired into loadActiveShares, so requests aren't cancelled on dialog close/reopen — risk of post-unmount state updates and out-of-order race conditions overwriting fresher state with stale data. Medium confidence.

Security

  • verifySharePassword sends the password redundantly in both the JSON body and a custom X-Share-Password header — unnecessary duplication of a secret across two channels. Low confidence, minor.

Performance

  • None found.

Quality

  • project-url.ts has a misplaced import statement stuck between a stale duplicate JSDoc block and the real one for projectUrlFromLocation — looks like a copy/paste artifact; should move the import to the top and remove the duplicate comment. Medium confidence.
  • Revoke is a destructive, irreversible action triggered by a single click with no confirmation step. Medium confidence.
  • The icon-only Revoke button has no aria-label/title, unlike the adjacent Copy button. Medium confidence.
  • fetchProjectShares's .map((item: any) => ...) opts out of type checking; unknown plus the existing manual guards would be safer. Low confidence.
  • Several previously-existing explanatory "why" comments in ShareProjectDialog.tsx (re-entry guard, abort-controller currency check, username-required routing, clipboard-copy rationale) were deleted during this refactor even though the underlying logic they explained is unchanged — reduces maintainability. Low-medium confidence.

CLAUDE.md

  • New user-facing strings "Password" and "Expires " in the Active Shares list are hardcoded rather than run through t(), violating this repo's i18n convention. en.json even defines share.passwordProtected and share.expires for this exact purpose in this same PR, but they're never referenced. High confidence.

@rickgonzalez

Copy link
Copy Markdown

I did a PR explainer for this during Lenzon testing - I hope it's helpful in some way: https://www.lenzon.ai/viewer/cms6rx1w8000ak9ceg476dy1h?voice=google-chirp3

- ShareProjectDialog: surface a dedicated `sharesError` instead of
  swallowing a failed share fetch into the "no active shares" empty
  state, which hid an expired token or dropped connection from the user.
- ShareProjectDialog: give `loadActiveShares` its own AbortController so
  a close/reopen or token edit cancels the in-flight request; a
  superseded response can no longer overwrite a newer list or write
  state after the dialog is gone.
- ShareProjectDialog: wire the Manage tab through i18n — the previously
  unused `share.passwordProtected` / `share.expires` keys, new short
  visibility/role labels instead of rendering the raw enum with
  `capitalize`, and a locale-aware expiry date via `i18n.language`.
- ShareProjectDialog: give the icon-only revoke button an accessible
  name (`share.revoke` / `share.revoking`) and a `window.confirm` step,
  since revoking is immediate and irreversible.
- ShareProjectDialog: restore the explanatory comments dropped in the
  refactor (re-entry guard, current-controller check, username-required
  routing, clipboard-copy rationale) — the logic they document is
  unchanged.
- share-geolibre: fail closed to the `view` role when the server sends
  an unknown or missing one, instead of defaulting to full `edit`.
- share-geolibre: percent-encode the project URL in the fallback viewer
  link so a raw `&`/`#` cannot truncate it, and encode the slug path
  segment; replace `item: any` in the mapper with `unknown` narrowed to
  `Record<string, unknown>`.
- share-geolibre: stop duplicating the share password into a custom
  `X-Share-Password` header; the JSON body alone is enough and headers
  are captured separately by proxy/logging layers.
- Tests for the fail-closed role and the encoded fallback viewer URL.
- project-url: hoist the `ShareRole` import to the top and delete the
  stale duplicate JSDoc block left above `parseShareRole`.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
apps/geolibre-desktop/src/components/layout/ShareProjectDialog.tsx (1)

557-566: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Per-item Copy button in the Manage tab has no aria-label, unlike its sibling revoke button.

The revoke button right next to it gets both aria-label and title (Line 571-574), and the top-level share-result copy button also uses aria-label={t("share.copyLink")} (Line 314). This per-share Copy button only has title={t("share.copyLink")}, which is a weaker accessible-name signal for screen readers on an icon-only control (<Copy className="h-3.5 w-3.5" />).

♿ Suggested fix
                           <Button
                             type="button"
                             variant="secondary"
                             size="sm"
+                            aria-label={t("share.copyLink")}
                             title={t("share.copyLink")}
                             onClick={() => handleCopy(s.projectUrl)}
                           >
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/geolibre-desktop/src/components/layout/ShareProjectDialog.tsx` around
lines 557 - 566, Add aria-label={t("share.copyLink")} to the per-item Copy
Button in the Manage tab, matching the existing top-level share-result copy
button and sibling revoke button while preserving the current title and click
behavior.
apps/geolibre-desktop/src/lib/share-geolibre.ts (2)

386-390: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

verifySharePassword returns the server's role unchecked, unlike fetchProjectShares's fail-closed normalization.

fetchProjectShares explicitly normalizes an unknown/missing role to "view" const role: ShareRole = item.role === "view" || item.role === "comment" || item.role === "edit" ? item.role : "view";, but verifySharePassword type-asserts the response and returns data.role verbatim without the same check: role: data.role. The as { content?: string; role?: ShareRole } cast doesn't validate at runtime — a server (or a compromised/misbehaving proxy) sending role: "owner" would flow through untouched as a ShareRole. This function is currently unwired to any consumer per the earlier open thread, but if/when its role is used to gate permissions on link-open, this fail-open path would undermine the exact protection just added on the listing side.

Extract the normalization into a shared helper (e.g. normalizeShareRole(value: unknown): ShareRole) and reuse it in both fetchProjectShares and here.

🔒 Suggested fix
+function normalizeShareRole(value: unknown): ShareRole {
+  return value === "view" || value === "comment" || value === "edit" ? value : "view";
+}
+
 export async function fetchProjectShares(options: FetchSharesOptions): Promise<ActiveShare[]> {
   ...
-      const role: ShareRole =
-        item.role === "view" || item.role === "comment" || item.role === "edit"
-          ? item.role
-          : "view";
+      const role = normalizeShareRole(item.role);
   ...
 }

 export async function verifySharePassword(...) {
   ...
   const data = (await response.json()) as { content?: string; role?: unknown };
   return {
     projectContent: typeof data.content === "string" ? data.content : JSON.stringify(data),
-    role: data.role,
+    role: data.role === undefined ? undefined : normalizeShareRole(data.role),
   };
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/geolibre-desktop/src/lib/share-geolibre.ts` around lines 386 - 390,
Extract the existing fail-closed role validation into a shared
normalizeShareRole helper that accepts unknown values and returns "view" unless
the value is "view", "comment", or "edit". Update both fetchProjectShares and
verifySharePassword to use this helper, replacing the unchecked data.role return
while preserving their existing response behavior.

357-357: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use getShareFetch() for password verification unless caller provides fetchImpl.

UploadProjectToShare, fetchProjectShares, and revokeShare fall back to the shared getShareFetch() transport, whose desktop build overrides fetch to avoid WebView CORS issues. verifySharePassword skips that fallback and uses bare fetch; use options.fetchImpl ?? getShareFetch() unless the shareUrl is intentionally meant for browser fetch only.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/geolibre-desktop/src/lib/share-geolibre.ts` at line 357, Update
verifySharePassword’s fetch implementation to use the caller-provided
options.fetchImpl when present, otherwise fall back to getShareFetch() instead
of bare fetch. Preserve the existing password-verification behavior and URL
handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@apps/geolibre-desktop/src/components/layout/ShareProjectDialog.tsx`:
- Around line 557-566: Add aria-label={t("share.copyLink")} to the per-item Copy
Button in the Manage tab, matching the existing top-level share-result copy
button and sibling revoke button while preserving the current title and click
behavior.

In `@apps/geolibre-desktop/src/lib/share-geolibre.ts`:
- Around line 386-390: Extract the existing fail-closed role validation into a
shared normalizeShareRole helper that accepts unknown values and returns "view"
unless the value is "view", "comment", or "edit". Update both fetchProjectShares
and verifySharePassword to use this helper, replacing the unchecked data.role
return while preserving their existing response behavior.
- Line 357: Update verifySharePassword’s fetch implementation to use the
caller-provided options.fetchImpl when present, otherwise fall back to
getShareFetch() instead of bare fetch. Preserve the existing
password-verification behavior and URL handling.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: bbd29507-da22-4dd5-b865-0d180e4e0c00

📥 Commits

Reviewing files that changed from the base of the PR and between 835b50a and 38aca65.

📒 Files selected for processing (5)
  • apps/geolibre-desktop/src/components/layout/ShareProjectDialog.tsx
  • apps/geolibre-desktop/src/i18n/locales/en.json
  • apps/geolibre-desktop/src/lib/project-url.ts
  • apps/geolibre-desktop/src/lib/share-geolibre.ts
  • tests/share-geolibre.test.ts

@giswqs

giswqs commented Jul 30, 2026

Copy link
Copy Markdown
Member

The server side of this feature does not exist yet

I pushed fixes for all the inline review comments in 38aca65, but while doing so I checked whether the endpoints this PR calls actually exist on share.geolibre.app. Most of them don't. Flagging it here because it changes how this PR should be sequenced, not because anything is wrong with the client code — the UI and the client helpers are reasonable.

The share backend isn't in this repo (workers/ has ai-proxy, collab, tiles, and viewer, no share service), so I probed the live API directly:

# Control: a route that exists rejects a bad token with 401
curl -s -o /dev/null -w "%{http_code}\n" -X POST \
  -H "Authorization: Bearer glb_bogus_probe" -H "Content-Type: application/json" \
  -d '{"filename":"x.geolibre.json","content":"{}","visibility":"unlisted"}' \
  https://share.geolibre.app/api/projects
# -> 401 {"error":"Unauthorized"}

curl -s -o /dev/null -w "%{http_code}\n" -H "Authorization: Bearer glb_bogus_probe" \
  https://share.geolibre.app/api/shares                        # -> 404
curl -s -o /dev/null -w "%{http_code}\n" -X DELETE -H "Authorization: Bearer glb_bogus_probe" \
  https://share.geolibre.app/api/shares/xyz                    # -> 404
curl -s -o /dev/null -w "%{http_code}\n" -X POST -H "Content-Type: application/json" \
  -d '{"password":"x"}' https://share.geolibre.app/tlo/lidar/access   # -> 404
Endpoint this PR calls Bogus bearer token Verdict
POST /api/projects (pre-existing) 401 Unauthorized route exists — control
GET /api/shares 404 Not Found route absent
DELETE /api/shares/:id 404 Not Found route absent
POST /<user>/<slug>/access 404 Not Found route absent

The 401-vs-404 contrast is what makes this conclusive: the auth layer answers 401 for routes that exist, so a 404 means there is no route, not that auth ran first. GET /api/projects also confirms the project DTO carries no role, expiresAt, or hasPassword fields today.

What that means feature by feature

1. Active Shares tab — errors in production. fetchProjectShares gets a 404 and throws Failed to fetch shares (HTTP 404). Before 38aca65 this was swallowed and rendered as "No active share links found.", which is why it looked like it worked. My fix surfaces the error, so the breakage is now visible rather than repaired.

2. Revoke — reports success while the link stays live. This is the one I'd treat as blocking:

if (!response.ok && response.status !== 404) {
  throw new Error(`Failed to revoke share (HTTP ${response.status}).`);
}

With the route absent, the DELETE 404s, revokeShare resolves normally, and the row is optimistically removed from the list. The user sees a clean success and believes the link is dead while it is still live and serving their project. The 404 tolerance was presumably meant for idempotency, but with no route deployed it converts "not implemented" into "done" — and the window.confirm step I added in 38aca65 makes that false confirmation feel more authoritative. I have deliberately not changed this line yet, since the right fix depends on the sequencing decision below.

3. Password protection — not wired at either end. /access 404s, and verifySharePassword is never called from any consumption path anyway (see the open thread on share-geolibre.ts).

4. Create path — the open risk to functionality that works today. The PR adds role, expiresIn, and password as extra keys in the POST /api/projects body. Two possibilities, and I can't tell them apart from outside because 401 fires before body validation:

  • the handler validates strictly and rejects unknown keys → sharing breaks for every user;
  • the handler ignores unknown keys → sharing still works, but every new setting is silently discarded, and result.role / result.expiresAt / result.hasPassword are always undefined because the response has no such fields.

This is the blocking question for the PR. Someone with access to the share.geolibre.app source or a valid API token should confirm which it is before this merges, because one branch is a regression of a shipping feature.

Suggested sequencing

  1. Answer the POST /api/projects strict-validation question first.
  2. Land the server side (GET /api/shares, DELETE /api/shares/:id, password issue/verify, and the role/expiresAt/hasPassword fields on the project DTO) before or alongside this client PR.
  3. Until then, either hold this PR or gate the new UI behind a capability probe so the Manage tab hides itself instead of erroring, and drop the create-tab controls for settings the server discards.
  4. Change the revoke 404 handling to treat 404 as a failure, so it can never manufacture confidence that a link is dead.

To be clear about how this got missed: three automated reviewers (Copilot, CodeRabbit, and the Claude review bot) went several rounds on this PR and all of us reviewed the client code against itself. The two "exported but never called" findings were the visible edge of it; none of us checked the endpoints against the deployed API. The client-side work here is solid — it just needs the backend to exist.

@giswqs
giswqs marked this pull request as draft July 30, 2026 02:08
@AyushDubey23

AyushDubey23 commented Jul 30, 2026

Copy link
Copy Markdown
Author

Thanks @giswqs for probing the backend endpoints and catching the 401 vs 404 contrast on share.geolibre.app!

I've pushed commit d047b00 with the client-side fixes:

  • Revoke 404 handling: revokeShare no longer swallows 404 responses. If the deletion route is missing, it now explicitly throws an HTTP 404 error (Failed to revoke share (HTTP 404)) instead of falsely reporting link revocation.
  • Fail-closed role normalization: Added normalizeShareRole() to ensure any missing or unrecognized role defaults safely to "view".
  • Accessibility & Transport: Added aria-label to the Manage tab copy button and updated verifySharePassword transport to use getShareFetch().

I completely agree with your suggested sequencing — we can hold/coordinate merging this client PR alongside the server-side endpoint deployment on share.geolibre.app.

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.

Share links: access roles, expiry, password, and revoke

3 participants