Skip to content

Per-space viewer controls#1795

Merged
richiemcilroy merged 19 commits into
mainfrom
spaces-sharing
May 11, 2026
Merged

Per-space viewer controls#1795
richiemcilroy merged 19 commits into
mainfrom
spaces-sharing

Conversation

@richiemcilroy
Copy link
Copy Markdown
Member

@richiemcilroy richiemcilroy commented May 10, 2026

This PR adds per-space viewer controls (summary, captions, chapters, reactions, transcript, comments) and optional space-level password protection, then merges those rules with video and org settings so the dashboard, share page, and embed player reflect the effective policy.

Greptile Summary

This PR introduces per-space viewer controls (summary, captions, chapters, reactions, transcript, comments) and optional space-level password protection, merging those rules with video and org settings so the dashboard, share page, and embed player all reflect the effective policy.

  • EffectiveVideoRules.ts is a new pure function that merges video, org, and space-level viewer settings; it is well-tested and cleanly separated from I/O.
  • VideosPolicy.ts now fetches space password hashes before the membership check so any space password gates access (note: this also affects explicit space members, discussed in a prior review comment).
  • Schema migration adds settings (JSON) and password (encrypted text) columns to the spaces table, with create-space.ts / update-space.ts handling pro-tier gating and the shared space-settings.ts helper keeping both actions in sync.

Confidence Score: 4/5

Safe to merge with minor attention to org-settings scoping in the caps dashboard page.

The core policy and rules logic is solid and well-covered by unit tests. The one inconsistency is that caps/page.tsx resolves org settings from user.activeOrganizationId for all videos, while folder.ts correctly fetches per unique video.orgId. For users whose active org matches every video's org this makes no difference; for multi-org accounts it would silently compute the wrong effective viewer policy on the caps dashboard.

apps/web/app/(org)/dashboard/caps/page.tsx — single-org settings lookup should be replaced with per-video org lookup to match the pattern in folder.ts.

Important Files Changed

Filename Overview
packages/web-backend/src/Videos/EffectiveVideoRules.ts New module computing merged viewer settings from video, org, and space inputs; logic is clean and well-tested.
packages/web-backend/src/Videos/VideosPolicy.ts Adds space-password lookup before membership checks; space members now require a password if any of their spaces is protected (noted in prior review comments).
apps/web/actions/videos/password.ts Password verification now checks video AND all space passwords, iterating bcrypt compares and setting the matched hash in the cookie.
apps/web/actions/organization/space-settings.ts New shared helper for extracting/preserving space settings from form data; correctly handles pro-gating of summary/chapters/transcript toggles.
apps/web/app/(org)/dashboard/caps/page.tsx Adds effective-rules computation per cap; org settings fetched once from user.activeOrganizationId rather than per-video orgId (inconsistent with folder.ts).
apps/web/lib/folder.ts Correctly fetches org settings per unique orgId across all videos before computing effective rules.
apps/web/app/embed/[videoId]/page.tsx EmbedContent now fetches space settings and computes effective rules; organizations left-joined cleanly provides orgSettings.
apps/web/app/s/[videoId]/page.tsx Share page now passes merged effective settings to the player; org-share entries correctly filtered before resolveEffectiveVideoRules.
packages/database/schema.ts Adds settings (json) and password (encryptedTextNullable) columns to spaces table; migration matches.

Comments Outside Diff (1)

  1. packages/web-backend/src/Videos/VideosPolicy.ts, line 82-91 (link)

    P1 Space members now blocked by space passwords they may not know

    buildCanView now calls verifyPasswordCandidates(video, passwordHashes) for org/space members where passwordHashes includes every space-level password hash. Because the only way to satisfy this check is via the x-cap-password cookie (set only through the verifyVideoPassword action), a user who is an explicit member of a space but has never manually entered that space's password will receive a VerifyVideoPasswordError and be denied access to any video inside that password-protected space.

    The test "requires a space password for space members" confirms this is the intentional design, but it is a sharp behavioral change: previously, having org/space membership granted access to unpassworded videos unconditionally. Now even members must go through the password-entry flow if their space has a password set, but they may never be shown that UI prompt. If the design intent is that space passwords are exclusively for external viewers, consider only passing the video-level password hash to verifyPasswordCandidates for the member branch, or skipping the call entirely for confirmed members.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: packages/web-backend/src/Videos/VideosPolicy.ts
    Line: 82-91
    
    Comment:
    **Space members now blocked by space passwords they may not know**
    
    `buildCanView` now calls `verifyPasswordCandidates(video, passwordHashes)` for org/space members where `passwordHashes` includes every space-level password hash. Because the only way to satisfy this check is via the `x-cap-password` cookie (set only through the `verifyVideoPassword` action), a user who is an explicit member of a space but has never manually entered that space's password will receive a `VerifyVideoPasswordError` and be denied access to any video inside that password-protected space.
    
    The test `"requires a space password for space members"` confirms this is the intentional design, but it is a sharp behavioral change: previously, having org/space membership granted access to unpassworded videos unconditionally. Now even members must go through the password-entry flow if their space has a password set, but they may never be shown that UI prompt. If the design intent is that space passwords are exclusively for external viewers, consider only passing the video-level password hash to `verifyPasswordCandidates` for the member branch, or skipping the call entirely for confirmed members.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
apps/web/app/(org)/dashboard/caps/page.tsx:244-251
Organization settings are fetched once using `user.activeOrganizationId` and applied uniformly to every cap on this page. In contrast, `folder.ts` fetches org settings per unique `video.orgId` across all videos. If a user's caps page ever includes videos whose `orgId` differs from `activeOrganizationId` (e.g. after an org switch), the wrong org-level viewer rules would be merged in, silently showing an incorrect effective policy in the dashboard.

```suggestion
	const orgIds = Array.from(
		new Set(videoData.map((video) => video.orgId).filter(Boolean)),
	) as string[];
	const organizationSettingsRows =
		orgIds.length > 0
			? await db()
					.select({ id: organizations.id, settings: organizations.settings })
					.from(organizations)
					.where(inArray(organizations.id, orgIds))
			: [];
	const organizationSettingsById = Object.fromEntries(
		organizationSettingsRows.map((org) => [org.id, org.settings]),
	);
```

### Issue 2 of 2
apps/web/app/(org)/dashboard/caps/page.tsx:258-265
**Follow-up to per-video org lookup** — if the lookup above is switched to per-video, this call also needs updating to `organizationSettings: organizationSettingsById[video.orgId] ?? null` to match the pattern already used in `folder.ts`.

Reviews (3): Last reviewed commit: "fix: restore typecheck setup" | Re-trigger Greptile

@brin-security-scanner brin-security-scanner Bot added contributor:verified Contributor passed trust analysis. pr:verified PR passed security analysis. labels May 10, 2026
@paragon-review
Copy link
Copy Markdown

Paragon Review Skipped

Hi @richiemcilroy! Your Polarity credit balance is insufficient to complete this review.

Please visit https://app.paragon.run to finish your review.

Comment thread apps/web/lib/folder.ts
Comment on lines 262 to +270
const processedVideoData = yield* Effect.all(
videoData.map(
Effect.fn(function* (video) {
const sharedSpaces = sharedSpacesMap[video.id] ?? [];
const rules = resolveEffectiveVideoRules({
videoSettings: video.settings,
organizationSettings: null,
spaces: sharedSpaces.filter((space) => !space.isOrg),
});
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.

P2 Org settings not passed to resolveEffectiveVideoRules

organizationSettings is hardcoded to null here (and identically in spaces/[spaceId]/page.tsx). If an org has disabled, say, disableCaptions at the org level, the dashboard card will compute settings.disableCaptions = false — showing a different state than what viewers actually see on the share/embed pages, which correctly receive org settings. An owner could be misled about effective settings for their video.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/web/lib/folder.ts
Line: 262-270

Comment:
**Org settings not passed to `resolveEffectiveVideoRules`**

`organizationSettings` is hardcoded to `null` here (and identically in `spaces/[spaceId]/page.tsx`). If an org has disabled, say, `disableCaptions` at the org level, the dashboard card will compute `settings.disableCaptions = false` — showing a different state than what viewers actually see on the share/embed pages, which correctly receive org settings. An owner could be misled about effective settings for their video.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +22 to +40
const settingKeys = [
"disableSummary",
"disableCaptions",
"disableChapters",
"disableReactions",
"disableTranscript",
"disableComments",
] as const;

const getSettingsFromFormData = (formData: FormData) =>
Object.fromEntries(
settingKeys.map((key) => [key, formData.get(key) === "true"]),
);

const proSettingKeys = [
"disableSummary",
"disableChapters",
"disableTranscript",
] as const;
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.

P2 Duplicate setting-key arrays silently diverge from create-space.ts

settingKeys and proSettingKeys are defined verbatim in both create-space.ts and update-space.ts. If a new viewer-control key (e.g. disableDownload) is added to one file but missed in the other, create will honour it while update silently drops it (or vice-versa). Consider extracting these into a shared constants module so both actions stay in sync automatically.

Suggested change
const settingKeys = [
"disableSummary",
"disableCaptions",
"disableChapters",
"disableReactions",
"disableTranscript",
"disableComments",
] as const;
const getSettingsFromFormData = (formData: FormData) =>
Object.fromEntries(
settingKeys.map((key) => [key, formData.get(key) === "true"]),
);
const proSettingKeys = [
"disableSummary",
"disableChapters",
"disableTranscript",
] as const;
// TODO: extract these into a shared constants module (e.g. lib/space-settings.ts)
// that both create-space.ts and update-space.ts import, so adding a new key
// only needs to happen in one place.
const settingKeys = [
"disableSummary",
"disableCaptions",
"disableChapters",
"disableReactions",
"disableTranscript",
"disableComments",
] as const;
const getSettingsFromFormData = (formData: FormData) =>
Object.fromEntries(
settingKeys.map((key) => [key, formData.get(key) === "true"]),
);
const proSettingKeys = [
"disableSummary",
"disableChapters",
"disableTranscript",
] as const;
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/web/actions/organization/update-space.ts
Line: 22-40

Comment:
**Duplicate setting-key arrays silently diverge from `create-space.ts`**

`settingKeys` and `proSettingKeys` are defined verbatim in both `create-space.ts` and `update-space.ts`. If a new viewer-control key (e.g. `disableDownload`) is added to one file but missed in the other, create will honour it while update silently drops it (or vice-versa). Consider extracting these into a shared constants module so both actions stay in sync automatically.

```suggestion
// TODO: extract these into a shared constants module (e.g. lib/space-settings.ts)
// that both create-space.ts and update-space.ts import, so adding a new key
// only needs to happen in one place.
const settingKeys = [
	"disableSummary",
	"disableCaptions",
	"disableChapters",
	"disableReactions",
	"disableTranscript",
	"disableComments",
] as const;

const getSettingsFromFormData = (formData: FormData) =>
	Object.fromEntries(
		settingKeys.map((key) => [key, formData.get(key) === "true"]),
	);

const proSettingKeys = [
	"disableSummary",
	"disableChapters",
	"disableTranscript",
] as const;
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines 87 to 90

if (!isCreator && !membership) {
if (!isCreator && membership?.role !== "Admin") {
return { success: false, error: "Unauthorized" };
}
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.

P2 Authorization tightened from "any member" to "Admin-role only"

Previously any space member could submit the update form. This PR changes the guard to membership?.role !== "Admin", so regular (non-admin) members can no longer rename a space, change viewer settings, or manage the member list. This is a sensible security improvement, but it's a silent behaviour change that could break existing UX flows where regular members were previously able to update the space. Worth documenting in the PR or confirming it's intentional.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/web/actions/organization/update-space.ts
Line: 87-90

Comment:
**Authorization tightened from "any member" to "Admin-role only"**

Previously any space member could submit the update form. This PR changes the guard to `membership?.role !== "Admin"`, so regular (non-admin) members can no longer rename a space, change viewer settings, or manage the member list. This is a sensible security improvement, but it's a silent behaviour change that could break existing UX flows where regular members were previously able to update the space. Worth documenting in the PR or confirming it's intentional.

How can I resolve this? If you propose a fix, please make it concise.

@richiemcilroy
Copy link
Copy Markdown
Member Author

hey! please re-review the pr @greptileai

@richiemcilroy
Copy link
Copy Markdown
Member Author

please re-review the pr @greptileai

@richiemcilroy richiemcilroy merged commit 64f494f into main May 11, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contributor:verified Contributor passed trust analysis. pr:verified PR passed security analysis.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant