From 2f61acc7f4e762e7c847ad45359ab0e5a4b25e72 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Tue, 7 Oct 2025 21:31:32 +1000 Subject: [PATCH 1/2] do it --- apps/web/actions/caps/share.ts | 6 +- apps/web/actions/organization/check-domain.ts | 5 +- apps/web/actions/organization/create-space.ts | 3 +- apps/web/actions/organization/delete-space.ts | 3 +- .../organization/get-organization-sso-data.ts | 5 +- .../web/actions/organization/remove-domain.ts | 5 +- apps/web/actions/organization/remove-icon.ts | 5 +- .../web/actions/organization/remove-invite.ts | 3 +- .../web/actions/organization/remove-member.ts | 3 +- apps/web/actions/organization/send-invites.ts | 3 +- .../actions/organization/update-details.ts | 3 +- .../web/actions/organization/update-domain.ts | 6 +- apps/web/actions/organization/update-space.ts | 5 +- .../organization/upload-organization-icon.ts | 3 +- .../actions/organization/upload-space-icon.ts | 6 +- apps/web/actions/organizations/add-videos.ts | 4 +- .../organizations/get-organization-videos.ts | 5 +- .../actions/organizations/remove-videos.ts | 3 +- apps/web/actions/spaces/add-videos.ts | 4 +- apps/web/actions/spaces/get-space-videos.ts | 3 +- apps/web/actions/spaces/remove-videos.ts | 4 +- apps/web/actions/video/upload.ts | 4 +- apps/web/actions/videos/delete-comment.ts | 7 +- apps/web/actions/videos/new-comment.ts | 6 +- .../_components/Navbar/SpacesList.tsx | 9 +- .../dashboard/_components/Navbar/server.ts | 5 +- .../caps/components/NewFolderDialog.tsx | 4 +- .../caps/components/SharingDialog.tsx | 8 +- .../caps/components/UploadCapButton.tsx | 4 +- .../dashboard/settings/account/Settings.tsx | 11 +- .../dashboard/settings/account/server.ts | 5 +- .../components/AccessEmailDomain.tsx | 4 +- .../organization/components/CustomDomain.tsx | 5 +- .../CustomDomainDialog/CustomDomainDialog.tsx | 15 ++- .../organization/components/InviteDialog.tsx | 3 +- .../organization/components/MembersCard.tsx | 6 +- .../organization/components/OrgName.tsx | 2 +- .../dashboard/spaces/[spaceId]/SharedCaps.tsx | 14 +-- .../dashboard/spaces/[spaceId]/actions.ts | 17 +-- .../[spaceId]/components/AddVideosDialog.tsx | 3 +- .../components/AddVideosDialogBase.tsx | 16 +-- .../[spaceId]/components/MembersIndicator.tsx | 11 +- .../[spaceId]/folder/[folderId]/page.tsx | 6 +- .../(org)/dashboard/spaces/[spaceId]/page.tsx | 14 +-- apps/web/app/(org)/login/form.tsx | 5 +- apps/web/app/(org)/signup/form.tsx | 5 +- apps/web/app/api/desktop/[...route]/video.ts | 13 ++- apps/web/app/api/settings/onboarding/route.ts | 3 +- .../app/api/upload/[...route]/multipart.ts | 2 +- .../web/app/api/video/comment/delete/route.ts | 19 +++- apps/web/app/api/video/comment/route.ts | 9 +- apps/web/app/api/webhooks/stripe/route.ts | 43 ++++--- apps/web/app/embed/[videoId]/page.tsx | 4 +- .../app/s/[videoId]/_components/Toolbar.tsx | 19 ++-- .../_components/tabs/Activity/Comment.tsx | 21 ++-- .../_components/tabs/Activity/Comments.tsx | 26 +++-- apps/web/app/s/[videoId]/page.tsx | 26 +++-- apps/web/components/forms/server.ts | 7 +- apps/web/lib/Notification.ts | 6 +- apps/web/lib/folder.ts | 6 +- apps/web/utils/effect.ts | 4 + apps/web/utils/helpers.ts | 2 +- packages/database/auth/auth-options.ts | 33 +++--- packages/database/auth/drizzle-adapter.ts | 26 +++-- packages/database/auth/session.ts | 3 +- packages/database/schema.ts | 106 ++++++++++++------ packages/web-backend/src/Folders/index.ts | 5 +- .../src/Organisations/OrganisationsPolicy.ts | 6 +- .../src/Organisations/OrganisationsRepo.ts | 6 +- .../src/S3Buckets/S3BucketsRepo.ts | 4 +- packages/web-backend/src/S3Buckets/index.ts | 7 +- .../web-backend/src/Spaces/SpacesPolicy.ts | 8 +- packages/web-backend/src/Spaces/SpacesRepo.ts | 13 ++- packages/web-backend/src/Spaces/index.ts | 13 ++- packages/web-domain/src/Authentication.ts | 7 +- packages/web-domain/src/Folder.ts | 6 +- packages/web-domain/src/Space.ts | 8 +- 77 files changed, 467 insertions(+), 270 deletions(-) create mode 100644 apps/web/utils/effect.ts diff --git a/apps/web/actions/caps/share.ts b/apps/web/actions/caps/share.ts index de025712d1..707e0e4d65 100644 --- a/apps/web/actions/caps/share.ts +++ b/apps/web/actions/caps/share.ts @@ -11,13 +11,13 @@ import { spaceVideos, videos, } from "@cap/database/schema"; -import type { Video } from "@cap/web-domain"; +import type { Organisation, Space, Video } from "@cap/web-domain"; import { and, eq, inArray } from "drizzle-orm"; import { revalidatePath } from "next/cache"; interface ShareCapParams { capId: Video.VideoId; - spaceIds: string[]; + spaceIds: Space.SpaceIdOrOrganisationId[]; public?: boolean; } @@ -53,7 +53,7 @@ export async function shareCap({ .from(organizations) .where( and( - inArray(organizations.id, spaceIds), + inArray(organizations.id, spaceIds as Organisation.OrganisationId[]), inArray(organizations.id, userOrganizationIds), ), ) diff --git a/apps/web/actions/organization/check-domain.ts b/apps/web/actions/organization/check-domain.ts index 93a57282c7..2f580afdaf 100644 --- a/apps/web/actions/organization/check-domain.ts +++ b/apps/web/actions/organization/check-domain.ts @@ -5,8 +5,11 @@ import { getCurrentUser } from "@cap/database/auth/session"; import { organizations } from "@cap/database/schema"; import { eq } from "drizzle-orm"; import { checkDomainStatus } from "./domain-utils"; +import { Organisation } from "@cap/web-domain"; -export async function checkOrganizationDomain(organizationId: string) { +export async function checkOrganizationDomain( + organizationId: Organisation.OrganisationId, +) { const user = await getCurrentUser(); if (!user || !organizationId) { diff --git a/apps/web/actions/organization/create-space.ts b/apps/web/actions/organization/create-space.ts index 552344f1a4..d929cc9f1e 100644 --- a/apps/web/actions/organization/create-space.ts +++ b/apps/web/actions/organization/create-space.ts @@ -11,6 +11,7 @@ import { Effect, Option } from "effect"; import { revalidatePath } from "next/cache"; import { v4 as uuidv4 } from "uuid"; import { runPromise } from "@/lib/server"; +import { Space } from "@cap/web-domain"; interface CreateSpaceResponse { success: boolean; @@ -62,7 +63,7 @@ export async function createSpace( } // Generate the space ID early so we can use it in the file path - const spaceId = nanoId(); + const spaceId = Space.SpaceId.make(nanoId()); const iconFile = formData.get("icon") as File | null; let iconUrl = null; diff --git a/apps/web/actions/organization/delete-space.ts b/apps/web/actions/organization/delete-space.ts index 066222621b..ff9a552bae 100644 --- a/apps/web/actions/organization/delete-space.ts +++ b/apps/web/actions/organization/delete-space.ts @@ -13,6 +13,7 @@ import { eq } from "drizzle-orm"; import { Effect, Option } from "effect"; import { revalidatePath } from "next/cache"; import { runPromise } from "@/lib/server"; +import { Space } from "@cap/web-domain"; interface DeleteSpaceResponse { success: boolean; @@ -20,7 +21,7 @@ interface DeleteSpaceResponse { } export async function deleteSpace( - spaceId: string, + spaceId: Space.SpaceIdOrOrganisationId, ): Promise { try { const user = await getCurrentUser(); diff --git a/apps/web/actions/organization/get-organization-sso-data.ts b/apps/web/actions/organization/get-organization-sso-data.ts index 2340d86077..696edc9886 100644 --- a/apps/web/actions/organization/get-organization-sso-data.ts +++ b/apps/web/actions/organization/get-organization-sso-data.ts @@ -3,8 +3,11 @@ import { db } from "@cap/database"; import { organizations } from "@cap/database/schema"; import { eq } from "drizzle-orm"; +import { Organisation } from "@cap/web-domain"; -export async function getOrganizationSSOData(organizationId: string) { +export async function getOrganizationSSOData( + organizationId: Organisation.OrganisationId, +) { if (!organizationId) { throw new Error("Organization ID is required"); } diff --git a/apps/web/actions/organization/remove-domain.ts b/apps/web/actions/organization/remove-domain.ts index 9085875431..369640f76e 100644 --- a/apps/web/actions/organization/remove-domain.ts +++ b/apps/web/actions/organization/remove-domain.ts @@ -3,10 +3,13 @@ import { db } from "@cap/database"; import { getCurrentUser } from "@cap/database/auth/session"; import { organizations } from "@cap/database/schema"; +import { Organisation } from "@cap/web-domain"; import { eq } from "drizzle-orm"; import { revalidatePath } from "next/cache"; -export async function removeOrganizationDomain(organizationId: string) { +export async function removeOrganizationDomain( + organizationId: Organisation.OrganisationId, +) { const user = await getCurrentUser(); if (!user) { diff --git a/apps/web/actions/organization/remove-icon.ts b/apps/web/actions/organization/remove-icon.ts index 7efa19fb2e..5f5bb5ff7e 100644 --- a/apps/web/actions/organization/remove-icon.ts +++ b/apps/web/actions/organization/remove-icon.ts @@ -3,10 +3,13 @@ import { db } from "@cap/database"; import { getCurrentUser } from "@cap/database/auth/session"; import { organizations } from "@cap/database/schema"; +import { Organisation } from "@cap/web-domain"; import { eq } from "drizzle-orm"; import { revalidatePath } from "next/cache"; -export async function removeOrganizationIcon(organizationId: string) { +export async function removeOrganizationIcon( + organizationId: Organisation.OrganisationId, +) { const user = await getCurrentUser(); if (!user) { diff --git a/apps/web/actions/organization/remove-invite.ts b/apps/web/actions/organization/remove-invite.ts index 4e06234f18..2a81f14a0e 100644 --- a/apps/web/actions/organization/remove-invite.ts +++ b/apps/web/actions/organization/remove-invite.ts @@ -5,10 +5,11 @@ import { getCurrentUser } from "@cap/database/auth/session"; import { organizationInvites, organizations } from "@cap/database/schema"; import { and, eq } from "drizzle-orm"; import { revalidatePath } from "next/cache"; +import { Organisation } from "@cap/web-domain"; export async function removeOrganizationInvite( inviteId: string, - organizationId: string, + organizationId: Organisation.OrganisationId, ) { const user = await getCurrentUser(); diff --git a/apps/web/actions/organization/remove-member.ts b/apps/web/actions/organization/remove-member.ts index 82e3a097c3..4fe75baafe 100644 --- a/apps/web/actions/organization/remove-member.ts +++ b/apps/web/actions/organization/remove-member.ts @@ -5,6 +5,7 @@ import { getCurrentUser } from "@cap/database/auth/session"; import { organizationMembers, organizations } from "@cap/database/schema"; import { and, eq } from "drizzle-orm"; import { revalidatePath } from "next/cache"; +import { Organisation } from "@cap/web-domain"; /** * Remove a member from an organization. Only the owner can perform this action. @@ -13,7 +14,7 @@ import { revalidatePath } from "next/cache"; */ export async function removeOrganizationMember( memberId: string, - organizationId: string, + organizationId: Organisation.OrganisationId, ) { const user = await getCurrentUser(); if (!user) throw new Error("Unauthorized"); diff --git a/apps/web/actions/organization/send-invites.ts b/apps/web/actions/organization/send-invites.ts index 3532fde120..da8b817904 100644 --- a/apps/web/actions/organization/send-invites.ts +++ b/apps/web/actions/organization/send-invites.ts @@ -7,12 +7,13 @@ import { OrganizationInvite } from "@cap/database/emails/organization-invite"; import { nanoId } from "@cap/database/helpers"; import { organizationInvites, organizations } from "@cap/database/schema"; import { serverEnv } from "@cap/env"; +import { Organisation } from "@cap/web-domain"; import { eq } from "drizzle-orm"; import { revalidatePath } from "next/cache"; export async function sendOrganizationInvites( invitedEmails: string[], - organizationId: string, + organizationId: Organisation.OrganisationId, ) { const user = await getCurrentUser(); diff --git a/apps/web/actions/organization/update-details.ts b/apps/web/actions/organization/update-details.ts index 0422708057..85533bd0c3 100644 --- a/apps/web/actions/organization/update-details.ts +++ b/apps/web/actions/organization/update-details.ts @@ -3,6 +3,7 @@ import { db } from "@cap/database"; import { getCurrentUser } from "@cap/database/auth/session"; import { organizations } from "@cap/database/schema"; +import { Organisation } from "@cap/web-domain"; import { eq } from "drizzle-orm"; import { revalidatePath } from "next/cache"; @@ -13,7 +14,7 @@ export async function updateOrganizationDetails({ }: { organizationName?: string | null; allowedEmailDomain?: string | null; - organizationId: string; + organizationId: Organisation.OrganisationId; }) { const user = await getCurrentUser(); diff --git a/apps/web/actions/organization/update-domain.ts b/apps/web/actions/organization/update-domain.ts index dd109e7f13..43a97491b5 100644 --- a/apps/web/actions/organization/update-domain.ts +++ b/apps/web/actions/organization/update-domain.ts @@ -6,8 +6,12 @@ import { organizations } from "@cap/database/schema"; import { eq } from "drizzle-orm"; import { revalidatePath } from "next/cache"; import { addDomain, checkDomainStatus } from "./domain-utils"; +import { Organisation } from "@cap/web-domain"; -export async function updateDomain(domain: string, organizationId: string) { +export async function updateDomain( + domain: string, + organizationId: Organisation.OrganisationId, +) { const user = await getCurrentUser(); if (!user) { diff --git a/apps/web/actions/organization/update-space.ts b/apps/web/actions/organization/update-space.ts index ab6237d0bd..211a4c4f3e 100644 --- a/apps/web/actions/organization/update-space.ts +++ b/apps/web/actions/organization/update-space.ts @@ -4,6 +4,7 @@ import { db } from "@cap/database"; import { getCurrentUser } from "@cap/database/auth/session"; import { nanoIdLength } from "@cap/database/helpers"; import { spaceMembers, spaces } from "@cap/database/schema"; +import { Space, User } from "@cap/web-domain"; import { S3Buckets } from "@cap/web-backend"; import { and, eq } from "drizzle-orm"; import { Effect, Option } from "effect"; @@ -16,9 +17,9 @@ export async function updateSpace(formData: FormData) { const user = await getCurrentUser(); if (!user) return { success: false, error: "Unauthorized" }; - const id = formData.get("id") as string; + const id = Space.SpaceId.make(formData.get("id") as string); const name = formData.get("name") as string; - const members = formData.getAll("members[]") as string[]; + const members = formData.getAll("members[]") as User.UserId[]; const iconFile = formData.get("icon") as File | null; const [membership] = await db() diff --git a/apps/web/actions/organization/upload-organization-icon.ts b/apps/web/actions/organization/upload-organization-icon.ts index 485418da7c..d151e04a48 100644 --- a/apps/web/actions/organization/upload-organization-icon.ts +++ b/apps/web/actions/organization/upload-organization-icon.ts @@ -12,10 +12,11 @@ import { JSDOM } from "jsdom"; import { revalidatePath } from "next/cache"; import { sanitizeFile } from "@/lib/sanitizeFile"; import { runPromise } from "@/lib/server"; +import { Organisation } from "@cap/web-domain"; export async function uploadOrganizationIcon( formData: FormData, - organizationId: string, + organizationId: Organisation.OrganisationId, ) { const user = await getCurrentUser(); diff --git a/apps/web/actions/organization/upload-space-icon.ts b/apps/web/actions/organization/upload-space-icon.ts index e56c92b5a6..20ee30b349 100644 --- a/apps/web/actions/organization/upload-space-icon.ts +++ b/apps/web/actions/organization/upload-space-icon.ts @@ -10,8 +10,12 @@ import { Effect, Option } from "effect"; import { revalidatePath } from "next/cache"; import { sanitizeFile } from "@/lib/sanitizeFile"; import { runPromise } from "@/lib/server"; +import { Space } from "@cap/web-domain"; -export async function uploadSpaceIcon(formData: FormData, spaceId: string) { +export async function uploadSpaceIcon( + formData: FormData, + spaceId: Space.SpaceId, +) { const user = await getCurrentUser(); if (!user) { diff --git a/apps/web/actions/organizations/add-videos.ts b/apps/web/actions/organizations/add-videos.ts index 3571238088..97c24fc84e 100644 --- a/apps/web/actions/organizations/add-videos.ts +++ b/apps/web/actions/organizations/add-videos.ts @@ -9,12 +9,12 @@ import { sharedVideos, videos, } from "@cap/database/schema"; -import type { Video } from "@cap/web-domain"; +import type { Organisation, Video } from "@cap/web-domain"; import { and, eq, inArray } from "drizzle-orm"; import { revalidatePath } from "next/cache"; export async function addVideosToOrganization( - organizationId: string, + organizationId: Organisation.OrganisationId, videoIds: Video.VideoId[], ) { try { diff --git a/apps/web/actions/organizations/get-organization-videos.ts b/apps/web/actions/organizations/get-organization-videos.ts index 1b847fa1d4..b534c7b772 100644 --- a/apps/web/actions/organizations/get-organization-videos.ts +++ b/apps/web/actions/organizations/get-organization-videos.ts @@ -3,9 +3,12 @@ import { db } from "@cap/database"; import { getCurrentUser } from "@cap/database/auth/session"; import { sharedVideos } from "@cap/database/schema"; +import { Organisation } from "@cap/web-domain"; import { eq } from "drizzle-orm"; -export async function getOrganizationVideoIds(organizationId: string) { +export async function getOrganizationVideoIds( + organizationId: Organisation.OrganisationId, +) { try { const user = await getCurrentUser(); diff --git a/apps/web/actions/organizations/remove-videos.ts b/apps/web/actions/organizations/remove-videos.ts index 307384fc39..ff70414408 100644 --- a/apps/web/actions/organizations/remove-videos.ts +++ b/apps/web/actions/organizations/remove-videos.ts @@ -12,9 +12,10 @@ import { import type { Video } from "@cap/web-domain"; import { and, eq, inArray } from "drizzle-orm"; import { revalidatePath } from "next/cache"; +import { Organisation } from "@cap/web-domain"; export async function removeVideosFromOrganization( - organizationId: string, + organizationId: Organisation.OrganisationId, videoIds: Video.VideoId[], ) { try { diff --git a/apps/web/actions/spaces/add-videos.ts b/apps/web/actions/spaces/add-videos.ts index 0e5e77aec5..bbb5656816 100644 --- a/apps/web/actions/spaces/add-videos.ts +++ b/apps/web/actions/spaces/add-videos.ts @@ -4,12 +4,12 @@ import { db } from "@cap/database"; import { getCurrentUser } from "@cap/database/auth/session"; import { nanoId } from "@cap/database/helpers"; import { spaces, spaceVideos, videos } from "@cap/database/schema"; -import type { Video } from "@cap/web-domain"; +import type { Space, Video } from "@cap/web-domain"; import { and, eq, inArray } from "drizzle-orm"; import { revalidatePath } from "next/cache"; export async function addVideosToSpace( - spaceId: string, + spaceId: Space.SpaceIdOrOrganisationId, videoIds: Video.VideoId[], ) { try { diff --git a/apps/web/actions/spaces/get-space-videos.ts b/apps/web/actions/spaces/get-space-videos.ts index be72f9c5bb..607adf9322 100644 --- a/apps/web/actions/spaces/get-space-videos.ts +++ b/apps/web/actions/spaces/get-space-videos.ts @@ -3,9 +3,10 @@ import { db } from "@cap/database"; import { getCurrentUser } from "@cap/database/auth/session"; import { spaceVideos } from "@cap/database/schema"; +import { Space } from "@cap/web-domain"; import { eq } from "drizzle-orm"; -export async function getSpaceVideoIds(spaceId: string) { +export async function getSpaceVideoIds(spaceId: Space.SpaceIdOrOrganisationId) { try { const user = await getCurrentUser(); diff --git a/apps/web/actions/spaces/remove-videos.ts b/apps/web/actions/spaces/remove-videos.ts index 0f252bfc9d..eaadc5311a 100644 --- a/apps/web/actions/spaces/remove-videos.ts +++ b/apps/web/actions/spaces/remove-videos.ts @@ -3,12 +3,12 @@ import { db } from "@cap/database"; import { getCurrentUser } from "@cap/database/auth/session"; import { folders, spaceVideos, videos } from "@cap/database/schema"; -import type { Video } from "@cap/web-domain"; +import type { Video, Space } from "@cap/web-domain"; import { and, eq, inArray } from "drizzle-orm"; import { revalidatePath } from "next/cache"; export async function removeVideosFromSpace( - spaceId: string, + spaceId: Space.SpaceIdOrOrganisationId, videoIds: Video.VideoId[], ) { try { diff --git a/apps/web/actions/video/upload.ts b/apps/web/actions/video/upload.ts index 87a3481b65..da78942878 100644 --- a/apps/web/actions/video/upload.ts +++ b/apps/web/actions/video/upload.ts @@ -11,7 +11,7 @@ import { s3Buckets, videos, videoUploads } from "@cap/database/schema"; import { buildEnv, NODE_ENV, serverEnv } from "@cap/env"; import { userIsPro } from "@cap/utils"; import { S3Buckets } from "@cap/web-backend"; -import { type Folder, Video } from "@cap/web-domain"; +import { type Folder, Organisation, Video } from "@cap/web-domain"; import { eq } from "drizzle-orm"; import { Effect, Option } from "effect"; import { revalidatePath } from "next/cache"; @@ -175,7 +175,7 @@ export async function createVideoAndGetUploadUrl({ isScreenshot?: boolean; isUpload?: boolean; folderId?: Folder.FolderId; - orgId: string; + orgId: Organisation.OrganisationId; // TODO: Remove this once we are happy with it's stability supportsUploadProgress?: boolean; }) { diff --git a/apps/web/actions/videos/delete-comment.ts b/apps/web/actions/videos/delete-comment.ts index 1cfb069321..a2527da312 100644 --- a/apps/web/actions/videos/delete-comment.ts +++ b/apps/web/actions/videos/delete-comment.ts @@ -5,15 +5,16 @@ import { getCurrentUser } from "@cap/database/auth/session"; import { comments, notifications } from "@cap/database/schema"; import { and, eq, sql } from "drizzle-orm"; import { revalidatePath } from "next/cache"; +import type { Comment, Video } from "@cap/web-domain"; export async function deleteComment({ commentId, parentId, videoId, }: { - commentId: string; - parentId?: string; - videoId: string; + commentId: Comment.CommentId; + parentId?: Comment.CommentId; + videoId: Video.VideoId; }) { const user = await getCurrentUser(); diff --git a/apps/web/actions/videos/new-comment.ts b/apps/web/actions/videos/new-comment.ts index 85a4466f7b..2bf8d6d595 100644 --- a/apps/web/actions/videos/new-comment.ts +++ b/apps/web/actions/videos/new-comment.ts @@ -4,7 +4,7 @@ import { db } from "@cap/database"; import { getCurrentUser } from "@cap/database/auth/session"; import { nanoId } from "@cap/database/helpers"; import { comments } from "@cap/database/schema"; -import type { Video } from "@cap/web-domain"; +import { Video, Comment } from "@cap/web-domain"; import { revalidatePath } from "next/cache"; import { createNotification } from "@/lib/Notification"; @@ -12,7 +12,7 @@ export async function newComment(data: { content: string; videoId: Video.VideoId; type: "text" | "emoji"; - parentCommentId: string; + parentCommentId: Comment.CommentId; timestamp: number; }) { const user = await getCurrentUser(); @@ -35,7 +35,7 @@ export async function newComment(data: { if (!content || !videoId) { throw new Error("Content and videoId are required"); } - const id = nanoId(); + const id = Comment.CommentId.make(nanoId()); const newComment = { id: id, diff --git a/apps/web/app/(org)/dashboard/_components/Navbar/SpacesList.tsx b/apps/web/app/(org)/dashboard/_components/Navbar/SpacesList.tsx index ad77e09ce7..edb4a0d41c 100644 --- a/apps/web/app/(org)/dashboard/_components/Navbar/SpacesList.tsx +++ b/apps/web/app/(org)/dashboard/_components/Navbar/SpacesList.tsx @@ -24,6 +24,7 @@ import { LayersIcon } from "../AnimatedIcons"; import type { LayersIconHandle } from "../AnimatedIcons/Layers"; import { ConfirmationDialog } from "../ConfirmationDialog"; import SpaceDialog from "./SpaceDialog"; +import { Space } from "@cap/web-domain"; const SpacesList = ({ toggleMobileNav }: { toggleMobileNav?: () => void }) => { const { spacesData, sidebarCollapsed, user } = useDashboardContext(); @@ -91,7 +92,10 @@ const SpacesList = ({ toggleMobileNav }: { toggleMobileNav?: () => void }) => { setActiveDropTarget(null); }; - const handleDrop = async (e: React.DragEvent, spaceId: string) => { + const handleDrop = async ( + e: React.DragEvent, + spaceId: Space.SpaceIdOrOrganisationId, + ) => { e.preventDefault(); setActiveDropTarget(null); @@ -120,7 +124,8 @@ const SpacesList = ({ toggleMobileNav }: { toggleMobileNav?: () => void }) => { } }; - const activeSpaceParams = (spaceId: string) => params.spaceId === spaceId; + const activeSpaceParams = (spaceId: Space.SpaceIdOrOrganisationId) => + params.spaceId === spaceId; return (
diff --git a/apps/web/app/(org)/dashboard/_components/Navbar/server.ts b/apps/web/app/(org)/dashboard/_components/Navbar/server.ts index 419eb5e8c5..83109fd149 100644 --- a/apps/web/app/(org)/dashboard/_components/Navbar/server.ts +++ b/apps/web/app/(org)/dashboard/_components/Navbar/server.ts @@ -7,12 +7,15 @@ import { organizations, users, } from "@cap/database/schema"; +import type { Organisation } from "@cap/web-domain"; import { and, eq } from "drizzle-orm"; import { revalidatePath } from "next/cache"; import { createSpace as createSpaceAction } from "@/actions/organization/create-space"; import { updateSpace as updateSpaceAction } from "@/actions/organization/update-space"; -export async function updateActiveOrganization(organizationId: string) { +export async function updateActiveOrganization( + organizationId: Organisation.OrganisationId, +) { const user = await getCurrentUser(); if (!user) throw new Error("Unauthorized"); diff --git a/apps/web/app/(org)/dashboard/caps/components/NewFolderDialog.tsx b/apps/web/app/(org)/dashboard/caps/components/NewFolderDialog.tsx index ef7dfe3c60..6565de9b7b 100644 --- a/apps/web/app/(org)/dashboard/caps/components/NewFolderDialog.tsx +++ b/apps/web/app/(org)/dashboard/caps/components/NewFolderDialog.tsx @@ -9,7 +9,7 @@ import { DialogTitle, Input, } from "@cap/ui"; -import type { Folder } from "@cap/web-domain"; +import type { Folder, Space } from "@cap/web-domain"; import { faFolderPlus } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { type RiveFile, useRiveFile } from "@rive-app/react-canvas"; @@ -31,7 +31,7 @@ import { interface Props { open: boolean; onOpenChange: (open: boolean) => void; - spaceId?: string; + spaceId?: Space.SpaceIdOrOrganisationId; } const FolderOptions = [ diff --git a/apps/web/app/(org)/dashboard/caps/components/SharingDialog.tsx b/apps/web/app/(org)/dashboard/caps/components/SharingDialog.tsx index 4eb32644f4..a4a6b43a4e 100644 --- a/apps/web/app/(org)/dashboard/caps/components/SharingDialog.tsx +++ b/apps/web/app/(org)/dashboard/caps/components/SharingDialog.tsx @@ -9,7 +9,7 @@ import { Input, Switch, } from "@cap/ui"; -import type { Video } from "@cap/web-domain"; +import { Space, Video } from "@cap/web-domain"; import { faCopy, faShareNodes } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useMutation } from "@tanstack/react-query"; @@ -69,7 +69,7 @@ export const SharingDialog: React.FC = ({ public: isPublic, }: { capId: Video.VideoId; - spaceIds: string[]; + spaceIds: Space.SpaceIdOrOrganisationId[]; public: boolean; }) => { const result = await shareCap({ capId, spaceIds, public: isPublic }); @@ -358,7 +358,9 @@ export const SharingDialog: React.FC = ({ onClick={() => updateSharing.mutate({ capId, - spaceIds: Array.from(selectedSpaces), + spaceIds: Array.from(selectedSpaces).map((v) => + Space.SpaceId.make(v), + ), public: publicToggle, }) } diff --git a/apps/web/app/(org)/dashboard/caps/components/UploadCapButton.tsx b/apps/web/app/(org)/dashboard/caps/components/UploadCapButton.tsx index 085d0dfb96..638f546520 100644 --- a/apps/web/app/(org)/dashboard/caps/components/UploadCapButton.tsx +++ b/apps/web/app/(org)/dashboard/caps/components/UploadCapButton.tsx @@ -2,7 +2,7 @@ import { Button } from "@cap/ui"; import { userIsPro } from "@cap/utils"; -import type { Folder } from "@cap/web-domain"; +import type { Folder, Organisation } from "@cap/web-domain"; import { faUpload } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { type QueryClient, useQueryClient } from "@tanstack/react-query"; @@ -100,7 +100,7 @@ export const UploadCapButton = ({ async function legacyUploadCap( file: File, folderId: Folder.FolderId | undefined, - orgId: string, + orgId: Organisation.OrganisationId, setUploadStatus: (state: UploadStatus | undefined) => void, queryClient: QueryClient, ) { diff --git a/apps/web/app/(org)/dashboard/settings/account/Settings.tsx b/apps/web/app/(org)/dashboard/settings/account/Settings.tsx index 384c78438b..3986e804d1 100644 --- a/apps/web/app/(org)/dashboard/settings/account/Settings.tsx +++ b/apps/web/app/(org)/dashboard/settings/account/Settings.tsx @@ -15,6 +15,7 @@ import { useEffect, useState } from "react"; import { toast } from "sonner"; import { useDashboardContext } from "../../Contexts"; import { patchAccountSettings } from "./server"; +import { Organisation } from "@cap/web-domain"; export const Settings = ({ user, @@ -25,9 +26,9 @@ export const Settings = ({ const { organizationData } = useDashboardContext(); const [firstName, setFirstName] = useState(user?.name || ""); const [lastName, setLastName] = useState(user?.lastName || ""); - const [defaultOrgId, setDefaultOrgId] = useState( - user?.defaultOrgId || undefined, - ); + const [defaultOrgId, setDefaultOrgId] = useState< + Organisation.OrganisationId | undefined + >(user?.defaultOrgId || undefined); // Track if form has unsaved changes const hasChanges = @@ -131,7 +132,9 @@ export const Settings = ({ organizationData?.[0]?.organization.id ?? "" } - onValueChange={(value) => setDefaultOrgId(value)} + onValueChange={(value) => + setDefaultOrgId(Organisation.OrganisationId.make(value)) + } options={(organizationData || []).map((org) => ({ value: org.organization.id, label: org.organization.name, diff --git a/apps/web/app/(org)/dashboard/settings/account/server.ts b/apps/web/app/(org)/dashboard/settings/account/server.ts index b0a8bceaee..a93b9cacff 100644 --- a/apps/web/app/(org)/dashboard/settings/account/server.ts +++ b/apps/web/app/(org)/dashboard/settings/account/server.ts @@ -7,13 +7,14 @@ import { organizations, users, } from "@cap/database/schema"; +import { Organisation } from "@cap/web-domain"; import { eq, or } from "drizzle-orm"; import { revalidatePath } from "next/cache"; export async function patchAccountSettings( firstName?: string, lastName?: string, - defaultOrgId?: string, + defaultOrgId?: Organisation.OrganisationId, ) { const currentUser = await getCurrentUser(); if (!currentUser) throw new Error("Unauthorized"); @@ -21,7 +22,7 @@ export async function patchAccountSettings( const updatePayload: Partial<{ name: string; lastName: string; - defaultOrgId: string; + defaultOrgId: Organisation.OrganisationId; }> = {}; if (firstName !== undefined) updatePayload.name = firstName; diff --git a/apps/web/app/(org)/dashboard/settings/organization/components/AccessEmailDomain.tsx b/apps/web/app/(org)/dashboard/settings/organization/components/AccessEmailDomain.tsx index b00c3ecd96..030e808c23 100644 --- a/apps/web/app/(org)/dashboard/settings/organization/components/AccessEmailDomain.tsx +++ b/apps/web/app/(org)/dashboard/settings/organization/components/AccessEmailDomain.tsx @@ -4,6 +4,7 @@ import { useState } from "react"; import { toast } from "sonner"; import { updateOrganizationDetails } from "@/actions/organization/update-details"; import { useDashboardContext } from "../../../Contexts"; +import { Organisation } from "@cap/web-domain"; export const AccessEmailDomain = () => { const { activeOrganization } = useDashboardContext(); @@ -18,7 +19,8 @@ export const AccessEmailDomain = () => { setSaveLoading(true); await updateOrganizationDetails({ allowedEmailDomain: emailDomain, - organizationId: activeOrganization?.organization.id as string, + organizationId: activeOrganization?.organization + .id as Organisation.OrganisationId, }); toast.success("Settings updated successfully"); router.refresh(); diff --git a/apps/web/app/(org)/dashboard/settings/organization/components/CustomDomain.tsx b/apps/web/app/(org)/dashboard/settings/organization/components/CustomDomain.tsx index f3737fcd5f..3a9a8ec3c5 100644 --- a/apps/web/app/(org)/dashboard/settings/organization/components/CustomDomain.tsx +++ b/apps/web/app/(org)/dashboard/settings/organization/components/CustomDomain.tsx @@ -17,6 +17,7 @@ import { UpgradeModal } from "@/components/UpgradeModal"; import { ConfirmationDialog } from "../../../_components/ConfirmationDialog"; import { useDashboardContext } from "../../../Contexts"; import CustomDomainDialog from "./CustomDomainDialog/CustomDomainDialog"; +import { Organisation } from "@cap/web-domain"; export function CustomDomain() { const router = useRouter(); @@ -32,7 +33,9 @@ export function CustomDomain() { const removeDomainMutation = useMutation({ mutationFn: (organizationId: string) => - removeOrganizationDomain(organizationId), + removeOrganizationDomain( + Organisation.OrganisationId.make(organizationId), + ), onSuccess: () => { setIsVerified(false); toast.success("Custom domain removed"); diff --git a/apps/web/app/(org)/dashboard/settings/organization/components/CustomDomainDialog/CustomDomainDialog.tsx b/apps/web/app/(org)/dashboard/settings/organization/components/CustomDomainDialog/CustomDomainDialog.tsx index 0df988ad91..625a2aeeaf 100644 --- a/apps/web/app/(org)/dashboard/settings/organization/components/CustomDomainDialog/CustomDomainDialog.tsx +++ b/apps/web/app/(org)/dashboard/settings/organization/components/CustomDomainDialog/CustomDomainDialog.tsx @@ -31,6 +31,7 @@ import { StepStatus, } from "./types"; import VerifyStep from "./VerifyStep"; +import { Organisation } from "@cap/web-domain"; const STEP_CONFIGS: StepConfig[] = [ { @@ -151,9 +152,12 @@ const CustomDomainDialog = ({ orgId: string; }) => { if (activeOrganization?.organization.customDomain) { - await removeOrganizationDomain(orgId); + await removeOrganizationDomain(Organisation.OrganisationId.make(orgId)); } - return await updateDomain(domain, orgId); + return await updateDomain( + domain, + Organisation.OrganisationId.make(orgId), + ); }, onSuccess: (data) => { handleNext(); @@ -181,7 +185,12 @@ const CustomDomainDialog = ({ orgId: string; showToasts: boolean; }) => { - return { data: await checkOrganizationDomain(orgId), showToasts }; + return { + data: await checkOrganizationDomain( + Organisation.OrganisationId.make(orgId), + ), + showToasts, + }; }, onSuccess: ({ data, showToasts }) => { setIsVerified(data.verified); diff --git a/apps/web/app/(org)/dashboard/settings/organization/components/InviteDialog.tsx b/apps/web/app/(org)/dashboard/settings/organization/components/InviteDialog.tsx index 6f53c2309b..45ab411e3d 100644 --- a/apps/web/app/(org)/dashboard/settings/organization/components/InviteDialog.tsx +++ b/apps/web/app/(org)/dashboard/settings/organization/components/InviteDialog.tsx @@ -18,6 +18,7 @@ import { toast } from "sonner"; import { sendOrganizationInvites } from "@/actions/organization/send-invites"; import { calculateSeats } from "@/utils/organization"; import { useDashboardContext } from "../../../Contexts"; +import { Organisation } from "@cap/web-domain"; interface InviteDialogProps { isOpen: boolean; @@ -95,7 +96,7 @@ export const InviteDialog = ({ return await sendOrganizationInvites( inviteEmails, - activeOrganization?.organization.id as string, + activeOrganization?.organization.id as Organisation.OrganisationId, ); }, onSuccess: () => { diff --git a/apps/web/app/(org)/dashboard/settings/organization/components/MembersCard.tsx b/apps/web/app/(org)/dashboard/settings/organization/components/MembersCard.tsx index 3359170943..7d9b0f8924 100644 --- a/apps/web/app/(org)/dashboard/settings/organization/components/MembersCard.tsx +++ b/apps/web/app/(org)/dashboard/settings/organization/components/MembersCard.tsx @@ -54,7 +54,7 @@ export const MembersCard = ({ try { await removeOrganizationInvite( inviteId, - activeOrganization?.organization.id as string, + activeOrganization!.organization.id, ); toast.success("Invite deleted successfully"); router.refresh(); @@ -90,7 +90,7 @@ export const MembersCard = ({ try { await removeOrganizationMember( pendingMember.id, - activeOrganization?.organization.id as string, + activeOrganization!.organization.id, ); toast.success("Member removed successfully"); setConfirmOpen(false); @@ -126,7 +126,7 @@ export const MembersCard = ({ title="Remove member" description={ pendingMemberTest - ? `Are you sure you want to remove ${pendingMemberTest.name} + ? `Are you sure you want to remove ${pendingMemberTest.name} from your organization? this action cannot be undone.` : "" } diff --git a/apps/web/app/(org)/dashboard/settings/organization/components/OrgName.tsx b/apps/web/app/(org)/dashboard/settings/organization/components/OrgName.tsx index 952afff150..5917c18212 100644 --- a/apps/web/app/(org)/dashboard/settings/organization/components/OrgName.tsx +++ b/apps/web/app/(org)/dashboard/settings/organization/components/OrgName.tsx @@ -19,7 +19,7 @@ const OrgName = () => { setSaveLoading(true); await updateOrganizationDetails({ organizationName: orgName, - organizationId: activeOrganization?.organization.id as string, + organizationId: activeOrganization!.organization.id, }); toast.success("Settings updated successfully"); router.refresh(); diff --git a/apps/web/app/(org)/dashboard/spaces/[spaceId]/SharedCaps.tsx b/apps/web/app/(org)/dashboard/spaces/[spaceId]/SharedCaps.tsx index 2519774669..d4bd062996 100644 --- a/apps/web/app/(org)/dashboard/spaces/[spaceId]/SharedCaps.tsx +++ b/apps/web/app/(org)/dashboard/spaces/[spaceId]/SharedCaps.tsx @@ -2,7 +2,7 @@ import type { VideoMetadata } from "@cap/database/types"; import { Button } from "@cap/ui"; -import type { Video } from "@cap/web-domain"; +import type { Organisation, Space, User, Video } from "@cap/web-domain"; import { faFolderPlus, faInfoCircle } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useQuery } from "@tanstack/react-query"; @@ -36,10 +36,10 @@ type SharedVideoData = { }[]; type SpaceData = { - id: string; + id: Space.SpaceIdOrOrganisationId; name: string; - organizationId: string; - createdById: string; + organizationId: Organisation.OrganisationId; + createdById: User.UserId; }; export const SharedCaps = ({ @@ -60,12 +60,12 @@ export const SharedCaps = ({ hideSharedWith?: boolean; spaceMembers?: SpaceMemberData[]; organizationMembers?: OrganizationMemberData[]; - currentUserId?: string; + currentUserId?: User.UserId; folders?: FolderDataType[]; organizationData?: { - id: string; + id: Organisation.OrganisationId; name: string; - ownerId: string; + ownerId: User.UserId; }; }) => { const params = useSearchParams(); diff --git a/apps/web/app/(org)/dashboard/spaces/[spaceId]/actions.ts b/apps/web/app/(org)/dashboard/spaces/[spaceId]/actions.ts index e641b35217..847ef9dc40 100644 --- a/apps/web/app/(org)/dashboard/spaces/[spaceId]/actions.ts +++ b/apps/web/app/(org)/dashboard/spaces/[spaceId]/actions.ts @@ -4,6 +4,7 @@ import { db } from "@cap/database"; import { getCurrentUser } from "@cap/database/auth/session"; import { nanoIdLength } from "@cap/database/helpers"; import { spaceMembers } from "@cap/database/schema"; +import { Space, User } from "@cap/web-domain"; import { eq, inArray } from "drizzle-orm"; import { revalidatePath } from "next/cache"; import { v4 as uuidv4 } from "uuid"; @@ -12,14 +13,14 @@ import { z } from "zod"; const spaceRole = z.union([z.literal("Admin"), z.literal("member")]); const addSpaceMemberSchema = z.object({ - spaceId: z.string(), - userId: z.string(), + spaceId: z.string().transform((v) => Space.SpaceId.make(v)), + userId: z.string().transform((v) => User.UserId.make(v)), role: spaceRole, }); const addSpaceMembersSchema = z.object({ - spaceId: z.string(), - userIds: z.array(z.string()), + spaceId: z.string().transform((v) => Space.SpaceId.make(v)), + userIds: z.array(z.string().transform((v) => User.UserId.make(v))), role: spaceRole, }); @@ -149,8 +150,10 @@ export async function removeSpaceMember( // Replace all members for a space const setSpaceMembersSchema = z.object({ - spaceId: z.string(), - userIds: z.array(z.string()), + spaceId: z + .string() + .transform((v) => Space.SpaceId.make(v) as Space.SpaceIdOrOrganisationId), + userIds: z.array(z.string().transform((v) => User.UserId.make(v))), role: spaceRole.default("member"), }); @@ -174,7 +177,7 @@ export async function setSpaceMembers( if (userIds.length > 0) { const now = new Date(); const values = userIds.map((userId) => ({ - id: uuidv4().substring(0, nanoIdLength), + id: User.UserId.make(uuidv4().substring(0, nanoIdLength)), spaceId, userId, role, diff --git a/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/AddVideosDialog.tsx b/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/AddVideosDialog.tsx index efb3bc9439..10d7f4d497 100644 --- a/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/AddVideosDialog.tsx +++ b/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/AddVideosDialog.tsx @@ -6,11 +6,12 @@ import { getSpaceVideoIds } from "@/actions/spaces/get-space-videos"; import { removeVideosFromSpace } from "@/actions/spaces/remove-videos"; import { getUserVideos } from "@/actions/videos/get-user-videos"; import AddVideosDialogBase from "./AddVideosDialogBase"; +import { Space } from "@cap/web-domain"; interface AddVideosDialogProps { open: boolean; onClose: () => void; - spaceId: string; + spaceId: Space.SpaceIdOrOrganisationId; spaceName: string; onVideosAdded?: () => void; } diff --git a/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/AddVideosDialogBase.tsx b/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/AddVideosDialogBase.tsx index cb3d7a0134..3ec721dab6 100644 --- a/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/AddVideosDialogBase.tsx +++ b/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/AddVideosDialogBase.tsx @@ -23,16 +23,16 @@ import { toast } from "sonner"; import * as z from "zod"; import VirtualizedVideoGrid from "./VirtualizedVideoGrid"; -interface AddVideosDialogBaseProps { +interface AddVideosDialogBaseProp { open: boolean; onClose: () => void; - entityId: string; + entityId: T; entityName: string; onVideosAdded?: () => void; - addVideos: (entityId: string, videoIds: Video.VideoId[]) => Promise; - removeVideos: (entityId: string, videoIds: Video.VideoId[]) => Promise; + addVideos: (entityId: T, videoIds: Video.VideoId[]) => Promise; + removeVideos: (entityId: T, videoIds: Video.VideoId[]) => Promise; getVideos: (limit?: number) => Promise; - getEntityVideoIds: (entityId: string) => Promise; + getEntityVideoIds: (entityId: T) => Promise; } export interface VideoData { @@ -52,7 +52,7 @@ const formSchema = z.object({ search: z.string(), }); -const AddVideosDialogBase: React.FC = ({ +function AddVideosDialogBase({ open, onClose, entityId, @@ -62,7 +62,7 @@ const AddVideosDialogBase: React.FC = ({ removeVideos, getVideos, getEntityVideoIds, -}) => { +}: AddVideosDialogBaseProp) { const [selectedVideos, setSelectedVideos] = useState([]); const [searchTerm, setSearchTerm] = useState(""); const filterTabs = ["all", "added", "notAdded"]; @@ -334,6 +334,6 @@ const AddVideosDialogBase: React.FC = ({ ); -}; +} export default AddVideosDialogBase; diff --git a/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/MembersIndicator.tsx b/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/MembersIndicator.tsx index d4bf09afff..4408b13f73 100644 --- a/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/MembersIndicator.tsx +++ b/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/MembersIndicator.tsx @@ -25,12 +25,13 @@ import { useDashboardContext } from "../../../Contexts"; import { setSpaceMembers } from "../actions"; import type { SpaceMemberData } from "../page"; import { MemberSelect } from "./MemberSelect"; +import { Space, User } from "@cap/web-domain"; type MembersIndicatorProps = { memberCount: number; members: SpaceMemberData[]; organizationMembers: SpaceMemberData[]; - spaceId: string; + spaceId: Space.SpaceIdOrOrganisationId; canManageMembers: boolean; onAddVideos?: () => void; }; @@ -58,7 +59,7 @@ export const MembersIndicator = ({ }, }); - const handleSaveMembers = async (selectedUserIds: string[]) => { + const handleSaveMembers = async (selectedUserIds: User.UserId[]) => { if (!canManageMembers) return; // Compare selectedUserIds to current members' userIds (order-insensitive) @@ -196,7 +197,11 @@ export const MembersIndicator = ({ {canManageMembers && (