Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion apps/web/actions/organization/create-space.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,9 @@ export async function createSpace(
if (!userId) return null;
// Creator is always Owner, others are Member
const role =
email.toLowerCase() === creatorEmail ? "Admin" : "Member";
email.toLowerCase() === creatorEmail
? ("Admin" as const)
: ("member" as const);
return {
id: uuidv4().substring(0, nanoIdLength),
spaceId,
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/(org)/dashboard/Contexts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export function DashboardContexts({
(member) =>
member.userId === user.id &&
member.organizationId === space.organizationId &&
member.role === "MEMBER",
member.role === "member",
),
) || null;

Expand Down
116 changes: 62 additions & 54 deletions apps/web/app/(org)/dashboard/folder/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { getCurrentUser } from "@cap/database/auth/session";
import { serverEnv } from "@cap/env";
import type { Folder } from "@cap/web-domain";
import { CurrentUser, type Folder } from "@cap/web-domain";
import { Effect } from "effect";
import { notFound } from "next/navigation";
import {
getChildFolders,
getFolderBreadcrumb,
getVideosByFolderId,
} from "@/lib/folder";
import { runPromise } from "@/lib/server";
import { UploadCapButton } from "../../caps/components";
import FolderCard from "../../caps/components/Folder";
import {
Expand All @@ -14,66 +18,70 @@ import {
} from "./components";
import FolderVideosSection from "./components/FolderVideosSection";

const FolderPage = async (props: {
params: Promise<{ id: Folder.FolderId }>;
}) => {
const params = await props.params;
const [childFolders, breadcrumb, videosData] = await Promise.all([
getChildFolders(params.id),
getFolderBreadcrumb(params.id),
getVideosByFolderId(params.id),
]);
const FolderPage = async ({ params }: { params: { id: Folder.FolderId } }) => {
const user = await getCurrentUser();
if (!user || !user.activeOrganizationId) return notFound();

return (
<div>
<div className="flex gap-2 items-center mb-10">
<NewSubfolderButton parentFolderId={params.id} />
<UploadCapButton size="sm" folderId={params.id} />
</div>
<div className="flex justify-between items-center mb-6 w-full">
<div className="flex overflow-x-auto items-center font-medium">
<ClientMyCapsLink />
return Effect.gen(function* () {
const [childFolders, breadcrumb, videosData] = yield* Effect.all([
getChildFolders(params.id, { variant: "user" }),
getFolderBreadcrumb(params.id),
getVideosByFolderId(params.id),
]);

{breadcrumb.map((folder, index) => (
<div key={folder.id} className="flex items-center">
<p className="mx-2 text-gray-10">/</p>
<BreadcrumbItem
id={folder.id}
name={folder.name}
color={folder.color}
isLast={index === breadcrumb.length - 1}
/>
</div>
))}
return (
<div>
<div className="flex gap-2 items-center mb-10">
<NewSubfolderButton parentFolderId={params.id} />
<UploadCapButton size="sm" folderId={params.id} />
</div>
</div>
<div className="flex justify-between items-center mb-6 w-full">
<div className="flex overflow-x-auto items-center font-medium">
<ClientMyCapsLink />

{/* Display Child Folders */}
{childFolders.length > 0 && (
<>
<h1 className="mb-6 text-xl font-medium text-gray-12">Subfolders</h1>
<div className="grid grid-cols-[repeat(auto-fill,minmax(250px,1fr))] gap-4 mb-10">
{childFolders.map((folder) => (
<FolderCard
key={folder.id}
name={folder.name}
color={folder.color}
id={folder.id}
parentId={folder.parentId}
videoCount={folder.videoCount}
/>
{breadcrumb.map((folder, index) => (
<div key={folder.id} className="flex items-center">
<p className="mx-2 text-gray-10">/</p>
<BreadcrumbItem
id={folder.id}
name={folder.name}
color={folder.color}
isLast={index === breadcrumb.length - 1}
/>
</div>
))}
</div>
</>
)}
</div>

{/* Display Videos */}
<FolderVideosSection
initialVideos={videosData}
dubApiKeyEnabled={!!serverEnv().DUB_API_KEY}
/>
</div>
);
{/* Display Child Folders */}
{childFolders.length > 0 && (
<>
<h1 className="mb-6 text-xl font-medium text-gray-12">
Subfolders
</h1>
Comment on lines +56 to +61
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Remove JSX comments (repo forbids code comments)

Remove the “Display Child Folders” and “Display Videos” comments.

Apply this diff:

-        {/* Display Child Folders */}
         {childFolders.length > 0 && (
           <>
             <h1 className="mb-6 text-xl font-medium text-gray-12">
               Subfolders
             </h1>
@@
-        {/* Display Videos */}
         <FolderVideosSection
           initialVideos={videosData}
           dubApiKeyEnabled={!!serverEnv().DUB_API_KEY}
         />

Also applies to: 77-81

🤖 Prompt for AI Agents
In apps/web/app/(org)/dashboard/folder/[id]/page.tsx around lines 56-61 and
77-81, remove the inline JSX comments "// Display Child Folders" and "// Display
Videos" (the {/* ... */} blocks) so the file no longer contains JSX comments;
simply delete those comment nodes leaving the surrounding JSX unchanged.

<div className="grid grid-cols-[repeat(auto-fill,minmax(250px,1fr))] gap-4 mb-10">
{childFolders.map((folder) => (
<FolderCard
key={folder.id}
name={folder.name}
color={folder.color}
id={folder.id}
parentId={folder.parentId}
videoCount={folder.videoCount}
/>
))}
</div>
</>
)}

{/* Display Videos */}
<FolderVideosSection
initialVideos={videosData}
dubApiKeyEnabled={!!serverEnv().DUB_API_KEY}
/>
</div>
);
}).pipe(Effect.provideService(CurrentUser, user), runPromise);
};

export default FolderPage;
8 changes: 5 additions & 3 deletions apps/web/app/(org)/dashboard/spaces/[spaceId]/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,18 @@ import { revalidatePath } from "next/cache";
import { v4 as uuidv4 } from "uuid";
import { z } from "zod";

const spaceRole = z.union([z.literal("Admin"), z.literal("member")]);

const addSpaceMemberSchema = z.object({
spaceId: z.string(),
userId: z.string(),
role: z.string(),
role: spaceRole,
});
Comment on lines +17 to 18
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Missing authorization: member management endpoints allow any logged-in user.

Gate add-members actions by requiring the caller to be an Admin of the target space.

Example guard to add after currentUser and before mutating queries:

const me = await db()
  .select({ role: spaceMembers.role })
  .from(spaceMembers)
  .where(and(eq(spaceMembers.spaceId, spaceId), eq(spaceMembers.userId, currentUser.id)))
  .limit(1);

if (me.length === 0 || me[0].role !== "Admin") {
  throw new Error("Forbidden");
}

I can open a follow-up PR adding a shared assertSpaceAdmin(spaceId, userId) helper and applying it across all actions here. Proceed?

Also applies to: 23-24

🤖 Prompt for AI Agents
In apps/web/app/(org)/dashboard/spaces/[spaceId]/actions.ts around lines 17-18
(and also lines 23-24), the actions that add/manage members lack an
authorization guard and currently allow any authenticated user; after resolving
currentUser and before performing any mutating DB queries, query the
spaceMembers table for a record matching the spaceId and currentUser.id, check
that it exists and that role === "Admin", and if not throw a Forbidden error;
alternatively extract this into a shared assertSpaceAdmin(spaceId, userId)
helper and call it from these action handlers so only space Admins can perform
member management.


const addSpaceMembersSchema = z.object({
spaceId: z.string(),
userIds: z.array(z.string()),
role: z.string(),
role: spaceRole,
});

export async function addSpaceMember(
Expand Down Expand Up @@ -149,7 +151,7 @@ export async function removeSpaceMember(
const setSpaceMembersSchema = z.object({
spaceId: z.string(),
userIds: z.array(z.string()),
role: z.string().default("member"),
role: spaceRole.default("member"),
});
Comment on lines +154 to 155
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Set-all members: wrap delete+insert in a transaction; consider onConflictDoNothing for bulk inserts.

Avoid partial writes and duplicate key errors under concurrent calls.

Example refactor:

await db().transaction(async (tx) => {
  await tx.delete(spaceMembers).where(eq(spaceMembers.spaceId, spaceId));
  if (userIds.length > 0) {
    await tx.insert(spaceMembers).values(values);
  }
});

For idempotency on bulk adds elsewhere, use Drizzle’s onConflictDoNothing targeting (spaceId, userId) if you have a unique index on that pair.

🤖 Prompt for AI Agents
In apps/web/app/(org)/dashboard/spaces/[spaceId]/actions.ts around lines
154-155, the current set-all members logic performs a delete followed by inserts
without a transaction, risking partial writes and duplicate-key errors under
concurrent calls; wrap the delete+insert in a single db transaction so both
operations commit or roll back together, and when doing bulk inserts consider
using Drizzle’s onConflictDoNothing (targeting the unique (spaceId, userId)
index) to make bulk adds idempotent and avoid duplicate key errors.


export async function setSpaceMembers(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,79 +1,98 @@
import { getCurrentUser } from "@cap/database/auth/session";
import { serverEnv } from "@cap/env";
import type { Folder } from "@cap/web-domain";
import { CurrentUser, type Folder } from "@cap/web-domain";
import { Effect } from "effect";
import { notFound } from "next/navigation";
import FolderCard from "@/app/(org)/dashboard/caps/components/Folder";
import {
getChildFolders,
getFolderBreadcrumb,
getVideosByFolderId,
} from "@/lib/folder";
import { runPromise } from "@/lib/server";
import {
BreadcrumbItem,
ClientMyCapsLink,
NewSubfolderButton,
} from "../../../../folder/[id]/components";
import FolderVideosSection from "../../../../folder/[id]/components/FolderVideosSection";
import { getSpaceOrOrg } from "../../utils";

const FolderPage = async (props: {
params: Promise<{ spaceId: string; folderId: Folder.FolderId }>;
}) => {
const params = await props.params;
const user = await getCurrentUser();
if (!user) return;
if (!user) return notFound();

const [childFolders, breadcrumb, videosData] = await Promise.all([
getChildFolders(params.folderId),
getFolderBreadcrumb(params.folderId),
getVideosByFolderId(params.folderId),
]);
const userId = user?.id as string;
return await Effect.gen(function* () {
const spaceOrOrg = yield* getSpaceOrOrg(params.spaceId);
if (!spaceOrOrg) notFound();

return (
<div>
<div className="flex gap-2 items-center mb-10">
<NewSubfolderButton parentFolderId={params.folderId} />
</div>
<div className="flex justify-between items-center mb-6 w-full">
<div className="flex overflow-x-auto items-center font-medium">
<ClientMyCapsLink />
{breadcrumb.map((folder, index) => (
<div key={folder.id} className="flex items-center">
<p className="mx-2 text-gray-10">/</p>
<BreadcrumbItem
id={folder.id}
name={folder.name}
color={folder.color}
isLast={index === breadcrumb.length - 1}
/>
</div>
))}
const [childFolders, breadcrumb, videosData] = yield* Effect.all([
getChildFolders(
params.folderId,
spaceOrOrg.variant === "space"
? { variant: "space", spaceId: spaceOrOrg.space.id }
: { variant: "org", organizationId: spaceOrOrg.organization.id },
),
getFolderBreadcrumb(params.folderId),
getVideosByFolderId(params.folderId),
]);

return (
<div>
<div className="flex gap-2 items-center mb-10">
<NewSubfolderButton parentFolderId={params.folderId} />
</div>
</div>
{/* Display Child Folders */}
{childFolders.length > 0 && (
<>
<h1 className="mb-6 text-xl font-medium text-gray-12">Subfolders</h1>
<div className="grid grid-cols-[repeat(auto-fill,minmax(250px,1fr))] gap-4 mb-10">
{childFolders.map((folder) => (
<FolderCard
key={folder.id}
name={folder.name}
color={folder.color}
spaceId={params.spaceId}
id={folder.id}
parentId={folder.parentId}
videoCount={folder.videoCount}
/>
<div className="flex justify-between items-center mb-6 w-full">
<div className="flex overflow-x-auto items-center font-medium">
<ClientMyCapsLink />
{breadcrumb.map((folder, index) => (
<div key={folder.id} className="flex items-center">
<p className="mx-2 text-gray-10">/</p>
<BreadcrumbItem
id={folder.id}
name={folder.name}
color={folder.color}
isLast={index === breadcrumb.length - 1}
/>
</div>
))}
</div>
</>
)}
{/* Display Videos */}
<FolderVideosSection
initialVideos={videosData}
dubApiKeyEnabled={!!serverEnv().DUB_API_KEY}
/>
</div>
</div>
{/* Display Child Folders */}
{childFolders.length > 0 && (
<>
<h1 className="mb-6 text-xl font-medium text-gray-12">
Subfolders
</h1>
<div className="grid grid-cols-[repeat(auto-fill,minmax(250px,1fr))] gap-4 mb-10">
{childFolders.map((folder) => (
<FolderCard
key={folder.id}
name={folder.name}
color={folder.color}
spaceId={params.spaceId}
id={folder.id}
parentId={folder.parentId}
videoCount={folder.videoCount}
/>
))}
</div>
</>
)}
{/* Display Videos */}
<FolderVideosSection
initialVideos={videosData}
dubApiKeyEnabled={!!serverEnv().DUB_API_KEY}
Comment on lines +64 to +88
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Remove inline JSX comments to comply with codebase policy

The /* Display … */ markers violate the repo rule forbidding inline/block comments in TSX. Please drop them and rely on component structure instead. As per coding guidelines

@@
-				{/* Display Child Folders */}
 				{childFolders.length > 0 && (
 					<>
@@
-				{/* Display Videos */}
 				<FolderVideosSection
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{/* Display Child Folders */}
{childFolders.length > 0 && (
<>
<h1 className="mb-6 text-xl font-medium text-gray-12">
Subfolders
</h1>
<div className="grid grid-cols-[repeat(auto-fill,minmax(250px,1fr))] gap-4 mb-10">
{childFolders.map((folder) => (
<FolderCard
key={folder.id}
name={folder.name}
color={folder.color}
spaceId={params.spaceId}
id={folder.id}
parentId={folder.parentId}
videoCount={folder.videoCount}
/>
))}
</div>
</>
)}
{/* Display Videos */}
<FolderVideosSection
initialVideos={videosData}
dubApiKeyEnabled={!!serverEnv().DUB_API_KEY}
{childFolders.length > 0 && (
<>
<h1 className="mb-6 text-xl font-medium text-gray-12">
Subfolders
</h1>
<div className="grid grid-cols-[repeat(auto-fill,minmax(250px,1fr))] gap-4 mb-10">
{childFolders.map((folder) => (
<FolderCard
key={folder.id}
name={folder.name}
color={folder.color}
spaceId={params.spaceId}
id={folder.id}
parentId={folder.parentId}
videoCount={folder.videoCount}
/>
))}
</div>
</>
)}
<FolderVideosSection
initialVideos={videosData}
dubApiKeyEnabled={!!serverEnv().DUB_API_KEY}
🤖 Prompt for AI Agents
In apps/web/app/(org)/dashboard/spaces/[spaceId]/folder/[folderId]/page.tsx
around lines 64 to 88, remove the inline/block JSX comments (/* Display Child
Folders */ and /* Display Videos */) that violate the TSX comment policy;
instead, delete those comment lines and rely on the existing component structure
and headings to convey intent, ensuring no other code or JSX formatting is
changed and the surrounding indentation remains consistent.

/>
</div>
);
}).pipe(
Effect.catchTag("PolicyDenied", () => Effect.sync(() => notFound())),
Effect.provideService(CurrentUser, user),
runPromise,
);
};

Expand Down
Loading
Loading