Skip to content

feat: project-level share links#21

Open
Engm4nn wants to merge 5 commits intopingdotgg:mainfrom
Engm4nn:feature/project-share-links
Open

feat: project-level share links#21
Engm4nn wants to merge 5 commits intopingdotgg:mainfrom
Engm4nn:feature/project-share-links

Conversation

@Engm4nn
Copy link

@Engm4nn Engm4nn commented Mar 9, 2026

Summary

  • Adds a single shareable URL per project (/project-share/$token) so teams can share all videos at once without sending individual links
  • Public video grid at /project-share/$token — no auth required, read-only
  • Public video player at /project-share/$token/$videoId — Mux playback + read-only comment thread
  • "Share" button on the project page (members only) with copy + revoke controls

Changes

Backend (Convex)

  • schema.ts: shareToken field + by_share_token index on projects table
  • projects.ts: generateShareToken, revokeShareToken, getByShareToken
  • videos.ts: listForProjectShare, getForProjectShare (unauthenticated queries)
  • comments.ts: getThreadedForProjectShare (unauthenticated query)
  • videoActions.ts: getProjectSharePlaybackSession action

Frontend

  • New route tree: project-share.$token → layout, .index → video grid, .$videoId → player
  • Share button in project header with inline popover (URL input, Copy, Revoke)
  • Matches existing brutalist design system

Test plan

  • Project member clicks "Share" → token generated, URL shown in popover
  • Copy button copies the full URL to clipboard
  • Open URL in incognito → project video grid renders without login
  • Click a video → Mux player loads, comments shown (read-only)
  • Revoke → incognito URL shows "not found"
  • Viewer role → Share button not shown

🤖 Generated with Claude Code

Note

Add project-level share links by introducing public /project-share/:token and /project-share/:token/:videoId routes with dashboard controls to generate and revoke tokens in convex/projects.ts

Add public project and video pages under /project-share/:token with video playback and comments, route registrations, and a dashboard share panel with copy and revoke actions. Implement token-backed Convex queries and mutations for projects, videos, comments, and a playback session action, and extend the schema with shareToken and a by_share_token index.

📍Where to Start

Start with token generation and validation in projects.generateShareToken, projects.getByShareToken, and related queries in convex/projects.ts, then follow public access checks through convex/videos.ts and convex/videoActions.ts, and finally review route wiring in app/routes/project-share.$token.index.tsx and app/routes/project-share.$token.$videoId.tsx.

Macroscope summarized 06aa161.

Summary by CodeRabbit

  • New Features
    • Generate, view, copy, and revoke shareable project links via a dashboard Share panel.
    • Public shared project view with responsive video gallery, metadata, thumbnails, and duration badges.
    • Per-video public playback page with playback sessions, poster/thumbnail fallback, and threaded comments with quick-seek timestamps.
    • Comment loading and playback session handling with loading, not-found, and error states.

Adds a single shareable URL per project so teams can share all videos
at once without sending individual links for every video.

- convex/schema.ts: add shareToken field + by_share_token index to projects
- convex/projects.ts: add generateShareToken, revokeShareToken, getByShareToken
- convex/videos.ts: add listForProjectShare, getForProjectShare (public queries)
- convex/comments.ts: add getThreadedForProjectShare (public query)
- convex/videoActions.ts: add getProjectSharePlaybackSession action
- src/lib/routes.ts: add projectSharePath, projectShareVideoPath helpers
- app/routes/project-share.$token.*: new public route tree
- app/routes/-project-share.tsx: read-only video grid component
- app/routes/-project-share-video.tsx: read-only video player + comments
- app/routes/dashboard/-project.tsx: add Share button with popover UI

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vercel
Copy link

vercel bot commented Mar 9, 2026

Someone is attempting to deploy a commit to the Ping Labs Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link

coderabbitai bot commented Mar 9, 2026

Walkthrough

Adds token-based project sharing: DB schema and Convex APIs for tokens and public access; new routes, pages, and route-tree entries for shared project and shared-video views; dashboard UI to generate/revoke/copy share URLs; playback/comment endpoints and route helpers.

Changes

Cohort / File(s) Summary
Route Tree & Runtime
app/routeTree.gen.ts
Regenerated route tree: added /project-share/$token, /project-share/$token/, /project-share/$token/$videoId routes, types, mappings, and wiring into root children and router augmentations.
Route Files
app/routes/project-share.$token.tsx, app/routes/project-share.$token.index.tsx, app/routes/project-share.$token.$videoId.tsx
New file-route definitions using createFileRoute for layout, index (shared project), and per-video pages with SEO head and component wiring.
Share Pages / Components
app/routes/-project-share.tsx, app/routes/-project-share-video.tsx
New pages: ProjectSharePage lists shared project videos; ProjectShareVideoPage fetches video, threaded comments, playback session, and renders player with comment markers and metadata.
Dashboard Integration
app/routes/dashboard/-project.tsx
Adds Share button and panel, uses getShareToken/generateShareToken/revokeShareToken Convex calls, copy/revoke UI and outside-click dismissal.
Convex Schema
convex/schema.ts
Added optional shareToken field to projects table and by_share_token index.
Convex Projects API
convex/projects.ts
Adds getShareToken, generateShareToken, revokeShareToken, getByShareToken; strips shareToken from normal list/get responses.
Convex Videos & Actions
convex/videos.ts, convex/videoActions.ts
Adds listForProjectShare and getForProjectShare queries and getProjectSharePlaybackSession action to return public playback session for shared videos.
Convex Comments
convex/comments.ts
Adds getThreadedForProjectShare query to return threaded public comments for a share token + videoId.
Client Route Helpers
src/lib/routes.ts
Adds projectSharePath(token) and projectShareVideoPath(token, videoId) helpers.

Sequence Diagram(s)

sequenceDiagram
    participant User as User / Client
    participant Dashboard as Dashboard UI
    participant Router as Router / Shared Pages
    participant Convex as Convex Backend
    participant DB as Database

    rect rgba(100,200,100,0.5)
    Note over User,DB: Share Token Generation Flow
    User->>Dashboard: Click "Share"
    Dashboard->>Convex: generateShareToken(projectId)
    Convex->>DB: validate access & persist unique token
    DB-->>Convex: token stored
    Convex-->>Dashboard: return token
    Dashboard->>User: display share URL
    end

    rect rgba(100,150,200,0.5)
    Note over User,DB: Shared Project / Video View Flow
    User->>Router: GET /project-share/{token}
    Router->>Convex: getByShareToken(token)
    Convex->>DB: lookup project by shareToken
    DB-->>Convex: return project info
    Convex-->>Router: project DTO
    Router->>User: render project page

    User->>Router: open video -> /project-share/{token}/{videoId}
    Router->>Convex: getForProjectShare(token, videoId)
    Router->>Convex: getThreadedForProjectShare(token, videoId)
    Convex->>DB: fetch video & comments
    DB-->>Convex: return video+comments
    Convex-->>Router: data
    Router->>Convex: getProjectSharePlaybackSession(token, videoId)
    Convex->>Convex: ensure public playback id & build session
    Convex-->>Router: playback session
    Router->>User: render player with markers
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: project-level share links' directly and clearly summarizes the main change: introducing shareable project URLs with read-only public views.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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 and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
app/routes/dashboard/-project.tsx (1)

236-251: Consider adding user feedback for token generation failure.

When generateProjectShare fails, only console.error is called. The user receives no visual feedback that the share operation failed.

♻️ Proposed improvement to add error toast
   const handleShareProject = useCallback(async () => {
     if (!resolvedProjectId) return;
     if (project?.shareToken) {
       setShowSharePanel(true);
       return;
     }
     setIsGeneratingToken(true);
     try {
       await generateProjectShare({ projectId: resolvedProjectId });
       setShowSharePanel(true);
     } catch (error) {
       console.error("Failed to generate share token:", error);
+      showShareToast("error", "Could not generate share link");
     } finally {
       setIsGeneratingToken(false);
     }
-  }, [project?.shareToken, resolvedProjectId, generateProjectShare]);
+  }, [project?.shareToken, resolvedProjectId, generateProjectShare, showShareToast]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/routes/dashboard/-project.tsx` around lines 236 - 251, The catch block in
handleShareProject currently only logs errors to the console so users get no
feedback when generateProjectShare fails; update the catch to show a user-facing
error (for example call your app's toast/error notification helper or set an
error state) and ensure setIsGeneratingToken(false) still runs in finally;
modify handleShareProject to call the notification helper (e.g., toast.error or
showError) with a clear message like "Failed to generate share token" and
include the error details, while keeping references to generateProjectShare,
setShowSharePanel, and setIsGeneratingToken intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/routes/dashboard/-project.tsx`:
- Around line 386-396: Wrap the await revokeProjectShare call in a try/catch
inside the onClick handler for the Revoke link button (referencing
revokeProjectShare, resolvedProjectId, and setShowSharePanel) so failures don’t
silently close the panel; on success keep the current behavior
(setShowSharePanel(false)), on failure keep the panel open and surface the error
to the user by invoking your app’s standard error notification (e.g.,
toast/flash/error handler) and optionally set a local loading state to disable
the button while the mutation is running.

In `@convex/projects.ts`:
- Around line 126-143: The current generateShareToken persists shareToken onto
the projects record (see generateShareToken which calls
ctx.db.patch(args.projectId, { shareToken: token })), which causes existing
project list/get handlers in this file to leak the token to viewers; instead
stop storing the bearer token on the generic project payloads or ensure those
handlers never return it: remove or avoid writing shareToken into the public
projects document and implement a dedicated, member-only read endpoint (e.g.,
getProjectShareToken) that calls requireProjectAccess(ctx, projectId, "member")
and returns the token from a secure store (or from a separate table/field only
accessible via this endpoint) so only members can retrieve it; also update
generateShareToken to persist the token to that secure location (not the public
project fields) and keep list/get handlers unchanged or explicitly redact
shareToken before returning.

In `@convex/videoActions.ts`:
- Around line 490-516: Add an optional shareTokenExpiresAt timestamp to the
projects schema and update the getForProjectShare query and the
getProjectSharePlaybackSession action to verify the token is not expired
(compare now to shareTokenExpiresAt) similar to resolveActiveShareGrant for
video shares; ensure revokeShareToken behavior remains compatible (it can still
clear the token) and add migration/validation so existing tokens without
shareTokenExpiresAt are treated as non-expiring only if you intend that,
otherwise require an expiry when creating a project share token.

In `@convex/videos.ts`:
- Around line 503-505: Public share endpoints treat URL path segments as
untrusted strings; replace the v.id("videos") param with v.string() in
getForProjectShare, getProjectSharePlaybackSession, and
getThreadedForProjectShare, then call ctx.db.normalizeId("videos",
providedVideoId) inside each handler and bail out (return null / not-found) if
normalizeId returns falsy before any DB queries or validation that assumes a
Convex ID; ensure subsequent code uses the normalized ID variable when querying.

---

Nitpick comments:
In `@app/routes/dashboard/-project.tsx`:
- Around line 236-251: The catch block in handleShareProject currently only logs
errors to the console so users get no feedback when generateProjectShare fails;
update the catch to show a user-facing error (for example call your app's
toast/error notification helper or set an error state) and ensure
setIsGeneratingToken(false) still runs in finally; modify handleShareProject to
call the notification helper (e.g., toast.error or showError) with a clear
message like "Failed to generate share token" and include the error details,
while keeping references to generateProjectShare, setShowSharePanel, and
setIsGeneratingToken intact.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f91a393c-6ea7-430b-86ad-7774ce54ba75

📥 Commits

Reviewing files that changed from the base of the PR and between 046c2b2 and 5d1a481.

📒 Files selected for processing (13)
  • app/routeTree.gen.ts
  • app/routes/-project-share-video.tsx
  • app/routes/-project-share.tsx
  • app/routes/dashboard/-project.tsx
  • app/routes/project-share.$token.$videoId.tsx
  • app/routes/project-share.$token.index.tsx
  • app/routes/project-share.$token.tsx
  • convex/comments.ts
  • convex/projects.ts
  • convex/schema.ts
  • convex/videoActions.ts
  • convex/videos.ts
  • src/lib/routes.ts

Comment on lines +490 to +516
export const getProjectSharePlaybackSession = action({
args: { shareToken: v.string(), videoId: v.id("videos") },
returns: v.object({
url: v.string(),
posterUrl: v.string(),
}),
handler: async (
ctx,
args,
): Promise<{ url: string; posterUrl: string }> => {
const result = await ctx.runQuery(api.videos.getForProjectShare, {
shareToken: args.shareToken,
videoId: args.videoId,
});

if (!result?.muxPlaybackId) {
throw new Error("Video not found or not ready");
}

const playbackId = await ensurePublicPlaybackId(ctx, {
videoId: args.videoId,
muxAssetId: result.muxAssetId,
muxPlaybackId: result.muxPlaybackId,
});
return buildPublicPlaybackSession(playbackId);
},
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the schema for projects table - check if expiry field exists
rg -n "shareToken" convex/schema.ts -A 3 -B 3

Repository: pingdotgg/lawn

Length of output: 360


🏁 Script executed:

#!/bin/bash
# Get the full projects schema definition
sed -n '59,70p' convex/schema.ts

Repository: pingdotgg/lawn

Length of output: 357


🏁 Script executed:

#!/bin/bash
# Search for getForProjectShare implementation to verify the validation logic
rg -n "getForProjectShare" convex/videos.ts -A 25 -B 2

Repository: pingdotgg/lawn

Length of output: 985


🏁 Script executed:

#!/bin/bash
# Search for getByShareGrant to compare how it handles expiry
rg -n "getByShareGrant" convex/videos.ts -A 15 -B 2

Repository: pingdotgg/lawn

Length of output: 552


🏁 Script executed:

#!/bin/bash
# Look for resolveActiveShareGrant to understand share grant expiry validation
rg -n "resolveActiveShareGrant" -A 10

Repository: pingdotgg/lawn

Length of output: 3323


🏁 Script executed:

#!/bin/bash
# Get the full resolveActiveShareGrant implementation
sed -n '61,90p' convex/shareAccess.ts

Repository: pingdotgg/lawn

Length of output: 683


🏁 Script executed:

#!/bin/bash
# Check shareAccessGrants schema to see if it has expiry field
rg -n "shareAccessGrants" convex/schema.ts -A 8 -B 2

Repository: pingdotgg/lawn

Length of output: 377


🏁 Script executed:

#!/bin/bash
# Check shareLinks schema
rg -n "shareLinks" convex/schema.ts -A 8 -B 2

Repository: pingdotgg/lawn

Length of output: 719


🏁 Script executed:

#!/bin/bash
# Check if there's any deletion/revocation mechanism for project share tokens
rg -n "shareToken" convex/videoActions.ts -B 5 -A 5

Repository: pingdotgg/lawn

Length of output: 719


🏁 Script executed:

#!/bin/bash
# Check if there's any management query/mutation for revoking project shares
rg -n "shareToken" convex/ -l

Repository: pingdotgg/lawn

Length of output: 176


🏁 Script executed:

#!/bin/bash
# Check projects.ts for any mutations or functions related to shareToken management
rg -n "shareToken" convex/projects.ts -B 5 -A 10

Repository: pingdotgg/lawn

Length of output: 1235


🏁 Script executed:

#!/bin/bash
# Search for any mutation that might delete or update shareToken
rg -n "mutation\|action" convex/projects.ts | head -20

Repository: pingdotgg/lawn

Length of output: 40


Project share tokens lack expiration, creating indefinite access risk.

The getForProjectShare query only validates that the project exists and the video belongs to that project, with no expiration check. Unlike video share grants (which validate expiry via resolveActiveShareGrant), project share tokens have no expiresAt field in the schema.

This means a leaked project share token grants permanent access to all current and future videos in that project until manually revoked via revokeShareToken. This is a significant design inconsistency with the video-level sharing mechanism.

Consider adding an optional shareTokenExpiresAt field to the projects schema to support time-limited sharing, similar to the shareAccessGrants model.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@convex/videoActions.ts` around lines 490 - 516, Add an optional
shareTokenExpiresAt timestamp to the projects schema and update the
getForProjectShare query and the getProjectSharePlaybackSession action to verify
the token is not expired (compare now to shareTokenExpiresAt) similar to
resolveActiveShareGrant for video shares; ensure revokeShareToken behavior
remains compatible (it can still clear the token) and add migration/validation
so existing tokens without shareTokenExpiresAt are treated as non-expiring only
if you intend that, otherwise require an expiry when creating a project share
token.

- Fix hardcoded SEO paths in project-share routes (macroscope): use
  dynamic params.token/videoId in seoHead path so og:url and canonical
  tags point to the correct URL
- Redact shareToken from projects.list/get queries to prevent leaking
  bearer tokens to viewer-role users; add member-only getShareToken
  query and update dashboard to use it
- Add try/catch with user-facing toast feedback to revoke button and
  generate-token error path in project dashboard
- Change videoId args from v.id("videos") to v.string() + normalizeId
  on all public project-share endpoints (getForProjectShare,
  getThreadedForProjectShare, getProjectSharePlaybackSession) so
  malformed URL segments return null instead of a validation error

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (1)
convex/projects.ts (1)

42-44: Use a lint-safe helper for token redaction.

Line 42 and Line 106 both strip shareToken via _shareToken, and static analysis is already flagging that binding as unused. In the current ESLint setup this will fail lint even though the redaction logic is correct. A small helper that clones the document and deletes shareToken avoids the duplicate error in both places.

♻️ Suggested refactor
+function omitProjectShareToken<T extends { shareToken?: string }>(
+  project: T,
+): Omit<T, "shareToken"> {
+  const copy = { ...project };
+  delete copy.shareToken;
+  return copy;
+}
+
 ...
-        const { shareToken: _shareToken, ...projectWithoutToken } = project;
+        const projectWithoutToken = omitProjectShareToken(project);
         return {
           ...projectWithoutToken,
           videoCount: videos.length,
         };
 ...
-    const { shareToken: _shareToken, ...projectWithoutToken } = project;
+    const projectWithoutToken = omitProjectShareToken(project);
     return { ...projectWithoutToken, role: membership.role };

Also applies to: 106-107

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@convex/projects.ts` around lines 42 - 44, Replace the current destructuring
pattern that binds an unused _shareToken when returning projectWithoutToken with
a small lint-safe helper (e.g., redactShareToken or cloneWithoutShareToken) that
takes the project object, shallow-clones it and deletes the shareToken property,
then returns the cleaned object; update both places that now create
projectWithoutToken to call that helper (referencing project and
projectWithoutToken in the diff) so ESLint no longer flags an unused binding.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/routes/-project-share-video.tsx`:
- Around line 28-55: Reset all playback-related state at the start of the effect
and before early-return: when token/videoId/video?.muxPlaybackId change, call
setPlaybackSession(null), setIsLoadingPlayback(false) (or true as appropriate)
and setPlaybackError(null) before returning early and also clear/set them
immediately before calling getPlaybackSession; update the useEffect block that
references getPlaybackSession, token, videoId, video?.muxPlaybackId to ensure
the old playbackSession, loading and error state are cleared so stale video
isn’t shown while the new request resolves.

In `@app/routes/dashboard/-project.tsx`:
- Around line 148-152: The code currently collapses the query result into
shareToken immediately (const shareToken = shareTokenData?.shareToken ?? null),
treating a loading query as “no token” and allowing generateProjectShare to run
and rotate tokens; instead, preserve the raw useQuery result (shareTokenData)
and use its loading/state flags to gate UI/actions: do not derive shareToken to
null while shareTokenData is unresolved, check shareTokenData.isLoading or
similar before enabling the generateProjectShare action/button, and only call
generateProjectShare when the query has settled and shareTokenData.data (or
shareTokenData.shareToken) is explicitly null or present; update the same
pattern wherever shareToken is derived (refs:
useQuery(api.projects.getShareToken), shareTokenData, shareToken, and
generateProjectShare) so generation is disabled until the query resolves.
- Around line 382-387: The popover onClick handler calls
copyTextToClipboard(url) but only chains .then(...), so rejections (e.g.,
navigator.clipboard.writeText denied) are unhandled; update the onClick handler
for the popover to handle promise rejections from copyTextToClipboard by adding
a .catch(...) (or using async/await with try/catch) and call
showShareToast("error", "Could not copy link") on failure; reference the
existing onClick, copyTextToClipboard, showShareToast, projectSharePath and
shareToken symbols so you update the same handler logic.
- Around line 257-258: Remove the invalid ESLint suppression comment for
react-hooks/exhaustive-deps and reorder the local functions so showShareToast is
declared before handleShareProject; specifically, move the showShareToast
function (currently at line ~260) above handleShareProject (currently ~241),
then delete the "// eslint-disable-next-line react-hooks/exhaustive-deps" line
and keep the effect dependency array as [shareToken, resolvedProjectId,
generateProjectShare] since showShareToast is a stable reference with empty
deps.

In `@convex/videoActions.ts`:
- Around line 490-515: getProjectSharePlaybackSession currently returns a
permanent public playback URL by calling ensurePublicPlaybackId and
buildPublicPlaybackSession, so revoking a share token (revokeShareToken) won't
stop already-fetched URLs; change this handler to create and return a
short-lived, revocable/signed playback session (e.g. call a new or existing
helper like createSignedPlaybackSession or ensurePublicPlaybackId({ ephemeral:
true })) instead of persisting a public playback ID on the Video record, and do
not write the playback id to the video in ensurePublicPlaybackId; update
getProjectSharePlaybackSession to use that signed session flow and keep
buildPublicPlaybackSession only for non-revocable public uses.

---

Nitpick comments:
In `@convex/projects.ts`:
- Around line 42-44: Replace the current destructuring pattern that binds an
unused _shareToken when returning projectWithoutToken with a small lint-safe
helper (e.g., redactShareToken or cloneWithoutShareToken) that takes the project
object, shallow-clones it and deletes the shareToken property, then returns the
cleaned object; update both places that now create projectWithoutToken to call
that helper (referencing project and projectWithoutToken in the diff) so ESLint
no longer flags an unused binding.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8af141c0-88e0-4ba3-af41-620f81cfaba8

📥 Commits

Reviewing files that changed from the base of the PR and between 5d1a481 and eddb485.

📒 Files selected for processing (8)
  • app/routes/-project-share-video.tsx
  • app/routes/dashboard/-project.tsx
  • app/routes/project-share.$token.$videoId.tsx
  • app/routes/project-share.$token.index.tsx
  • convex/comments.ts
  • convex/projects.ts
  • convex/videoActions.ts
  • convex/videos.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • convex/videos.ts
  • convex/comments.ts

Comment on lines +490 to +515
export const getProjectSharePlaybackSession = action({
args: { shareToken: v.string(), videoId: v.string() },
returns: v.object({
url: v.string(),
posterUrl: v.string(),
}),
handler: async (
ctx,
args,
): Promise<{ url: string; posterUrl: string }> => {
const result = await ctx.runQuery(api.videos.getForProjectShare, {
shareToken: args.shareToken,
videoId: args.videoId,
});

if (!result?.muxPlaybackId) {
throw new Error("Video not found or not ready");
}

const playbackId = await ensurePublicPlaybackId(ctx, {
videoId: result._id,
muxAssetId: result.muxAssetId,
muxPlaybackId: result.muxPlaybackId,
});
return buildPublicPlaybackSession(playbackId);
},
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Revoke link does not revoke the stream URL once it has been fetched.

This action returns a public playback URL, and ensurePublicPlaybackId() can also persist a public playback ID onto the video. After revokeShareToken() clears the project token, anyone who already captured session.url can still hit Mux directly, so revocation only hides the app route, not the video itself. Project-share playback needs a revocable/signed session here rather than a bare public playback URL.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@convex/videoActions.ts` around lines 490 - 515,
getProjectSharePlaybackSession currently returns a permanent public playback URL
by calling ensurePublicPlaybackId and buildPublicPlaybackSession, so revoking a
share token (revokeShareToken) won't stop already-fetched URLs; change this
handler to create and return a short-lived, revocable/signed playback session
(e.g. call a new or existing helper like createSignedPlaybackSession or
ensurePublicPlaybackId({ ephemeral: true })) instead of persisting a public
playback ID on the Video record, and do not write the playback id to the video
in ensurePublicPlaybackId; update getProjectSharePlaybackSession to use that
signed session flow and keep buildPublicPlaybackSession only for non-revocable
public uses.

Malthe Engmann Kristensen and others added 2 commits March 9, 2026 16:01
- Reset playback state (session/loading/error) before each new fetch in
  ProjectShareVideoPage so the old video never shows under new metadata
- Guard generateProjectShare against firing while getShareToken is still
  loading to prevent silently rotating an existing share URL; also
  disable the Share button while loading
- Reorder showShareToast before handleShareProject to eliminate the
  forward reference and remove the invalid ESLint suppression comment
- Add .catch() to clipboard copy handler so rejections surface as an
  error toast rather than an unhandled promise rejection

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… helper

Replaces the destructuring pattern `{ shareToken: _shareToken, ...rest }`
with a typed helper that shallow-clones the object and deletes the field,
avoiding the ESLint unused-variable binding flagged by CodeRabbit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/routes/-project-share-video.tsx`:
- Around line 152-169: The spinner is always rendered even when playbackError is
present; update the conditional rendering so the spinner element (the div with
classes "h-8 w-8 animate-spin rounded-full ...") only renders when there is no
playbackError and isLoadingPlayback is true, leaving the message paragraph
(which uses playbackError ?? ...) unchanged so errors display without the
loader.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d152d1de-17cb-413c-9529-0c19f65f0051

📥 Commits

Reviewing files that changed from the base of the PR and between eddb485 and 29e767d.

📒 Files selected for processing (2)
  • app/routes/-project-share-video.tsx
  • app/routes/dashboard/-project.tsx

The spinner was unconditionally rendered in the video placeholder, so it
spun alongside the error message. Now it only renders when loading and
no error is present.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant