From fb6c2ce03b3de084936e9ddaeda9ebe84ba115b4 Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Wed, 1 Apr 2026 22:38:35 -0700 Subject: [PATCH 1/9] wip --- .../migration.sql | 11 +++ packages/db/prisma/schema.prisma | 1 - packages/web/src/__mocks__/prisma.ts | 3 +- packages/web/src/actions.ts | 90 ++++--------------- .../app/{[domain] => (app)}/agents/page.tsx | 10 +-- .../askgh/[owner]/[repo]/api.ts | 0 .../[owner]/[repo]/components/landingPage.tsx | 2 +- .../[repo]/components/repoIndexedGuard.tsx | 0 .../askgh/[owner]/[repo]/page.tsx | 2 - .../askgh/[owner]/[repo]/types.ts | 0 .../app/{[domain] => (app)}/askgh/layout.tsx | 4 +- .../app/{[domain] => (app)}/browse/README.md | 0 .../[...path]/components/codePreviewPanel.tsx | 2 +- .../components/pureCodePreviewPanel.tsx | 0 .../components/pureTreePreviewPanel.tsx | 2 +- .../components/rangeHighlightingExtension.ts | 0 .../[...path]/components/treePreviewPanel.tsx | 2 +- .../browse/[...path]/page.tsx | 1 - .../browse/browseStateProvider.tsx | 0 .../browse/components/bottomPanel.tsx | 4 +- .../components/fileSearchCommandDialog.tsx | 2 +- .../components/fileTreeItemComponent.tsx | 0 .../browse/components/fileTreeItemIcon.tsx | 0 .../browse/components/fileTreePanel.tsx | 4 +- .../browse/components/pureFileTreePanel.tsx | 4 +- .../browse/hooks/useBrowseNavigation.ts | 0 .../browse/hooks/useBrowseParams.ts | 0 .../browse/hooks/useBrowseState.ts | 0 .../browse/hooks/utils.test.ts | 0 .../{[domain] => (app)}/browse/hooks/utils.ts | 4 +- .../app/{[domain] => (app)}/browse/layout.tsx | 0 .../browse/layoutClient.tsx | 6 +- .../chat/[id]/components/chatThreadPanel.tsx | 0 .../chat/[id]/opengraph-image.tsx | 10 +-- .../{[domain] => (app)}/chat/[id]/page.tsx | 15 +--- .../chat/components/chatActionsDropdown.tsx | 0 .../chat/components/chatName.tsx | 11 +-- .../chat/components/chatSidePanel.tsx | 11 ++- .../chat/components/deleteChatDialog.tsx | 0 .../chat/components/demoCards.tsx | 0 .../chat/components/duplicateChatDialog.tsx | 0 .../chat/components/landingPageChatBox.tsx | 0 .../chat/components/renameChatDialog.tsx | 0 .../shareChatPopover/ee/invitePanel.tsx | 0 .../components/shareChatPopover/index.tsx | 0 .../shareChatPopover/shareSettings.tsx | 0 .../chat/components/tutorialDialog.tsx | 0 .../app/{[domain] => (app)}/chat/layout.tsx | 0 .../src/app/{[domain] => (app)}/chat/page.tsx | 13 +-- .../app/{[domain] => (app)}/chat/useChatId.ts | 0 .../components/DisplayDate.tsx | 0 .../components/appearanceDropdownMenu.tsx | 0 .../appearanceDropdownMenuGroup.tsx | 0 .../components/backButton.tsx | 0 .../components/copyIconButton.tsx | 0 .../components/editorContextMenu.tsx | 7 +- .../components/gcpIapAuth.tsx | 0 .../components/githubStarToast.tsx | 0 .../components/lightweightCodeHighlighter.tsx | 0 .../components/meControlDropdownMenu.tsx | 5 +- .../mobileUnsupportedSplashScreen.tsx | 0 .../components/navigationMenu/index.tsx | 11 +-- .../navigationMenu/navigationItems.tsx | 22 +++-- .../navigationMenu/progressIndicator.tsx | 9 +- .../components/notFound.tsx | 0 .../components/notificationDot.tsx | 0 .../components/onboardGuard.tsx | 0 .../components/pageNotFound.tsx | 0 .../components/pathHeader.tsx | 0 .../components/pendingApproval.tsx | 0 .../components/permissionSyncBanner.tsx | 0 .../components/repositoryCarousel.tsx | 7 +- .../components/searchBar/constants.ts | 0 .../components/searchBar/index.ts | 0 .../components/searchBar/searchAssistBox.tsx | 0 .../components/searchBar/searchBar.tsx | 6 +- .../searchBar/searchSuggestionsBox.test.tsx | 0 .../searchBar/searchSuggestionsBox.tsx | 2 +- .../searchBar/useRefineModeSuggestions.ts | 0 .../searchBar/useSuggestionModeAndQuery.ts | 0 .../searchBar/useSuggestionModeMappings.ts | 0 .../searchBar/useSuggestionsData.ts | 8 +- .../searchBar/zoektLanguageExtension.ts | 0 .../components/searchModeSelector.tsx | 6 +- .../components/submitAccountRequestButton.tsx | 6 +- .../components/submitJoinRequest.tsx | 8 +- .../components/syntaxGuideProvider.tsx | 0 .../components/syntaxReferenceGuide.tsx | 0 .../components/syntaxReferenceGuideHint.tsx | 0 .../{[domain] => (app)}/components/topBar.tsx | 4 +- .../components/upgradeToast.tsx | 0 .../components/whatsNewIndicator.tsx | 0 .../src/app/{[domain] => (app)}/layout.tsx | 23 ++--- .../web/src/app/{[domain] => (app)}/page.tsx | 1 - .../{[domain] => (app)}/repos/[id]/page.tsx | 3 +- .../repos/components/repoActionsDropdown.tsx | 3 +- .../repos/components/repoBranchesTable.tsx | 0 .../repos/components/repoJobsTable.tsx | 0 .../repos/components/reposTable.tsx | 3 +- .../app/{[domain] => (app)}/repos/layout.tsx | 8 +- .../app/{[domain] => (app)}/repos/page.tsx | 0 .../codePreviewPanel/codePreview.tsx | 4 +- .../components/codePreviewPanel/index.tsx | 0 .../search/components/filterPanel/entry.tsx | 0 .../search/components/filterPanel/filter.tsx | 0 .../search/components/filterPanel/index.tsx | 0 .../filterPanel/useFilterMatches.ts | 0 .../filterPanel/useGetSelectedFromQuery.ts | 0 .../search/components/searchLandingPage.tsx | 34 ++++--- .../search/components/searchResultsPage.tsx | 7 +- .../searchResultsPanel/fileMatch.tsx | 4 +- .../searchResultsPanel/fileMatchContainer.tsx | 2 +- .../components/searchResultsPanel/index.tsx | 0 .../app/{[domain] => (app)}/search/page.tsx | 4 +- .../search/useStreamedSearch.ts | 0 .../settings/access/page.tsx | 21 ++--- .../settings/analytics/page.tsx | 21 ++--- .../settings/apiKeys/apiKeysPage.tsx | 0 .../settings/apiKeys/columns.tsx | 0 .../src/app/(app)/settings/apiKeys/layout.tsx | 31 +++++++ .../settings/apiKeys/page.tsx | 9 +- .../settings/components/sidebar-nav.tsx | 2 +- .../settings/connections/[id]/page.tsx | 7 +- .../components/connectionJobsTable.tsx | 6 +- .../components/connectionsTable.tsx | 7 +- .../settings/connections/layout.tsx | 12 +-- .../settings/connections/page.tsx | 0 .../{[domain] => (app)}/settings/layout.tsx | 28 +++--- .../settings/license/page.tsx | 21 ++--- .../settings/linked-accounts/page.tsx | 0 .../members/components/inviteMemberCard.tsx | 0 .../members/components/invitesList.tsx | 0 .../members/components/membersList.tsx | 0 .../members/components/requestsList.tsx | 0 .../settings/members/page.tsx | 16 +--- .../app/{[domain] => (app)}/settings/page.tsx | 3 +- .../app/[domain]/settings/apiKeys/layout.tsx | 42 --------- .../(server)/repo-status/[repoId]/route.ts | 2 +- .../app/api/(server)/repos/listReposApi.ts | 2 +- .../repos/[repoId]/image/route.ts | 4 +- .../components/organizationAccessSettings.tsx | 6 +- packages/web/src/app/invite/page.tsx | 7 +- packages/web/src/app/login/page.tsx | 8 +- packages/web/src/app/not-found.tsx | 2 +- packages/web/src/app/onboard/page.tsx | 7 +- packages/web/src/app/page.tsx | 30 ------- .../redeem/components/acceptInviteCard.tsx | 7 +- packages/web/src/app/redeem/page.tsx | 7 +- packages/web/src/app/signup/page.tsx | 8 +- packages/web/src/data/org.ts | 12 --- .../features/analytics/analyticsContent.tsx | 4 +- .../codeNav/components/exploreMenu/index.tsx | 8 +- .../components/exploreMenu/referenceList.tsx | 6 +- .../components/symbolHoverPopup/index.tsx | 2 +- .../symbolDefinitionPreview.tsx | 2 +- .../useHoveredOverSymbolInfo.ts | 6 +- .../src/emails/joinRequestApprovedEmail.tsx | 5 +- .../src/emails/joinRequestSubmittedEmail.tsx | 5 +- .../components/chatBox/useSuggestionsData.ts | 5 +- .../chat/components/chatThread/answerCard.tsx | 2 +- .../chat/components/chatThread/chatThread.tsx | 5 +- .../chat/components/chatThread/codeBlock.tsx | 2 +- .../chatThread/markdownRenderer.tsx | 3 +- .../referencedFileSourceListItem.tsx | 2 +- .../components/chatThread/tools/fileRow.tsx | 2 +- .../tools/listTreeToolComponent.tsx | 2 +- .../tools/readFileToolComponent.tsx | 2 +- .../components/chatThread/tools/repoBadge.tsx | 2 +- .../chatThread/tools/repoHeader.tsx | 2 +- .../chatThread/tools/toolOutputGuard.tsx | 2 +- .../features/chat/useCreateNewChatThread.ts | 3 +- packages/web/src/features/chat/utils.ts | 2 +- .../web/src/features/git/getFileSourceApi.ts | 2 +- packages/web/src/features/mcp/askCodebase.ts | 2 +- .../web/src/features/search/zoektSearcher.ts | 2 +- packages/web/src/hooks/useDomain.ts | 8 -- packages/web/src/initialize.ts | 11 ++- packages/web/src/lib/authUtils.ts | 17 ++-- packages/web/src/lib/constants.ts | 1 - packages/web/src/lib/utils.ts | 3 +- packages/web/src/proxy.ts | 36 -------- 181 files changed, 281 insertions(+), 614 deletions(-) create mode 100644 packages/db/prisma/migrations/20260402052154_remove_domain_from_org/migration.sql rename packages/web/src/app/{[domain] => (app)}/agents/page.tsx (93%) rename packages/web/src/app/{[domain] => (app)}/askgh/[owner]/[repo]/api.ts (100%) rename packages/web/src/app/{[domain] => (app)}/askgh/[owner]/[repo]/components/landingPage.tsx (98%) rename packages/web/src/app/{[domain] => (app)}/askgh/[owner]/[repo]/components/repoIndexedGuard.tsx (100%) rename packages/web/src/app/{[domain] => (app)}/askgh/[owner]/[repo]/page.tsx (95%) rename packages/web/src/app/{[domain] => (app)}/askgh/[owner]/[repo]/types.ts (100%) rename packages/web/src/app/{[domain] => (app)}/askgh/layout.tsx (69%) rename packages/web/src/app/{[domain] => (app)}/browse/README.md (100%) rename packages/web/src/app/{[domain] => (app)}/browse/[...path]/components/codePreviewPanel.tsx (98%) rename packages/web/src/app/{[domain] => (app)}/browse/[...path]/components/pureCodePreviewPanel.tsx (100%) rename packages/web/src/app/{[domain] => (app)}/browse/[...path]/components/pureTreePreviewPanel.tsx (93%) rename packages/web/src/app/{[domain] => (app)}/browse/[...path]/components/rangeHighlightingExtension.ts (100%) rename packages/web/src/app/{[domain] => (app)}/browse/[...path]/components/treePreviewPanel.tsx (96%) rename packages/web/src/app/{[domain] => (app)}/browse/[...path]/page.tsx (99%) rename packages/web/src/app/{[domain] => (app)}/browse/browseStateProvider.tsx (100%) rename packages/web/src/app/{[domain] => (app)}/browse/components/bottomPanel.tsx (97%) rename packages/web/src/app/{[domain] => (app)}/browse/components/fileSearchCommandDialog.tsx (99%) rename packages/web/src/app/{[domain] => (app)}/browse/components/fileTreeItemComponent.tsx (100%) rename packages/web/src/app/{[domain] => (app)}/browse/components/fileTreeItemIcon.tsx (100%) rename packages/web/src/app/{[domain] => (app)}/browse/components/fileTreePanel.tsx (99%) rename packages/web/src/app/{[domain] => (app)}/browse/components/pureFileTreePanel.tsx (96%) rename packages/web/src/app/{[domain] => (app)}/browse/hooks/useBrowseNavigation.ts (100%) rename packages/web/src/app/{[domain] => (app)}/browse/hooks/useBrowseParams.ts (100%) rename packages/web/src/app/{[domain] => (app)}/browse/hooks/useBrowseState.ts (100%) rename packages/web/src/app/{[domain] => (app)}/browse/hooks/utils.test.ts (100%) rename packages/web/src/app/{[domain] => (app)}/browse/hooks/utils.ts (91%) rename packages/web/src/app/{[domain] => (app)}/browse/layout.tsx (100%) rename packages/web/src/app/{[domain] => (app)}/browse/layoutClient.tsx (93%) rename packages/web/src/app/{[domain] => (app)}/chat/[id]/components/chatThreadPanel.tsx (100%) rename packages/web/src/app/{[domain] => (app)}/chat/[id]/opengraph-image.tsx (96%) rename packages/web/src/app/{[domain] => (app)}/chat/[id]/page.tsx (94%) rename packages/web/src/app/{[domain] => (app)}/chat/components/chatActionsDropdown.tsx (100%) rename packages/web/src/app/{[domain] => (app)}/chat/components/chatName.tsx (92%) rename packages/web/src/app/{[domain] => (app)}/chat/components/chatSidePanel.tsx (96%) rename packages/web/src/app/{[domain] => (app)}/chat/components/deleteChatDialog.tsx (100%) rename packages/web/src/app/{[domain] => (app)}/chat/components/demoCards.tsx (100%) rename packages/web/src/app/{[domain] => (app)}/chat/components/duplicateChatDialog.tsx (100%) rename packages/web/src/app/{[domain] => (app)}/chat/components/landingPageChatBox.tsx (100%) rename packages/web/src/app/{[domain] => (app)}/chat/components/renameChatDialog.tsx (100%) rename packages/web/src/app/{[domain] => (app)}/chat/components/shareChatPopover/ee/invitePanel.tsx (100%) rename packages/web/src/app/{[domain] => (app)}/chat/components/shareChatPopover/index.tsx (100%) rename packages/web/src/app/{[domain] => (app)}/chat/components/shareChatPopover/shareSettings.tsx (100%) rename packages/web/src/app/{[domain] => (app)}/chat/components/tutorialDialog.tsx (100%) rename packages/web/src/app/{[domain] => (app)}/chat/layout.tsx (100%) rename packages/web/src/app/{[domain] => (app)}/chat/page.tsx (94%) rename packages/web/src/app/{[domain] => (app)}/chat/useChatId.ts (100%) rename packages/web/src/app/{[domain] => (app)}/components/DisplayDate.tsx (100%) rename packages/web/src/app/{[domain] => (app)}/components/appearanceDropdownMenu.tsx (100%) rename packages/web/src/app/{[domain] => (app)}/components/appearanceDropdownMenuGroup.tsx (100%) rename packages/web/src/app/{[domain] => (app)}/components/backButton.tsx (100%) rename packages/web/src/app/{[domain] => (app)}/components/copyIconButton.tsx (100%) rename packages/web/src/app/{[domain] => (app)}/components/editorContextMenu.tsx (94%) rename packages/web/src/app/{[domain] => (app)}/components/gcpIapAuth.tsx (100%) rename packages/web/src/app/{[domain] => (app)}/components/githubStarToast.tsx (100%) rename packages/web/src/app/{[domain] => (app)}/components/lightweightCodeHighlighter.tsx (100%) rename packages/web/src/app/{[domain] => (app)}/components/meControlDropdownMenu.tsx (95%) rename packages/web/src/app/{[domain] => (app)}/components/mobileUnsupportedSplashScreen.tsx (100%) rename packages/web/src/app/{[domain] => (app)}/components/navigationMenu/index.tsx (95%) rename packages/web/src/app/{[domain] => (app)}/components/navigationMenu/navigationItems.tsx (83%) rename packages/web/src/app/{[domain] => (app)}/components/navigationMenu/progressIndicator.tsx (91%) rename packages/web/src/app/{[domain] => (app)}/components/notFound.tsx (100%) rename packages/web/src/app/{[domain] => (app)}/components/notificationDot.tsx (100%) rename packages/web/src/app/{[domain] => (app)}/components/onboardGuard.tsx (100%) rename packages/web/src/app/{[domain] => (app)}/components/pageNotFound.tsx (100%) rename packages/web/src/app/{[domain] => (app)}/components/pathHeader.tsx (100%) rename packages/web/src/app/{[domain] => (app)}/components/pendingApproval.tsx (100%) rename packages/web/src/app/{[domain] => (app)}/components/permissionSyncBanner.tsx (100%) rename packages/web/src/app/{[domain] => (app)}/components/repositoryCarousel.tsx (93%) rename packages/web/src/app/{[domain] => (app)}/components/searchBar/constants.ts (100%) rename packages/web/src/app/{[domain] => (app)}/components/searchBar/index.ts (100%) rename packages/web/src/app/{[domain] => (app)}/components/searchBar/searchAssistBox.tsx (100%) rename packages/web/src/app/{[domain] => (app)}/components/searchBar/searchBar.tsx (98%) rename packages/web/src/app/{[domain] => (app)}/components/searchBar/searchSuggestionsBox.test.tsx (100%) rename packages/web/src/app/{[domain] => (app)}/components/searchBar/searchSuggestionsBox.tsx (99%) rename packages/web/src/app/{[domain] => (app)}/components/searchBar/useRefineModeSuggestions.ts (100%) rename packages/web/src/app/{[domain] => (app)}/components/searchBar/useSuggestionModeAndQuery.ts (100%) rename packages/web/src/app/{[domain] => (app)}/components/searchBar/useSuggestionModeMappings.ts (100%) rename packages/web/src/app/{[domain] => (app)}/components/searchBar/useSuggestionsData.ts (96%) rename packages/web/src/app/{[domain] => (app)}/components/searchBar/zoektLanguageExtension.ts (100%) rename packages/web/src/app/{[domain] => (app)}/components/searchModeSelector.tsx (97%) rename packages/web/src/app/{[domain] => (app)}/components/submitAccountRequestButton.tsx (89%) rename packages/web/src/app/{[domain] => (app)}/components/submitJoinRequest.tsx (90%) rename packages/web/src/app/{[domain] => (app)}/components/syntaxGuideProvider.tsx (100%) rename packages/web/src/app/{[domain] => (app)}/components/syntaxReferenceGuide.tsx (100%) rename packages/web/src/app/{[domain] => (app)}/components/syntaxReferenceGuideHint.tsx (100%) rename packages/web/src/app/{[domain] => (app)}/components/topBar.tsx (98%) rename packages/web/src/app/{[domain] => (app)}/components/upgradeToast.tsx (100%) rename packages/web/src/app/{[domain] => (app)}/components/whatsNewIndicator.tsx (100%) rename packages/web/src/app/{[domain] => (app)}/layout.tsx (93%) rename packages/web/src/app/{[domain] => (app)}/page.tsx (85%) rename packages/web/src/app/{[domain] => (app)}/repos/[id]/page.tsx (98%) rename packages/web/src/app/{[domain] => (app)}/repos/components/repoActionsDropdown.tsx (94%) rename packages/web/src/app/{[domain] => (app)}/repos/components/repoBranchesTable.tsx (100%) rename packages/web/src/app/{[domain] => (app)}/repos/components/repoJobsTable.tsx (100%) rename packages/web/src/app/{[domain] => (app)}/repos/components/reposTable.tsx (99%) rename packages/web/src/app/{[domain] => (app)}/repos/layout.tsx (78%) rename packages/web/src/app/{[domain] => (app)}/repos/page.tsx (100%) rename packages/web/src/app/{[domain] => (app)}/search/components/codePreviewPanel/codePreview.tsx (98%) rename packages/web/src/app/{[domain] => (app)}/search/components/codePreviewPanel/index.tsx (100%) rename packages/web/src/app/{[domain] => (app)}/search/components/filterPanel/entry.tsx (100%) rename packages/web/src/app/{[domain] => (app)}/search/components/filterPanel/filter.tsx (100%) rename packages/web/src/app/{[domain] => (app)}/search/components/filterPanel/index.tsx (100%) rename packages/web/src/app/{[domain] => (app)}/search/components/filterPanel/useFilterMatches.ts (100%) rename packages/web/src/app/{[domain] => (app)}/search/components/filterPanel/useGetSelectedFromQuery.ts (100%) rename packages/web/src/app/{[domain] => (app)}/search/components/searchLandingPage.tsx (70%) rename packages/web/src/app/{[domain] => (app)}/search/components/searchResultsPage.tsx (98%) rename packages/web/src/app/{[domain] => (app)}/search/components/searchResultsPanel/fileMatch.tsx (90%) rename packages/web/src/app/{[domain] => (app)}/search/components/searchResultsPanel/fileMatchContainer.tsx (98%) rename packages/web/src/app/{[domain] => (app)}/search/components/searchResultsPanel/index.tsx (100%) rename packages/web/src/app/{[domain] => (app)}/search/page.tsx (87%) rename packages/web/src/app/{[domain] => (app)}/search/useStreamedSearch.ts (100%) rename packages/web/src/app/{[domain] => (app)}/settings/access/page.tsx (80%) rename packages/web/src/app/{[domain] => (app)}/settings/analytics/page.tsx (75%) rename packages/web/src/app/{[domain] => (app)}/settings/apiKeys/apiKeysPage.tsx (100%) rename packages/web/src/app/{[domain] => (app)}/settings/apiKeys/columns.tsx (100%) create mode 100644 packages/web/src/app/(app)/settings/apiKeys/layout.tsx rename packages/web/src/app/{[domain] => (app)}/settings/apiKeys/page.tsx (68%) rename packages/web/src/app/{[domain] => (app)}/settings/components/sidebar-nav.tsx (95%) rename packages/web/src/app/{[domain] => (app)}/settings/connections/[id]/page.tsx (96%) rename packages/web/src/app/{[domain] => (app)}/settings/connections/components/connectionJobsTable.tsx (98%) rename packages/web/src/app/{[domain] => (app)}/settings/connections/components/connectionsTable.tsx (97%) rename packages/web/src/app/{[domain] => (app)}/settings/connections/layout.tsx (72%) rename packages/web/src/app/{[domain] => (app)}/settings/connections/page.tsx (100%) rename packages/web/src/app/{[domain] => (app)}/settings/layout.tsx (80%) rename packages/web/src/app/{[domain] => (app)}/settings/license/page.tsx (94%) rename packages/web/src/app/{[domain] => (app)}/settings/linked-accounts/page.tsx (100%) rename packages/web/src/app/{[domain] => (app)}/settings/members/components/inviteMemberCard.tsx (100%) rename packages/web/src/app/{[domain] => (app)}/settings/members/components/invitesList.tsx (100%) rename packages/web/src/app/{[domain] => (app)}/settings/members/components/membersList.tsx (100%) rename packages/web/src/app/{[domain] => (app)}/settings/members/components/requestsList.tsx (100%) rename packages/web/src/app/{[domain] => (app)}/settings/members/page.tsx (96%) rename packages/web/src/app/{[domain] => (app)}/settings/page.tsx (81%) delete mode 100644 packages/web/src/app/[domain]/settings/apiKeys/layout.tsx rename packages/web/src/app/api/{[domain] => }/repos/[repoId]/image/route.ts (90%) delete mode 100644 packages/web/src/app/page.tsx delete mode 100644 packages/web/src/data/org.ts delete mode 100644 packages/web/src/hooks/useDomain.ts delete mode 100644 packages/web/src/proxy.ts diff --git a/packages/db/prisma/migrations/20260402052154_remove_domain_from_org/migration.sql b/packages/db/prisma/migrations/20260402052154_remove_domain_from_org/migration.sql new file mode 100644 index 000000000..2f22971e4 --- /dev/null +++ b/packages/db/prisma/migrations/20260402052154_remove_domain_from_org/migration.sql @@ -0,0 +1,11 @@ +/* + Warnings: + + - You are about to drop the column `domain` on the `Org` table. All the data in the column will be lost. + +*/ +-- DropIndex +DROP INDEX "Org_domain_key"; + +-- AlterTable +ALTER TABLE "Org" DROP COLUMN "domain"; diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index 0c58714b6..3a96eea6e 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -265,7 +265,6 @@ model AccountRequest { model Org { id Int @id @default(autoincrement()) name String - domain String @unique createdAt DateTime @default(now()) updatedAt DateTime @updatedAt members UserToOrg[] diff --git a/packages/web/src/__mocks__/prisma.ts b/packages/web/src/__mocks__/prisma.ts index acf687294..d142ad1e1 100644 --- a/packages/web/src/__mocks__/prisma.ts +++ b/packages/web/src/__mocks__/prisma.ts @@ -1,4 +1,4 @@ -import { SINGLE_TENANT_ORG_DOMAIN, SINGLE_TENANT_ORG_ID, SINGLE_TENANT_ORG_NAME } from '@/lib/constants'; +import { SINGLE_TENANT_ORG_ID, SINGLE_TENANT_ORG_NAME } from '@/lib/constants'; import { Account, ApiKey, OAuthRefreshToken, OAuthToken, Org, PrismaClient, User } from '@prisma/client'; import { beforeEach, vi } from 'vitest'; import { mockDeep, mockReset } from 'vitest-mock-extended'; @@ -12,7 +12,6 @@ export const prisma = mockDeep(); export const MOCK_ORG: Org = { id: SINGLE_TENANT_ORG_ID, name: SINGLE_TENANT_ORG_NAME, - domain: SINGLE_TENANT_ORG_DOMAIN, createdAt: new Date(), updatedAt: new Date(), isOnboarded: true, diff --git a/packages/web/src/actions.ts b/packages/web/src/actions.ts index ad5d5f24e..854f3ed72 100644 --- a/packages/web/src/actions.ts +++ b/packages/web/src/actions.ts @@ -8,8 +8,8 @@ import { notFound, orgNotFound, ServiceError } from "@/lib/serviceError"; import { getOrgMetadata, isHttpError, isServiceError } from "@/lib/utils"; import { prisma } from "@/prisma"; import { render } from "@react-email/components"; -import { generateApiKey, getTokenFromConfig, hashSecret } from "@sourcebot/shared"; -import { ApiKey, ConnectionSyncJobStatus, OrgRole, Prisma, RepoIndexingJobStatus, RepoIndexingJobType } from "@sourcebot/db"; +import { generateApiKey, getTokenFromConfig } from "@sourcebot/shared"; +import { ConnectionSyncJobStatus, OrgRole, Prisma, RepoIndexingJobStatus, RepoIndexingJobType } from "@sourcebot/db"; import { createLogger } from "@sourcebot/shared"; import { GiteaConnectionConfig } from "@sourcebot/schemas/v3/gitea.type"; import { GithubConnectionConfig } from "@sourcebot/schemas/v3/github.type"; @@ -19,15 +19,14 @@ import { StatusCodes } from "http-status-codes"; import { cookies } from "next/headers"; import { createTransport } from "nodemailer"; import { Octokit } from "octokit"; -import { getOrgFromDomain } from "./data/org"; import InviteUserEmail from "./emails/inviteUserEmail"; import JoinRequestApprovedEmail from "./emails/joinRequestApprovedEmail"; import JoinRequestSubmittedEmail from "./emails/joinRequestSubmittedEmail"; -import { AGENTIC_SEARCH_TUTORIAL_DISMISSED_COOKIE_NAME, MOBILE_UNSUPPORTED_SPLASH_SCREEN_DISMISSED_COOKIE_NAME, SOURCEBOT_SUPPORT_EMAIL } from "./lib/constants"; -import { ApiKeyPayload, RepositoryQuery } from "./lib/types"; +import { AGENTIC_SEARCH_TUTORIAL_DISMISSED_COOKIE_NAME, MOBILE_UNSUPPORTED_SPLASH_SCREEN_DISMISSED_COOKIE_NAME, SINGLE_TENANT_ORG_ID, SOURCEBOT_SUPPORT_EMAIL } from "./lib/constants"; +import { RepositoryQuery } from "./lib/types"; import { withAuth, withOptionalAuth, withAuth_skipOrgMembershipCheck } from "./middleware/withAuth"; import { withMinimumOrgRole } from "./middleware/withMinimumOrgRole"; -import { getBrowsePath } from "./app/[domain]/browse/hooks/utils"; +import { getBrowsePath } from "./app/(app)/browse/hooks/utils"; import { sew } from "@/middleware/sew"; const logger = createLogger('web-actions'); @@ -48,59 +47,6 @@ export const completeOnboarding = async (): Promise<{ success: boolean } | Servi } })); -export const verifyApiKey = async (apiKeyPayload: ApiKeyPayload): Promise<{ apiKey: ApiKey } | ServiceError> => sew(async () => { - const parts = apiKeyPayload.apiKey.split("-"); - if (parts.length !== 2 || parts[0] !== "sourcebot") { - return { - statusCode: StatusCodes.BAD_REQUEST, - errorCode: ErrorCode.INVALID_API_KEY, - message: "Invalid API key", - } satisfies ServiceError; - } - - const hash = hashSecret(parts[1]) - const apiKey = await prisma.apiKey.findUnique({ - where: { - hash, - }, - }); - - if (!apiKey) { - return { - statusCode: StatusCodes.UNAUTHORIZED, - errorCode: ErrorCode.INVALID_API_KEY, - message: "Invalid API key", - } satisfies ServiceError; - } - - const apiKeyTargetOrg = await prisma.org.findUnique({ - where: { - domain: apiKeyPayload.domain, - }, - }); - - if (!apiKeyTargetOrg) { - return { - statusCode: StatusCodes.UNAUTHORIZED, - errorCode: ErrorCode.INVALID_API_KEY, - message: `Invalid API key payload. Provided domain ${apiKeyPayload.domain} does not exist.`, - } satisfies ServiceError; - } - - if (apiKey.orgId !== apiKeyTargetOrg.id) { - return { - statusCode: StatusCodes.UNAUTHORIZED, - errorCode: ErrorCode.INVALID_API_KEY, - message: `Invalid API key payload. Provided domain ${apiKeyPayload.domain} does not match the API key's org.`, - } satisfies ServiceError; - } - - return { - apiKey, - } -}); - - export const createApiKey = async (name: string): Promise<{ key: string } | ServiceError> => sew(() => withAuth(async ({ org, user, role, prisma }) => { if ((env.DISABLE_API_KEY_CREATION_FOR_NON_OWNER_USERS === 'true' || env.DISABLE_API_KEY_USAGE_FOR_NON_OWNER_USERS === 'true') && role !== OrgRole.OWNER) { @@ -188,7 +134,7 @@ export const deleteApiKey = async (name: string): Promise<{ success: boolean } | type: "user" }, target: { - id: org.domain, + id: org.id.toString(), type: "org" }, orgId: org.id, @@ -600,7 +546,7 @@ export const createInvites = async (emails: string[]): Promise<{ success: boolea }); } - const hasAvailability = await orgHasAvailability(org.domain); + const hasAvailability = await orgHasAvailability(); if (!hasAvailability) { await auditService.createAudit({ action: "user.invite_failed", @@ -807,7 +753,6 @@ export const getMe = async () => sew(() => memberships: userWithOrgs.orgs.map((org) => ({ id: org.orgId, role: org.role, - domain: org.org.domain, name: org.org.name, })) } @@ -847,7 +792,7 @@ export const redeemInvite = async (inviteId: string): Promise<{ success: boolean } - const hasAvailability = await orgHasAvailability(invite.org.domain); + const hasAvailability = await orgHasAvailability(); if (!hasAvailability) { await failAuditCallback("Organization is at max capacity"); return { @@ -911,7 +856,6 @@ export const getInviteInfo = async (inviteId: string) => sew(() => id: invite.id, orgName: invite.org.name, orgImageUrl: invite.org.imageUrl ?? undefined, - orgDomain: invite.org.domain, host: { name: invite.host.name ?? undefined, email: invite.host.email!, @@ -983,7 +927,7 @@ export const getOrgAccountRequests = async () => sew(() => })); })); -export const createAccountRequest = async (userId: string, domain: string) => sew(async () => { +export const createAccountRequest = async (userId: string) => sew(async () => { const user = await prisma.user.findUnique({ where: { id: userId, @@ -996,7 +940,7 @@ export const createAccountRequest = async (userId: string, domain: string) => se const org = await prisma.org.findUnique({ where: { - domain, + id: SINGLE_TENANT_ORG_ID, }, }); @@ -1057,7 +1001,6 @@ export const createAccountRequest = async (userId: string, domain: string) => se avatarUrl: user.image ?? undefined, }, orgName: org.name, - orgDomain: org.domain, orgImageUrl: org.imageUrl ?? undefined, })); @@ -1086,10 +1029,10 @@ export const createAccountRequest = async (userId: string, domain: string) => se } }); -export const getMemberApprovalRequired = async (domain: string): Promise => sew(async () => { +export const getMemberApprovalRequired = async (): Promise => sew(async () => { const org = await prisma.org.findUnique({ where: { - domain, + id: SINGLE_TENANT_ORG_ID, }, }); @@ -1182,7 +1125,6 @@ export const approveAccountRequest = async (requestId: string) => sew(async () = avatarUrl: request.requestedBy.image ?? undefined, }, orgName: org.name, - orgDomain: org.domain })); const transport = createTransport(smtpConnectionUrl); @@ -1191,7 +1133,7 @@ export const approveAccountRequest = async (requestId: string) => sew(async () = from: env.EMAIL_FROM_ADDRESS, subject: `Your request to join ${org.name} has been approved`, html, - text: `Your request to join ${org.name} on Sourcebot has been approved. You can now access the organization at ${env.AUTH_URL}/${org.domain}`, + text: `Your request to join ${org.name} on Sourcebot has been approved. You can now access the organization at ${env.AUTH_URL}`, }); const failed = result.rejected.concat(result.pending).filter(Boolean); @@ -1334,8 +1276,10 @@ export const getRepoImage = async (repoId: number): Promise => sew(async () => { - const org = await getOrgFromDomain(domain); +export const getAnonymousAccessStatus = async (): Promise => sew(async () => { + const org = await prisma.org.findUnique({ + where: { id: SINGLE_TENANT_ORG_ID }, + }); if (!org) { return { statusCode: StatusCodes.NOT_FOUND, diff --git a/packages/web/src/app/[domain]/agents/page.tsx b/packages/web/src/app/(app)/agents/page.tsx similarity index 93% rename from packages/web/src/app/[domain]/agents/page.tsx rename to packages/web/src/app/(app)/agents/page.tsx index fd564268a..523198ef6 100644 --- a/packages/web/src/app/[domain]/agents/page.tsx +++ b/packages/web/src/app/(app)/agents/page.tsx @@ -13,16 +13,10 @@ const agents = [ }, ]; -export default async function AgentsPage(props: { params: Promise<{ domain: string }> }) { - const params = await props.params; - - const { - domain - } = params; - +export default async function AgentsPage() { return (
- +
; }) { - const params = await props.params; if (env.EXPERIMENT_ASK_GH_ENABLED !== 'true') { - redirect(`/${params.domain}`); + redirect('/'); } return <>{props.children}; diff --git a/packages/web/src/app/[domain]/browse/README.md b/packages/web/src/app/(app)/browse/README.md similarity index 100% rename from packages/web/src/app/[domain]/browse/README.md rename to packages/web/src/app/(app)/browse/README.md diff --git a/packages/web/src/app/[domain]/browse/[...path]/components/codePreviewPanel.tsx b/packages/web/src/app/(app)/browse/[...path]/components/codePreviewPanel.tsx similarity index 98% rename from packages/web/src/app/[domain]/browse/[...path]/components/codePreviewPanel.tsx rename to packages/web/src/app/(app)/browse/[...path]/components/codePreviewPanel.tsx index 15e2f6052..6f5725783 100644 --- a/packages/web/src/app/[domain]/browse/[...path]/components/codePreviewPanel.tsx +++ b/packages/web/src/app/(app)/browse/[...path]/components/codePreviewPanel.tsx @@ -1,5 +1,5 @@ import { getRepoInfoByName } from "@/actions"; -import { PathHeader } from "@/app/[domain]/components/pathHeader"; +import { PathHeader } from "@/app/(app)/components/pathHeader"; import { Separator } from "@/components/ui/separator"; import { cn, getCodeHostInfoForRepo, isServiceError } from "@/lib/utils"; import Image from "next/image"; diff --git a/packages/web/src/app/[domain]/browse/[...path]/components/pureCodePreviewPanel.tsx b/packages/web/src/app/(app)/browse/[...path]/components/pureCodePreviewPanel.tsx similarity index 100% rename from packages/web/src/app/[domain]/browse/[...path]/components/pureCodePreviewPanel.tsx rename to packages/web/src/app/(app)/browse/[...path]/components/pureCodePreviewPanel.tsx diff --git a/packages/web/src/app/[domain]/browse/[...path]/components/pureTreePreviewPanel.tsx b/packages/web/src/app/(app)/browse/[...path]/components/pureTreePreviewPanel.tsx similarity index 93% rename from packages/web/src/app/[domain]/browse/[...path]/components/pureTreePreviewPanel.tsx rename to packages/web/src/app/(app)/browse/[...path]/components/pureTreePreviewPanel.tsx index 6b714fb99..ca6bb7985 100644 --- a/packages/web/src/app/[domain]/browse/[...path]/components/pureTreePreviewPanel.tsx +++ b/packages/web/src/app/(app)/browse/[...path]/components/pureTreePreviewPanel.tsx @@ -1,7 +1,7 @@ 'use client'; import { useRef } from "react"; -import { FileTreeItemComponent } from "@/app/[domain]/browse/components/fileTreeItemComponent"; +import { FileTreeItemComponent } from "@/app/(app)/browse/components/fileTreeItemComponent"; import { getBrowsePath } from "../../hooks/utils"; import { ScrollArea } from "@/components/ui/scroll-area"; import { useBrowseParams } from "../../hooks/useBrowseParams"; diff --git a/packages/web/src/app/[domain]/browse/[...path]/components/rangeHighlightingExtension.ts b/packages/web/src/app/(app)/browse/[...path]/components/rangeHighlightingExtension.ts similarity index 100% rename from packages/web/src/app/[domain]/browse/[...path]/components/rangeHighlightingExtension.ts rename to packages/web/src/app/(app)/browse/[...path]/components/rangeHighlightingExtension.ts diff --git a/packages/web/src/app/[domain]/browse/[...path]/components/treePreviewPanel.tsx b/packages/web/src/app/(app)/browse/[...path]/components/treePreviewPanel.tsx similarity index 96% rename from packages/web/src/app/[domain]/browse/[...path]/components/treePreviewPanel.tsx rename to packages/web/src/app/(app)/browse/[...path]/components/treePreviewPanel.tsx index 90afe2916..5762e0f50 100644 --- a/packages/web/src/app/[domain]/browse/[...path]/components/treePreviewPanel.tsx +++ b/packages/web/src/app/(app)/browse/[...path]/components/treePreviewPanel.tsx @@ -1,7 +1,7 @@ import { Separator } from "@/components/ui/separator"; import { getRepoInfoByName } from "@/actions"; -import { PathHeader } from "@/app/[domain]/components/pathHeader"; +import { PathHeader } from "@/app/(app)/components/pathHeader"; import { getFolderContents } from "@/features/git/getFolderContentsApi"; import { isServiceError } from "@/lib/utils"; import { PureTreePreviewPanel } from "./pureTreePreviewPanel"; diff --git a/packages/web/src/app/[domain]/browse/[...path]/page.tsx b/packages/web/src/app/(app)/browse/[...path]/page.tsx similarity index 99% rename from packages/web/src/app/[domain]/browse/[...path]/page.tsx rename to packages/web/src/app/(app)/browse/[...path]/page.tsx index 2cadab600..df0174432 100644 --- a/packages/web/src/app/[domain]/browse/[...path]/page.tsx +++ b/packages/web/src/app/(app)/browse/[...path]/page.tsx @@ -44,7 +44,6 @@ const parsePathForTitle = (path: string[]): string => { type Props = { params: Promise<{ - domain: string; path: string[]; }>; }; diff --git a/packages/web/src/app/[domain]/browse/browseStateProvider.tsx b/packages/web/src/app/(app)/browse/browseStateProvider.tsx similarity index 100% rename from packages/web/src/app/[domain]/browse/browseStateProvider.tsx rename to packages/web/src/app/(app)/browse/browseStateProvider.tsx diff --git a/packages/web/src/app/[domain]/browse/components/bottomPanel.tsx b/packages/web/src/app/(app)/browse/components/bottomPanel.tsx similarity index 97% rename from packages/web/src/app/[domain]/browse/components/bottomPanel.tsx rename to packages/web/src/app/(app)/browse/components/bottomPanel.tsx index abd166d40..b8004aa51 100644 --- a/packages/web/src/app/[domain]/browse/components/bottomPanel.tsx +++ b/packages/web/src/app/(app)/browse/components/bottomPanel.tsx @@ -13,7 +13,6 @@ import { ImperativePanelHandle } from "react-resizable-panels"; import { useBrowseState } from "../hooks/useBrowseState"; import { ExploreMenu } from "@/ee/features/codeNav/components/exploreMenu"; import Link from "next/link"; -import { useDomain } from "@/hooks/useDomain"; import { useRouter } from "next/navigation"; export const BOTTOM_PANEL_MIN_SIZE = 35; @@ -27,7 +26,6 @@ interface BottomPanelProps { export const BottomPanel = ({ order }: BottomPanelProps) => { const panelRef = useRef(null); const hasCodeNavEntitlement = useHasEntitlement("code-nav"); - const domain = useDomain(); const router = useRouter(); const { @@ -105,7 +103,7 @@ export const BottomPanel = ({ order }: BottomPanelProps) => {

- Code navigation is not enabled for router.push(`/${domain}/settings/license`)}>your plan. + Code navigation is not enabled for router.push(`/settings/license`)}>your plan.

{ return ( diff --git a/packages/web/src/app/[domain]/browse/hooks/useBrowseNavigation.ts b/packages/web/src/app/(app)/browse/hooks/useBrowseNavigation.ts similarity index 100% rename from packages/web/src/app/[domain]/browse/hooks/useBrowseNavigation.ts rename to packages/web/src/app/(app)/browse/hooks/useBrowseNavigation.ts diff --git a/packages/web/src/app/[domain]/browse/hooks/useBrowseParams.ts b/packages/web/src/app/(app)/browse/hooks/useBrowseParams.ts similarity index 100% rename from packages/web/src/app/[domain]/browse/hooks/useBrowseParams.ts rename to packages/web/src/app/(app)/browse/hooks/useBrowseParams.ts diff --git a/packages/web/src/app/[domain]/browse/hooks/useBrowseState.ts b/packages/web/src/app/(app)/browse/hooks/useBrowseState.ts similarity index 100% rename from packages/web/src/app/[domain]/browse/hooks/useBrowseState.ts rename to packages/web/src/app/(app)/browse/hooks/useBrowseState.ts diff --git a/packages/web/src/app/[domain]/browse/hooks/utils.test.ts b/packages/web/src/app/(app)/browse/hooks/utils.test.ts similarity index 100% rename from packages/web/src/app/[domain]/browse/hooks/utils.test.ts rename to packages/web/src/app/(app)/browse/hooks/utils.test.ts diff --git a/packages/web/src/app/[domain]/browse/hooks/utils.ts b/packages/web/src/app/(app)/browse/hooks/utils.ts similarity index 91% rename from packages/web/src/app/[domain]/browse/hooks/utils.ts rename to packages/web/src/app/(app)/browse/hooks/utils.ts index 77cbb9ab2..e48782098 100644 --- a/packages/web/src/app/[domain]/browse/hooks/utils.ts +++ b/packages/web/src/app/(app)/browse/hooks/utils.ts @@ -1,5 +1,4 @@ import { BrowseState, SET_BROWSE_STATE_QUERY_PARAM } from "../browseStateProvider"; -import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants"; export const HIGHLIGHT_RANGE_QUERY_PARAM = 'highlightRange'; @@ -66,7 +65,6 @@ export const getBrowseParamsFromPathParam = (pathParam: string) => { export const getBrowsePath = ({ repoName, revisionName, path, pathType, highlightRange, setBrowseState, }: GetBrowsePathProps) => { - const domain = SINGLE_TENANT_ORG_DOMAIN; const params = new URLSearchParams(); if (highlightRange) { @@ -84,7 +82,7 @@ export const getBrowsePath = ({ } const encodedPath = encodeURIComponent(path); - const browsePath = `/${domain}/browse/${repoName}${revisionName ? `@${revisionName}` : ''}/-/${pathType}/${encodedPath}${params.size > 0 ? `?${params.toString()}` : ''}`; + const browsePath = `/browse/${repoName}${revisionName ? `@${revisionName}` : ''}/-/${pathType}/${encodedPath}${params.size > 0 ? `?${params.toString()}` : ''}`; return browsePath; }; diff --git a/packages/web/src/app/[domain]/browse/layout.tsx b/packages/web/src/app/(app)/browse/layout.tsx similarity index 100% rename from packages/web/src/app/[domain]/browse/layout.tsx rename to packages/web/src/app/(app)/browse/layout.tsx diff --git a/packages/web/src/app/[domain]/browse/layoutClient.tsx b/packages/web/src/app/(app)/browse/layoutClient.tsx similarity index 93% rename from packages/web/src/app/[domain]/browse/layoutClient.tsx rename to packages/web/src/app/(app)/browse/layoutClient.tsx index 8128b5717..b69dc4ad1 100644 --- a/packages/web/src/app/[domain]/browse/layoutClient.tsx +++ b/packages/web/src/app/(app)/browse/layoutClient.tsx @@ -5,10 +5,9 @@ import { BottomPanel } from "./components/bottomPanel"; import { AnimatedResizableHandle } from "@/components/ui/animatedResizableHandle"; import { BrowseStateProvider } from "./browseStateProvider"; import { FileTreePanel } from "./components/fileTreePanel"; -import { TopBar } from "@/app/[domain]/components/topBar"; +import { TopBar } from "@/app/(app)/components/topBar"; import { useBrowseParams } from "./hooks/useBrowseParams"; import { FileSearchCommandDialog } from "./components/fileSearchCommandDialog"; -import { useDomain } from "@/hooks/useDomain"; import { SearchBar } from "../components/searchBar"; import escapeStringRegexp from "escape-string-regexp"; import { Session } from "next-auth"; @@ -25,13 +24,10 @@ export function LayoutClient({ isSearchAssistSupported, }: LayoutProps) { const { repoName, revisionName } = useBrowseParams(); - const domain = useDomain(); - return (
; } export default async function Image({ params }: ImageProps) { - const { domain, id } = await params; - - const org = await getOrgFromDomain(domain); - if (!org) { - notFound(); - } + const { id } = await params; const chat = await prisma.chat.findUnique({ where: { id, - orgId: org.id, }, include: { createdBy: { diff --git a/packages/web/src/app/[domain]/chat/[id]/page.tsx b/packages/web/src/app/(app)/chat/[id]/page.tsx similarity index 94% rename from packages/web/src/app/[domain]/chat/[id]/page.tsx rename to packages/web/src/app/(app)/chat/[id]/page.tsx index 2fd31d7ad..686444fc5 100644 --- a/packages/web/src/app/[domain]/chat/[id]/page.tsx +++ b/packages/web/src/app/(app)/chat/[id]/page.tsx @@ -14,7 +14,6 @@ import { AnimatedResizableHandle } from '@/components/ui/animatedResizableHandle import { ChatSidePanel } from '../components/chatSidePanel'; import { ResizablePanelGroup } from '@/components/ui/resizable'; import { prisma } from '@/prisma'; -import { getOrgFromDomain } from '@/data/org'; import { ChatVisibility } from '@sourcebot/db'; import { Metadata } from 'next'; import { SBChatMessage } from '@/features/chat/types'; @@ -23,25 +22,16 @@ import { captureEvent } from '@/lib/posthog'; interface PageProps { params: Promise<{ - domain: string; id: string; }>; } export async function generateMetadata({ params }: PageProps): Promise { - const { domain, id } = await params; - - const org = await getOrgFromDomain(domain); - if (!org) { - return { - title: 'Chat | Sourcebot', - }; - } + const { id } = await params; const chat = await prisma.chat.findUnique({ where: { id, - orgId: org.id, }, }); @@ -154,8 +144,7 @@ export default async function Page(props: PageProps) { return (
(); - const onRenameChat = useCallback(async (newName: string): Promise => { const response = await updateChatName({ chatId: id, @@ -60,7 +57,7 @@ export const ChatName = ({ name, id, isOwner = false, isAuthenticated = false }: toast({ description: `✅ Chat deleted successfully` }); - router.push(`/${SINGLE_TENANT_ORG_DOMAIN}/chat`); + router.push(`/chat`); return true; } }, [id, toast, router]); @@ -77,10 +74,10 @@ export const ChatName = ({ name, id, isOwner = false, isAuthenticated = false }: toast({ description: `✅ Chat duplicated successfully` }); - router.push(`/${params.domain}/chat/${response.id}`); + router.push(`/chat/${response.id}`); return response.id; } - }, [id, toast, router, params.domain]); + }, [id, toast, router]); return ( <> diff --git a/packages/web/src/app/[domain]/chat/components/chatSidePanel.tsx b/packages/web/src/app/(app)/chat/components/chatSidePanel.tsx similarity index 96% rename from packages/web/src/app/[domain]/chat/components/chatSidePanel.tsx rename to packages/web/src/app/(app)/chat/components/chatSidePanel.tsx index 7d3dde922..9815eb9de 100644 --- a/packages/web/src/app/[domain]/chat/components/chatSidePanel.tsx +++ b/packages/web/src/app/(app)/chat/components/chatSidePanel.tsx @@ -24,7 +24,6 @@ import { RenameChatDialog } from "./renameChatDialog"; import { DeleteChatDialog } from "./deleteChatDialog"; import { DuplicateChatDialog } from "./duplicateChatDialog"; import Link from "next/link"; -import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants"; interface ChatSidePanelProps { order: number; @@ -112,7 +111,7 @@ export const ChatSidePanel = ({ // If we just deleted the current chat, navigate to new chat if (chatIdToDelete === chatId) { - router.push(`/${SINGLE_TENANT_ORG_DOMAIN}/chat`); + router.push(`/chat`); } return true; @@ -136,7 +135,7 @@ export const ChatSidePanel = ({ description: `✅ Chat duplicated successfully` }); captureEvent('wa_chat_duplicated', { chatId: chatIdToDuplicate }); - router.push(`/${SINGLE_TENANT_ORG_DOMAIN}/chat/${response.id}`); + router.push(`/chat/${response.id}`); return response.id; } }, [router, toast]); @@ -161,7 +160,7 @@ export const ChatSidePanel = ({ size="sm" className="w-full" onClick={() => { - router.push(`/${SINGLE_TENANT_ORG_DOMAIN}/chat`); + router.push(`/chat`); }} > @@ -175,7 +174,7 @@ export const ChatSidePanel = ({

Sign in @@ -193,7 +192,7 @@ export const ChatSidePanel = ({ chat.id === chatId && "bg-muted" )} onClick={() => { - router.push(`/${SINGLE_TENANT_ORG_DOMAIN}/chat/${chat.id}`); + router.push(`/chat/${chat.id}`); }} > {chat.name ?? 'Untitled chat'} diff --git a/packages/web/src/app/[domain]/chat/components/deleteChatDialog.tsx b/packages/web/src/app/(app)/chat/components/deleteChatDialog.tsx similarity index 100% rename from packages/web/src/app/[domain]/chat/components/deleteChatDialog.tsx rename to packages/web/src/app/(app)/chat/components/deleteChatDialog.tsx diff --git a/packages/web/src/app/[domain]/chat/components/demoCards.tsx b/packages/web/src/app/(app)/chat/components/demoCards.tsx similarity index 100% rename from packages/web/src/app/[domain]/chat/components/demoCards.tsx rename to packages/web/src/app/(app)/chat/components/demoCards.tsx diff --git a/packages/web/src/app/[domain]/chat/components/duplicateChatDialog.tsx b/packages/web/src/app/(app)/chat/components/duplicateChatDialog.tsx similarity index 100% rename from packages/web/src/app/[domain]/chat/components/duplicateChatDialog.tsx rename to packages/web/src/app/(app)/chat/components/duplicateChatDialog.tsx diff --git a/packages/web/src/app/[domain]/chat/components/landingPageChatBox.tsx b/packages/web/src/app/(app)/chat/components/landingPageChatBox.tsx similarity index 100% rename from packages/web/src/app/[domain]/chat/components/landingPageChatBox.tsx rename to packages/web/src/app/(app)/chat/components/landingPageChatBox.tsx diff --git a/packages/web/src/app/[domain]/chat/components/renameChatDialog.tsx b/packages/web/src/app/(app)/chat/components/renameChatDialog.tsx similarity index 100% rename from packages/web/src/app/[domain]/chat/components/renameChatDialog.tsx rename to packages/web/src/app/(app)/chat/components/renameChatDialog.tsx diff --git a/packages/web/src/app/[domain]/chat/components/shareChatPopover/ee/invitePanel.tsx b/packages/web/src/app/(app)/chat/components/shareChatPopover/ee/invitePanel.tsx similarity index 100% rename from packages/web/src/app/[domain]/chat/components/shareChatPopover/ee/invitePanel.tsx rename to packages/web/src/app/(app)/chat/components/shareChatPopover/ee/invitePanel.tsx diff --git a/packages/web/src/app/[domain]/chat/components/shareChatPopover/index.tsx b/packages/web/src/app/(app)/chat/components/shareChatPopover/index.tsx similarity index 100% rename from packages/web/src/app/[domain]/chat/components/shareChatPopover/index.tsx rename to packages/web/src/app/(app)/chat/components/shareChatPopover/index.tsx diff --git a/packages/web/src/app/[domain]/chat/components/shareChatPopover/shareSettings.tsx b/packages/web/src/app/(app)/chat/components/shareChatPopover/shareSettings.tsx similarity index 100% rename from packages/web/src/app/[domain]/chat/components/shareChatPopover/shareSettings.tsx rename to packages/web/src/app/(app)/chat/components/shareChatPopover/shareSettings.tsx diff --git a/packages/web/src/app/[domain]/chat/components/tutorialDialog.tsx b/packages/web/src/app/(app)/chat/components/tutorialDialog.tsx similarity index 100% rename from packages/web/src/app/[domain]/chat/components/tutorialDialog.tsx rename to packages/web/src/app/(app)/chat/components/tutorialDialog.tsx diff --git a/packages/web/src/app/[domain]/chat/layout.tsx b/packages/web/src/app/(app)/chat/layout.tsx similarity index 100% rename from packages/web/src/app/[domain]/chat/layout.tsx rename to packages/web/src/app/(app)/chat/layout.tsx diff --git a/packages/web/src/app/[domain]/chat/page.tsx b/packages/web/src/app/(app)/chat/page.tsx similarity index 94% rename from packages/web/src/app/[domain]/chat/page.tsx rename to packages/web/src/app/(app)/chat/page.tsx index 61b33b33f..a072bc2b7 100644 --- a/packages/web/src/app/[domain]/chat/page.tsx +++ b/packages/web/src/app/(app)/chat/page.tsx @@ -18,14 +18,7 @@ import { ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable"; import { ChatSidePanel } from "./components/chatSidePanel"; import { AnimatedResizableHandle } from "@/components/ui/animatedResizableHandle"; -interface PageProps { - params: Promise<{ - domain: string; - }>; -} - -export default async function Page(props: PageProps) { - const params = await props.params; +export default async function Page() { const languageModels = await getConfiguredLanguageModelsInfo(); const searchContexts = await getSearchContexts(); const allRepos = await getRepos(); @@ -74,9 +67,7 @@ export default async function Page(props: PageProps) { return (

- + (null); const { toast } = useToast(); const captureEvent = useCaptureEvent(); - const domain = useDomain(); - useEffect(() => { if (selection.empty) { ref.current?.classList.add('hidden'); @@ -106,7 +103,7 @@ export const EditorContextMenu = ({ const from = toLineAndColumn(selection.from); const to = toLineAndColumn(selection.to); - const basePath = `${window.location.origin}/${domain}/browse`; + const basePath = `${window.location.origin}/browse`; const url = createPathWithQueryParams(`${basePath}/${repoName}@${revisionName}/-/blob/${path}`, [HIGHLIGHT_RANGE_QUERY_PARAM, `${from?.line}:${from?.column},${to?.line}:${to?.column}`], ); @@ -127,7 +124,7 @@ export const EditorContextMenu = ({ } } ) - }, [selection.from, selection.to, domain, repoName, revisionName, path, toast, captureEvent, view]); + }, [selection.from, selection.to, repoName, revisionName, path, toast, captureEvent, view]); return (
{ - const domain = useDomain(); - return ( @@ -59,7 +56,7 @@ export const MeControlDropdownMenu = ({ - + Settings diff --git a/packages/web/src/app/[domain]/components/mobileUnsupportedSplashScreen.tsx b/packages/web/src/app/(app)/components/mobileUnsupportedSplashScreen.tsx similarity index 100% rename from packages/web/src/app/[domain]/components/mobileUnsupportedSplashScreen.tsx rename to packages/web/src/app/(app)/components/mobileUnsupportedSplashScreen.tsx diff --git a/packages/web/src/app/[domain]/components/navigationMenu/index.tsx b/packages/web/src/app/(app)/components/navigationMenu/index.tsx similarity index 95% rename from packages/web/src/app/[domain]/components/navigationMenu/index.tsx rename to packages/web/src/app/(app)/components/navigationMenu/index.tsx index b47afb67f..dc4b4b1b9 100644 --- a/packages/web/src/app/[domain]/components/navigationMenu/index.tsx +++ b/packages/web/src/app/(app)/components/navigationMenu/index.tsx @@ -16,13 +16,7 @@ import { redirect } from "next/navigation"; import { AppearanceDropdownMenu } from "../appearanceDropdownMenu"; -interface NavigationMenuProps { - domain: string; -} - -export const NavigationMenu = async ({ - domain, -}: NavigationMenuProps) => { +export const NavigationMenu = async () => { const session = await auth(); const isAuthenticated = session?.user !== undefined; @@ -89,7 +83,7 @@ export const NavigationMenu = async ({
0} isSettingsButtonNotificationDotVisible={ diff --git a/packages/web/src/app/[domain]/components/navigationMenu/navigationItems.tsx b/packages/web/src/app/(app)/components/navigationMenu/navigationItems.tsx similarity index 83% rename from packages/web/src/app/[domain]/components/navigationMenu/navigationItems.tsx rename to packages/web/src/app/(app)/components/navigationMenu/navigationItems.tsx index b5a314837..78ddbd7e4 100644 --- a/packages/web/src/app/[domain]/components/navigationMenu/navigationItems.tsx +++ b/packages/web/src/app/(app)/components/navigationMenu/navigationItems.tsx @@ -8,7 +8,6 @@ import { usePathname } from "next/navigation"; import { NotificationDot } from "../notificationDot"; interface NavigationItemsProps { - domain: string; numberOfRepos: number; isReposButtonNotificationDotVisible: boolean; isSettingsButtonNotificationDotVisible: boolean; @@ -16,7 +15,6 @@ interface NavigationItemsProps { } export const NavigationItems = ({ - domain, numberOfRepos, isReposButtonNotificationDotVisible, isSettingsButtonNotificationDotVisible, @@ -25,8 +23,8 @@ export const NavigationItems = ({ const pathname = usePathname(); const isActive = (href: string) => { - if (href === `/${domain}`) { - return pathname === `/${domain}`; + if (href === '/') { + return pathname === '/'; } return pathname.startsWith(href); }; @@ -35,27 +33,27 @@ export const NavigationItems = ({ Search - {((isActive(`/${domain}`) || isActive(`/${domain}/search`)) && )} + {((isActive('/') || isActive('/search')) && )} Ask - {isActive(`/${domain}/chat`) && } + {isActive('/chat') && } @@ -65,19 +63,19 @@ export const NavigationItems = ({ {isReposButtonNotificationDotVisible && } - {isActive(`/${domain}/repos`) && } + {isActive('/repos') && } {isAuthenticated && ( Settings {isSettingsButtonNotificationDotVisible && } - {isActive(`/${domain}/settings`) && } + {isActive('/settings') && } )} diff --git a/packages/web/src/app/[domain]/components/navigationMenu/progressIndicator.tsx b/packages/web/src/app/(app)/components/navigationMenu/progressIndicator.tsx similarity index 91% rename from packages/web/src/app/[domain]/components/navigationMenu/progressIndicator.tsx rename to packages/web/src/app/(app)/components/navigationMenu/progressIndicator.tsx index 7726f635f..80589e353 100644 --- a/packages/web/src/app/[domain]/components/navigationMenu/progressIndicator.tsx +++ b/packages/web/src/app/(app)/components/navigationMenu/progressIndicator.tsx @@ -5,8 +5,6 @@ import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Separator } from "@/components/ui/separator"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; -import { useDomain } from "@/hooks/useDomain"; -import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants"; import { RepositoryQuery } from "@/lib/types"; import { getCodeHostInfoForRepo, getShortenedNumberDisplayString } from "@/lib/utils"; import clsx from "clsx"; @@ -25,7 +23,6 @@ export const ProgressIndicator = ({ numberOfReposWithFirstTimeIndexingJobsInProgress: numRepos, sampleRepos, }: ProgressIndicatorProps) => { - const domain = useDomain(); const router = useRouter(); const { toast } = useToast(); @@ -38,7 +35,7 @@ export const ProgressIndicator = ({ return ( - + {numReposString} @@ -70,7 +67,7 @@ export const ProgressIndicator = ({
{numRepos > sampleRepos.length && (
- + {`View ${numRepos - sampleRepos.length} more`}
@@ -104,7 +101,7 @@ const RepoItem = ({ repo }: { repo: RepositoryQuery }) => { return ( {repoIcon} diff --git a/packages/web/src/app/[domain]/components/notFound.tsx b/packages/web/src/app/(app)/components/notFound.tsx similarity index 100% rename from packages/web/src/app/[domain]/components/notFound.tsx rename to packages/web/src/app/(app)/components/notFound.tsx diff --git a/packages/web/src/app/[domain]/components/notificationDot.tsx b/packages/web/src/app/(app)/components/notificationDot.tsx similarity index 100% rename from packages/web/src/app/[domain]/components/notificationDot.tsx rename to packages/web/src/app/(app)/components/notificationDot.tsx diff --git a/packages/web/src/app/[domain]/components/onboardGuard.tsx b/packages/web/src/app/(app)/components/onboardGuard.tsx similarity index 100% rename from packages/web/src/app/[domain]/components/onboardGuard.tsx rename to packages/web/src/app/(app)/components/onboardGuard.tsx diff --git a/packages/web/src/app/[domain]/components/pageNotFound.tsx b/packages/web/src/app/(app)/components/pageNotFound.tsx similarity index 100% rename from packages/web/src/app/[domain]/components/pageNotFound.tsx rename to packages/web/src/app/(app)/components/pageNotFound.tsx diff --git a/packages/web/src/app/[domain]/components/pathHeader.tsx b/packages/web/src/app/(app)/components/pathHeader.tsx similarity index 100% rename from packages/web/src/app/[domain]/components/pathHeader.tsx rename to packages/web/src/app/(app)/components/pathHeader.tsx diff --git a/packages/web/src/app/[domain]/components/pendingApproval.tsx b/packages/web/src/app/(app)/components/pendingApproval.tsx similarity index 100% rename from packages/web/src/app/[domain]/components/pendingApproval.tsx rename to packages/web/src/app/(app)/components/pendingApproval.tsx diff --git a/packages/web/src/app/[domain]/components/permissionSyncBanner.tsx b/packages/web/src/app/(app)/components/permissionSyncBanner.tsx similarity index 100% rename from packages/web/src/app/[domain]/components/permissionSyncBanner.tsx rename to packages/web/src/app/(app)/components/permissionSyncBanner.tsx diff --git a/packages/web/src/app/[domain]/components/repositoryCarousel.tsx b/packages/web/src/app/(app)/components/repositoryCarousel.tsx similarity index 93% rename from packages/web/src/app/[domain]/components/repositoryCarousel.tsx rename to packages/web/src/app/(app)/components/repositoryCarousel.tsx index fe929c370..26d92b5b0 100644 --- a/packages/web/src/app/[domain]/components/repositoryCarousel.tsx +++ b/packages/web/src/app/(app)/components/repositoryCarousel.tsx @@ -11,7 +11,6 @@ import clsx from "clsx"; import Autoscroll from "embla-carousel-auto-scroll"; import Image from "next/image"; import Link from "next/link"; -import { useDomain } from "@/hooks/useDomain"; interface RepositoryCarouselProps { displayRepos: RepositoryQuery[]; @@ -22,8 +21,6 @@ export function RepositoryCarousel({ displayRepos, numberOfReposWithIndex, }: RepositoryCarouselProps) { - const domain = useDomain(); - if (numberOfReposWithIndex === 0) { return (
@@ -34,7 +31,7 @@ export function RepositoryCarousel({ <> Create a{" "} - + connection {" "} to start indexing repositories @@ -51,7 +48,7 @@ export function RepositoryCarousel({ {`${numberOfReposWithIndex} `} {numberOfReposWithIndex > 1 ? 'repositories' : 'repository'} diff --git a/packages/web/src/app/[domain]/components/searchBar/constants.ts b/packages/web/src/app/(app)/components/searchBar/constants.ts similarity index 100% rename from packages/web/src/app/[domain]/components/searchBar/constants.ts rename to packages/web/src/app/(app)/components/searchBar/constants.ts diff --git a/packages/web/src/app/[domain]/components/searchBar/index.ts b/packages/web/src/app/(app)/components/searchBar/index.ts similarity index 100% rename from packages/web/src/app/[domain]/components/searchBar/index.ts rename to packages/web/src/app/(app)/components/searchBar/index.ts diff --git a/packages/web/src/app/[domain]/components/searchBar/searchAssistBox.tsx b/packages/web/src/app/(app)/components/searchBar/searchAssistBox.tsx similarity index 100% rename from packages/web/src/app/[domain]/components/searchBar/searchAssistBox.tsx rename to packages/web/src/app/(app)/components/searchBar/searchAssistBox.tsx diff --git a/packages/web/src/app/[domain]/components/searchBar/searchBar.tsx b/packages/web/src/app/(app)/components/searchBar/searchBar.tsx similarity index 98% rename from packages/web/src/app/[domain]/components/searchBar/searchBar.tsx rename to packages/web/src/app/(app)/components/searchBar/searchBar.tsx index 423d77fd1..7ce3a537c 100644 --- a/packages/web/src/app/[domain]/components/searchBar/searchBar.tsx +++ b/packages/web/src/app/(app)/components/searchBar/searchBar.tsx @@ -41,7 +41,6 @@ import { useSuggestionModeAndQuery } from "./useSuggestionModeAndQuery"; import { Separator } from "@/components/ui/separator"; import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip"; import { Toggle } from "@/components/ui/toggle"; -import { useDomain } from "@/hooks/useDomain"; import tailwind from "@/tailwind"; import React from "react"; import Link from "next/link"; @@ -110,7 +109,6 @@ export const SearchBar = ({ isSearchAssistSupported, }: SearchBarProps) => { const router = useRouter(); - const domain = useDomain(); const captureEvent = useCaptureEvent(); const suggestionBoxRef = useRef(null); const editorRef = useRef(null); @@ -227,13 +225,13 @@ export const SearchBar = ({ setActivePanel(undefined); setIsHistorySearchEnabled(false); - const url = createPathWithQueryParams(`/${domain}/search`, + const url = createPathWithQueryParams(`/search`, [SearchQueryParams.query, query], [SearchQueryParams.isRegexEnabled, isRegexEnabled ? "true" : null], [SearchQueryParams.isCaseSensitivityEnabled, isCaseSensitivityEnabled ? "true" : null], ); router.push(url); - }, [domain, router, isRegexEnabled, isCaseSensitivityEnabled]); + }, [router, isRegexEnabled, isCaseSensitivityEnabled]); return (
{ - const domain = useDomain(); const { data: repoSuggestions, isLoading: _isLoadingRepos } = useQuery({ queryKey: ["repoSuggestions", suggestionQuery], queryFn: () => unwrapServiceError(listRepos({ @@ -56,7 +54,7 @@ export const useSuggestionsData = ({ const isLoadingRepos = useMemo(() => suggestionMode === "repo" && _isLoadingRepos, [_isLoadingRepos, suggestionMode]); const { data: fileSuggestions, isLoading: _isLoadingFiles } = useQuery({ - queryKey: ["fileSuggestions", suggestionQuery, domain], + queryKey: ["fileSuggestions", suggestionQuery], queryFn: () => search({ query: `file:${suggestionQuery}`, matches: 15, @@ -76,7 +74,7 @@ export const useSuggestionsData = ({ const isLoadingFiles = useMemo(() => suggestionMode === "file" && _isLoadingFiles, [_isLoadingFiles, suggestionMode]); const { data: symbolSuggestions, isLoading: _isLoadingSymbols } = useQuery({ - queryKey: ["symbolSuggestions", suggestionQuery, domain], + queryKey: ["symbolSuggestions", suggestionQuery], queryFn: () => search({ query: `sym:${suggestionQuery.length > 0 ? suggestionQuery : ".*"}`, matches: 15, @@ -106,7 +104,7 @@ export const useSuggestionsData = ({ const isLoadingSymbols = useMemo(() => suggestionMode === "symbol" && _isLoadingSymbols, [suggestionMode, _isLoadingSymbols]); const { data: searchContextSuggestions, isLoading: _isLoadingSearchContexts } = useQuery({ - queryKey: ["searchContexts", domain], + queryKey: ["searchContexts"], queryFn: () => getSearchContexts(), select: (data): Suggestion[] => { if (isServiceError(data)) { diff --git a/packages/web/src/app/[domain]/components/searchBar/zoektLanguageExtension.ts b/packages/web/src/app/(app)/components/searchBar/zoektLanguageExtension.ts similarity index 100% rename from packages/web/src/app/[domain]/components/searchBar/zoektLanguageExtension.ts rename to packages/web/src/app/(app)/components/searchBar/zoektLanguageExtension.ts diff --git a/packages/web/src/app/[domain]/components/searchModeSelector.tsx b/packages/web/src/app/(app)/components/searchModeSelector.tsx similarity index 97% rename from packages/web/src/app/[domain]/components/searchModeSelector.tsx rename to packages/web/src/app/(app)/components/searchModeSelector.tsx index aaa8f4412..4b2ef3e95 100644 --- a/packages/web/src/app/[domain]/components/searchModeSelector.tsx +++ b/packages/web/src/app/(app)/components/searchModeSelector.tsx @@ -1,7 +1,6 @@ 'use client'; import { KeyboardShortcutHint } from "@/app/components/keyboardShortcutHint"; -import { useDomain } from "@/hooks/useDomain"; import { Select, SelectContent, SelectItemNoItemText, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Separator } from "@/components/ui/separator"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; @@ -27,13 +26,12 @@ export const SearchModeSelector = ({ searchMode, className, }: SearchModeSelectorProps) => { - const domain = useDomain(); const [focusedSearchMode, setFocusedSearchMode] = useState(searchMode); const router = useRouter(); const onSearchModeChanged = useCallback((value: SearchMode) => { - router.push(`/${domain}/${value === "precise" ? "search" : "chat"}`); - }, [domain, router]); + router.push(`/${value === "precise" ? "search" : "chat"}`); + }, [router]); useHotkeys("mod+i", (e) => { e.preventDefault(); diff --git a/packages/web/src/app/[domain]/components/submitAccountRequestButton.tsx b/packages/web/src/app/(app)/components/submitAccountRequestButton.tsx similarity index 89% rename from packages/web/src/app/[domain]/components/submitAccountRequestButton.tsx rename to packages/web/src/app/(app)/components/submitAccountRequestButton.tsx index 291e5f509..56a9a6b68 100644 --- a/packages/web/src/app/[domain]/components/submitAccountRequestButton.tsx +++ b/packages/web/src/app/(app)/components/submitAccountRequestButton.tsx @@ -9,18 +9,17 @@ import { isServiceError } from "@/lib/utils" import { useRouter } from "next/navigation" interface SubmitButtonProps { - domain: string userId: string } -export function SubmitAccountRequestButton({ domain, userId }: SubmitButtonProps) { +export function SubmitAccountRequestButton({ userId }: SubmitButtonProps) { const { toast } = useToast() const router = useRouter() const [isSubmitting, setIsSubmitting] = useState(false) const handleSubmit = async () => { setIsSubmitting(true) - const result = await createAccountRequest(userId, domain) + const result = await createAccountRequest(userId) if (!isServiceError(result)) { if (result.existingRequest) { toast({ @@ -52,7 +51,6 @@ export function SubmitAccountRequestButton({ domain, userId }: SubmitButtonProps e.preventDefault(); handleSubmit(); }}> -
diff --git a/packages/web/src/app/[domain]/components/syntaxGuideProvider.tsx b/packages/web/src/app/(app)/components/syntaxGuideProvider.tsx similarity index 100% rename from packages/web/src/app/[domain]/components/syntaxGuideProvider.tsx rename to packages/web/src/app/(app)/components/syntaxGuideProvider.tsx diff --git a/packages/web/src/app/[domain]/components/syntaxReferenceGuide.tsx b/packages/web/src/app/(app)/components/syntaxReferenceGuide.tsx similarity index 100% rename from packages/web/src/app/[domain]/components/syntaxReferenceGuide.tsx rename to packages/web/src/app/(app)/components/syntaxReferenceGuide.tsx diff --git a/packages/web/src/app/[domain]/components/syntaxReferenceGuideHint.tsx b/packages/web/src/app/(app)/components/syntaxReferenceGuideHint.tsx similarity index 100% rename from packages/web/src/app/[domain]/components/syntaxReferenceGuideHint.tsx rename to packages/web/src/app/(app)/components/syntaxReferenceGuideHint.tsx diff --git a/packages/web/src/app/[domain]/components/topBar.tsx b/packages/web/src/app/(app)/components/topBar.tsx similarity index 98% rename from packages/web/src/app/[domain]/components/topBar.tsx rename to packages/web/src/app/(app)/components/topBar.tsx index 6c3dc2b62..ed5996c6c 100644 --- a/packages/web/src/app/[domain]/components/topBar.tsx +++ b/packages/web/src/app/(app)/components/topBar.tsx @@ -15,7 +15,6 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip import { cn } from "@/lib/utils"; interface TopBarProps { - domain: string; children?: React.ReactNode; centerContent?: React.ReactNode; actions?: React.ReactNode; @@ -24,11 +23,10 @@ interface TopBarProps { } export const TopBar = ({ - domain, children, centerContent, actions, - homePath = `/${domain}`, + homePath = `/`, session, }: TopBarProps) => { const router = useRouter(); diff --git a/packages/web/src/app/[domain]/components/upgradeToast.tsx b/packages/web/src/app/(app)/components/upgradeToast.tsx similarity index 100% rename from packages/web/src/app/[domain]/components/upgradeToast.tsx rename to packages/web/src/app/(app)/components/upgradeToast.tsx diff --git a/packages/web/src/app/[domain]/components/whatsNewIndicator.tsx b/packages/web/src/app/(app)/components/whatsNewIndicator.tsx similarity index 100% rename from packages/web/src/app/[domain]/components/whatsNewIndicator.tsx rename to packages/web/src/app/(app)/components/whatsNewIndicator.tsx diff --git a/packages/web/src/app/[domain]/layout.tsx b/packages/web/src/app/(app)/layout.tsx similarity index 93% rename from packages/web/src/app/[domain]/layout.tsx rename to packages/web/src/app/(app)/layout.tsx index 3a4371cae..16587524f 100644 --- a/packages/web/src/app/[domain]/layout.tsx +++ b/packages/web/src/app/(app)/layout.tsx @@ -1,7 +1,7 @@ import { prisma } from "@/prisma"; import { auth } from "@/auth"; -import { getOrgFromDomain } from "@/data/org"; import { isServiceError } from "@/lib/utils"; +import { SINGLE_TENANT_ORG_ID } from "@/lib/constants"; import { OnboardGuard } from "./components/onboardGuard"; import { cookies, headers } from "next/headers"; import { getSelectorsByUserAgent } from "react-device-detect"; @@ -28,21 +28,16 @@ import { ConnectAccountsCard } from "@/ee/features/sso/components/connectAccount interface LayoutProps { children: React.ReactNode, - params: Promise<{ domain: string }> } export default async function Layout(props: LayoutProps) { - const params = await props.params; - - const { - domain - } = params; - const { children } = props; - const org = await getOrgFromDomain(domain); + const org = await prisma.org.findUnique({ + where: { id: SINGLE_TENANT_ORG_ID }, + }); if (!org) { return notFound(); @@ -54,7 +49,7 @@ export default async function Layout(props: LayoutProps) { return false; } - const status = await getAnonymousAccessStatus(domain); + const status = await getAnonymousAccessStatus(); if (isServiceError(status)) { return false; } @@ -81,7 +76,7 @@ export default async function Layout(props: LayoutProps) { // the join organization card to allow them to join the org if seat capacity is freed up. This card handles checking if the org has available seats. // 2. The org requires member approval, and they haven't been approved yet. In this case, we allow them to submit a request to join the org. if (!membership) { - const memberApprovalRequired = await getMemberApprovalRequired(domain); + const memberApprovalRequired = await getMemberApprovalRequired(); if (!memberApprovalRequired) { return (
@@ -100,7 +95,7 @@ export default async function Layout(props: LayoutProps) { if (hasPendingApproval) { return } else { - return + return } } } @@ -109,7 +104,7 @@ export default async function Layout(props: LayoutProps) { if (!anonymousAccessEnabled) { const ssoEntitlement = await hasEntitlement("sso"); if (ssoEntitlement && env.AUTH_EE_GCP_IAP_ENABLED && env.AUTH_EE_GCP_IAP_AUDIENCE) { - return ; + return ; } else { redirect('/login'); } @@ -142,7 +137,7 @@ export default async function Layout(props: LayoutProps) { return (
- +
) } diff --git a/packages/web/src/app/[domain]/page.tsx b/packages/web/src/app/(app)/page.tsx similarity index 85% rename from packages/web/src/app/[domain]/page.tsx rename to packages/web/src/app/(app)/page.tsx index 1ff9b8d6b..142220f55 100644 --- a/packages/web/src/app/[domain]/page.tsx +++ b/packages/web/src/app/(app)/page.tsx @@ -1,7 +1,6 @@ import SearchPage from "./search/page"; interface Props { - params: Promise<{ domain: string }>; searchParams: Promise<{ query?: string }>; } diff --git a/packages/web/src/app/[domain]/repos/[id]/page.tsx b/packages/web/src/app/(app)/repos/[id]/page.tsx similarity index 98% rename from packages/web/src/app/[domain]/repos/[id]/page.tsx rename to packages/web/src/app/(app)/repos/[id]/page.tsx index 193c9a1c9..a5869733f 100644 --- a/packages/web/src/app/[domain]/repos/[id]/page.tsx +++ b/packages/web/src/app/(app)/repos/[id]/page.tsx @@ -6,7 +6,6 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com import { Skeleton } from "@/components/ui/skeleton" import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip" import { env } from "@sourcebot/shared" -import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants" import { ServiceErrorException } from "@/lib/serviceError" import { cn, getCodeHostInfoForRepo, isServiceError } from "@/lib/utils" import { withOptionalAuth } from "@/middleware/withAuth" @@ -62,7 +61,7 @@ export default async function RepoDetailPage({ params }: { params: Promise<{ id: <>
diff --git a/packages/web/src/app/[domain]/repos/components/repoActionsDropdown.tsx b/packages/web/src/app/(app)/repos/components/repoActionsDropdown.tsx similarity index 94% rename from packages/web/src/app/[domain]/repos/components/repoActionsDropdown.tsx rename to packages/web/src/app/(app)/repos/components/repoActionsDropdown.tsx index 636de0c20..4a0049892 100644 --- a/packages/web/src/app/[domain]/repos/components/repoActionsDropdown.tsx +++ b/packages/web/src/app/(app)/repos/components/repoActionsDropdown.tsx @@ -2,7 +2,6 @@ import { Button } from "@/components/ui/button" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" -import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants" import { getCodeHostInfoForRepo, isServiceError } from "@/lib/utils" import { ExternalLink, MoreHorizontal } from "lucide-react" import Link from "next/link" @@ -58,7 +57,7 @@ export const RepoActionsDropdown = ({ repo }: RepoActionsDropdownProps) => { Actions - View details + View details [] => [ {/* Link to the details page (instead of browse) when the repo is indexing as the code will not be available yet */} ; } export default async function Layout( props: LayoutProps ) { - const params = await props.params; - const { domain } = params; const { children } = props; const repoStats = await getReposStats(); @@ -28,11 +24,11 @@ export default async function Layout( return (
- + {(repoStats.numberOfRepos === 0 && userRoleInOrg === OrgRole.OWNER) && (
- No repositories configured. Create a connection to get started. + No repositories configured. Create a connection to get started.
)}
diff --git a/packages/web/src/app/[domain]/repos/page.tsx b/packages/web/src/app/(app)/repos/page.tsx similarity index 100% rename from packages/web/src/app/[domain]/repos/page.tsx rename to packages/web/src/app/(app)/repos/page.tsx diff --git a/packages/web/src/app/[domain]/search/components/codePreviewPanel/codePreview.tsx b/packages/web/src/app/(app)/search/components/codePreviewPanel/codePreview.tsx similarity index 98% rename from packages/web/src/app/[domain]/search/components/codePreviewPanel/codePreview.tsx rename to packages/web/src/app/(app)/search/components/codePreviewPanel/codePreview.tsx index 6a2a69596..4d9ef76ba 100644 --- a/packages/web/src/app/[domain]/search/components/codePreviewPanel/codePreview.tsx +++ b/packages/web/src/app/(app)/search/components/codePreviewPanel/codePreview.tsx @@ -1,7 +1,7 @@ 'use client'; -import { useBrowseNavigation } from "@/app/[domain]/browse/hooks/useBrowseNavigation"; -import { EditorContextMenu } from "@/app/[domain]/components/editorContextMenu"; +import { useBrowseNavigation } from "@/app/(app)/browse/hooks/useBrowseNavigation"; +import { EditorContextMenu } from "@/app/(app)/components/editorContextMenu"; import { Button } from "@/components/ui/button"; import { ScrollArea } from "@/components/ui/scroll-area"; import { SymbolHoverPopup } from "@/ee/features/codeNav/components/symbolHoverPopup"; diff --git a/packages/web/src/app/[domain]/search/components/codePreviewPanel/index.tsx b/packages/web/src/app/(app)/search/components/codePreviewPanel/index.tsx similarity index 100% rename from packages/web/src/app/[domain]/search/components/codePreviewPanel/index.tsx rename to packages/web/src/app/(app)/search/components/codePreviewPanel/index.tsx diff --git a/packages/web/src/app/[domain]/search/components/filterPanel/entry.tsx b/packages/web/src/app/(app)/search/components/filterPanel/entry.tsx similarity index 100% rename from packages/web/src/app/[domain]/search/components/filterPanel/entry.tsx rename to packages/web/src/app/(app)/search/components/filterPanel/entry.tsx diff --git a/packages/web/src/app/[domain]/search/components/filterPanel/filter.tsx b/packages/web/src/app/(app)/search/components/filterPanel/filter.tsx similarity index 100% rename from packages/web/src/app/[domain]/search/components/filterPanel/filter.tsx rename to packages/web/src/app/(app)/search/components/filterPanel/filter.tsx diff --git a/packages/web/src/app/[domain]/search/components/filterPanel/index.tsx b/packages/web/src/app/(app)/search/components/filterPanel/index.tsx similarity index 100% rename from packages/web/src/app/[domain]/search/components/filterPanel/index.tsx rename to packages/web/src/app/(app)/search/components/filterPanel/index.tsx diff --git a/packages/web/src/app/[domain]/search/components/filterPanel/useFilterMatches.ts b/packages/web/src/app/(app)/search/components/filterPanel/useFilterMatches.ts similarity index 100% rename from packages/web/src/app/[domain]/search/components/filterPanel/useFilterMatches.ts rename to packages/web/src/app/(app)/search/components/filterPanel/useFilterMatches.ts diff --git a/packages/web/src/app/[domain]/search/components/filterPanel/useGetSelectedFromQuery.ts b/packages/web/src/app/(app)/search/components/filterPanel/useGetSelectedFromQuery.ts similarity index 100% rename from packages/web/src/app/[domain]/search/components/filterPanel/useGetSelectedFromQuery.ts rename to packages/web/src/app/(app)/search/components/filterPanel/useGetSelectedFromQuery.ts diff --git a/packages/web/src/app/[domain]/search/components/searchLandingPage.tsx b/packages/web/src/app/(app)/search/components/searchLandingPage.tsx similarity index 70% rename from packages/web/src/app/[domain]/search/components/searchLandingPage.tsx rename to packages/web/src/app/(app)/search/components/searchLandingPage.tsx index a0f33757d..668c84e57 100644 --- a/packages/web/src/app/[domain]/search/components/searchLandingPage.tsx +++ b/packages/web/src/app/(app)/search/components/searchLandingPage.tsx @@ -11,12 +11,10 @@ import { ServiceErrorException } from "@/lib/serviceError" import { isServiceError } from "@/lib/utils" export interface SearchLandingPageProps { - domain: string; isSearchAssistSupported: boolean; } export const SearchLandingPage = async ({ - domain, isSearchAssistSupported, }: SearchLandingPageProps) => { const carouselRepos = await getRepos({ @@ -35,9 +33,7 @@ export const SearchLandingPage = async ({ return (
- +
@@ -75,48 +71,48 @@ export const SearchLandingPage = async ({ title="Search in files or paths" > - test todo (both test and todo) + test todo (both test and todo) - test or todo (either test or todo) + test or todo (either test or todo) - {`"exit boot"`} (exact match) + {`"exit boot"`} (exact match) - TODO (case sensitive) + TODO (case sensitive) - file:README setup (by filename) + file:README setup (by filename) - repo:torvalds/linux test (by repo) + repo:torvalds/linux test (by repo) - lang:TypeScript (by language) + lang:TypeScript (by language) - rev:HEAD (by branch or tag) + rev:HEAD (by branch or tag) - file:{`\\.py$`} {`(files that end in ".py")`} + file:{`\\.py$`} {`(files that end in ".py")`} - sym:main {`(symbols named "main")`} + sym:main {`(symbols named "main")`} - todo -lang:c (negate filter) + todo -lang:c (negate filter) - content:README (search content only) + content:README (search content only)
@@ -161,10 +157,10 @@ const QueryExplanation = ({ children }: { children: React.ReactNode }) => { ) } -const Query = ({ query, domain, children, isCaseSensitivityEnabled = false }: { query: string, domain: string, children: React.ReactNode, isCaseSensitivityEnabled?: boolean }) => { +const Query = ({ query, children, isCaseSensitivityEnabled = false }: { query: string, children: React.ReactNode, isCaseSensitivityEnabled?: boolean }) => { return ( {children} diff --git a/packages/web/src/app/[domain]/search/components/searchResultsPage.tsx b/packages/web/src/app/(app)/search/components/searchResultsPage.tsx similarity index 98% rename from packages/web/src/app/[domain]/search/components/searchResultsPage.tsx rename to packages/web/src/app/(app)/search/components/searchResultsPage.tsx index a8a7c9139..fb9f68bc1 100644 --- a/packages/web/src/app/[domain]/search/components/searchResultsPage.tsx +++ b/packages/web/src/app/(app)/search/components/searchResultsPage.tsx @@ -13,7 +13,6 @@ import { Separator } from "@/components/ui/separator"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { RepositoryInfo, SearchResultFile, SearchStats } from "@/features/search"; import useCaptureEvent from "@/hooks/useCaptureEvent"; -import { useDomain } from "@/hooks/useDomain"; import { useNonEmptyQueryParam } from "@/hooks/useNonEmptyQueryParam"; import { useSearchHistory } from "@/hooks/useSearchHistory"; import { SearchQueryParams } from "@/lib/types"; @@ -55,7 +54,6 @@ export const SearchResultsPage = ({ }: SearchResultsPageProps) => { const router = useRouter(); const { setSearchHistory } = useSearchHistory(); - const domain = useDomain(); const { toast } = useToast(); const captureEvent = useCaptureEvent(); @@ -162,21 +160,20 @@ export const SearchResultsPage = ({ ]); const onLoadMoreResults = useCallback(() => { - const url = createPathWithQueryParams(`/${domain}/search`, + const url = createPathWithQueryParams(`/search`, [SearchQueryParams.query, searchQuery], [SearchQueryParams.matches, `${maxMatchCount * 2}`], [SearchQueryParams.isRegexEnabled, isRegexEnabled ? "true" : null], [SearchQueryParams.isCaseSensitivityEnabled, isCaseSensitivityEnabled ? "true" : null], ) router.push(url); - }, [maxMatchCount, router, searchQuery, domain, isRegexEnabled, isCaseSensitivityEnabled]); + }, [maxMatchCount, router, searchQuery, isRegexEnabled, isCaseSensitivityEnabled]); return (
{/* TopBar */} ; searchParams: Promise<{ query?: string; isRegexEnabled?: "true" | "false"; @@ -14,7 +13,6 @@ interface SearchPageProps { } export default async function SearchPage(props: SearchPageProps) { - const { domain } = await props.params; const searchParams = await props.searchParams; const query = searchParams?.query; const isRegexEnabled = searchParams?.isRegexEnabled === "true"; @@ -25,7 +23,7 @@ export default async function SearchPage(props: SearchPageProps) { const isSearchAssistSupported = languageModels.length > 0; if (query === undefined || query.length === 0) { - return + return } return ( diff --git a/packages/web/src/app/[domain]/search/useStreamedSearch.ts b/packages/web/src/app/(app)/search/useStreamedSearch.ts similarity index 100% rename from packages/web/src/app/[domain]/search/useStreamedSearch.ts rename to packages/web/src/app/(app)/search/useStreamedSearch.ts diff --git a/packages/web/src/app/[domain]/settings/access/page.tsx b/packages/web/src/app/(app)/settings/access/page.tsx similarity index 80% rename from packages/web/src/app/[domain]/settings/access/page.tsx rename to packages/web/src/app/(app)/settings/access/page.tsx index 2fd6163dc..b88a01560 100644 --- a/packages/web/src/app/[domain]/settings/access/page.tsx +++ b/packages/web/src/app/(app)/settings/access/page.tsx @@ -1,4 +1,5 @@ -import { getOrgFromDomain } from "@/data/org"; +import { SINGLE_TENANT_ORG_ID } from "@/lib/constants"; +import { prisma } from "@/prisma"; import { OrganizationAccessSettings } from "@/app/components/organizationAccessSettings"; import { isServiceError } from "@/lib/utils"; import { ServiceErrorException } from "@/lib/serviceError"; @@ -6,20 +7,8 @@ import { getMe } from "@/actions"; import { OrgRole } from "@sourcebot/db"; import { redirect } from "next/navigation"; -interface AccessPageProps { - params: Promise<{ - domain: string; - }> -} - -export default async function AccessPage(props: AccessPageProps) { - const params = await props.params; - - const { - domain - } = params; - - const org = await getOrgFromDomain(domain); +export default async function AccessPage() { + const org = await prisma.org.findUnique({ where: { id: SINGLE_TENANT_ORG_ID } }); if (!org) { throw new Error("Organization not found"); } @@ -35,7 +24,7 @@ export default async function AccessPage(props: AccessPageProps) { } if (userRoleInOrg !== OrgRole.OWNER) { - redirect(`/${domain}/settings`); + redirect('/settings'); } return ( diff --git a/packages/web/src/app/[domain]/settings/analytics/page.tsx b/packages/web/src/app/(app)/settings/analytics/page.tsx similarity index 75% rename from packages/web/src/app/[domain]/settings/analytics/page.tsx rename to packages/web/src/app/(app)/settings/analytics/page.tsx index b5a680611..9883d9718 100644 --- a/packages/web/src/app/[domain]/settings/analytics/page.tsx +++ b/packages/web/src/app/(app)/settings/analytics/page.tsx @@ -1,5 +1,6 @@ import { getMe } from "@/actions"; -import { getOrgFromDomain } from "@/data/org"; +import { SINGLE_TENANT_ORG_ID } from "@/lib/constants"; +import { prisma } from "@/prisma"; import { AnalyticsContent } from "@/ee/features/analytics/analyticsContent"; import { AnalyticsEntitlementMessage } from "@/ee/features/analytics/analyticsEntitlementMessage"; import { ServiceErrorException } from "@/lib/serviceError"; @@ -8,20 +9,8 @@ import { OrgRole } from "@sourcebot/db"; import { hasEntitlement } from "@sourcebot/shared"; import { redirect } from "next/navigation"; -interface Props { - params: Promise<{ - domain: string; - }> -} - -export default async function AnalyticsPage(props: Props) { - const params = await props.params; - - const { - domain - } = params; - - const org = await getOrgFromDomain(domain); +export default async function AnalyticsPage() { + const org = await prisma.org.findUnique({ where: { id: SINGLE_TENANT_ORG_ID } }); if (!org) { throw new Error("Organization not found"); } @@ -37,7 +26,7 @@ export default async function AnalyticsPage(props: Props) { } if (userRoleInOrg !== OrgRole.OWNER) { - redirect(`/${domain}/settings`); + redirect('/settings'); } const hasAnalyticsEntitlement = hasEntitlement("analytics"); diff --git a/packages/web/src/app/[domain]/settings/apiKeys/apiKeysPage.tsx b/packages/web/src/app/(app)/settings/apiKeys/apiKeysPage.tsx similarity index 100% rename from packages/web/src/app/[domain]/settings/apiKeys/apiKeysPage.tsx rename to packages/web/src/app/(app)/settings/apiKeys/apiKeysPage.tsx diff --git a/packages/web/src/app/[domain]/settings/apiKeys/columns.tsx b/packages/web/src/app/(app)/settings/apiKeys/columns.tsx similarity index 100% rename from packages/web/src/app/[domain]/settings/apiKeys/columns.tsx rename to packages/web/src/app/(app)/settings/apiKeys/columns.tsx diff --git a/packages/web/src/app/(app)/settings/apiKeys/layout.tsx b/packages/web/src/app/(app)/settings/apiKeys/layout.tsx new file mode 100644 index 000000000..e6da0a6bf --- /dev/null +++ b/packages/web/src/app/(app)/settings/apiKeys/layout.tsx @@ -0,0 +1,31 @@ +import { getMe } from "@/actions"; +import { ServiceErrorException } from "@/lib/serviceError"; +import { notFound } from "next/navigation"; +import { isServiceError } from "@/lib/utils"; +import { OrgRole } from "@sourcebot/db"; +import { SINGLE_TENANT_ORG_ID } from "@/lib/constants"; +import { prisma } from "@/prisma"; +import { env } from "@sourcebot/shared"; + +export default async function ApiKeysLayout({ children }: { children: React.ReactNode }) { + const org = await prisma.org.findUnique({ where: { id: SINGLE_TENANT_ORG_ID } }); + if (!org) { + throw new Error("Organization not found"); + } + + const me = await getMe(); + if (isServiceError(me)) { + throw new ServiceErrorException(me); + } + + const userRoleInOrg = me.memberships.find((membership) => membership.id === org.id)?.role; + if (!userRoleInOrg) { + throw new Error("User role not found"); + } + + if (env.DISABLE_API_KEY_USAGE_FOR_NON_OWNER_USERS === 'true' && userRoleInOrg !== OrgRole.OWNER) { + return notFound(); + } + + return children; +} \ No newline at end of file diff --git a/packages/web/src/app/[domain]/settings/apiKeys/page.tsx b/packages/web/src/app/(app)/settings/apiKeys/page.tsx similarity index 68% rename from packages/web/src/app/[domain]/settings/apiKeys/page.tsx rename to packages/web/src/app/(app)/settings/apiKeys/page.tsx index 37f6f8924..9bba20912 100644 --- a/packages/web/src/app/[domain]/settings/apiKeys/page.tsx +++ b/packages/web/src/app/(app)/settings/apiKeys/page.tsx @@ -2,15 +2,14 @@ import { getMe } from "@/actions"; import { isServiceError } from "@/lib/utils"; import { env } from "@sourcebot/shared"; import { OrgRole } from "@sourcebot/db"; -import { getOrgFromDomain } from "@/data/org"; +import { SINGLE_TENANT_ORG_ID } from "@/lib/constants"; +import { prisma } from "@/prisma"; import { ApiKeysPage } from "./apiKeysPage"; -export default async function Page({ params }: { params: Promise<{ domain: string }> }) { - const { domain } = await params; - +export default async function Page() { let canCreateApiKey = true; if (env.DISABLE_API_KEY_CREATION_FOR_NON_OWNER_USERS === 'true') { - const [org, me] = await Promise.all([getOrgFromDomain(domain), getMe()]); + const [org, me] = await Promise.all([prisma.org.findUnique({ where: { id: SINGLE_TENANT_ORG_ID } }), getMe()]); if (org && !isServiceError(me)) { const role = me.memberships.find((m) => m.id === org.id)?.role; canCreateApiKey = role === OrgRole.OWNER; diff --git a/packages/web/src/app/[domain]/settings/components/sidebar-nav.tsx b/packages/web/src/app/(app)/settings/components/sidebar-nav.tsx similarity index 95% rename from packages/web/src/app/[domain]/settings/components/sidebar-nav.tsx rename to packages/web/src/app/(app)/settings/components/sidebar-nav.tsx index 78b5a7ccc..23b17b3c3 100644 --- a/packages/web/src/app/[domain]/settings/components/sidebar-nav.tsx +++ b/packages/web/src/app/(app)/settings/components/sidebar-nav.tsx @@ -1,7 +1,7 @@ "use client" import { buttonVariants } from "@/components/ui/button" -import { NotificationDot } from "@/app/[domain]/components/notificationDot" +import { NotificationDot } from "@/app/(app)/components/notificationDot" import { cn } from "@/lib/utils" import Link from "next/link" import { usePathname } from "next/navigation" diff --git a/packages/web/src/app/[domain]/settings/connections/[id]/page.tsx b/packages/web/src/app/(app)/settings/connections/[id]/page.tsx similarity index 96% rename from packages/web/src/app/[domain]/settings/connections/[id]/page.tsx rename to packages/web/src/app/(app)/settings/connections/[id]/page.tsx index 3229112b2..edcc61069 100644 --- a/packages/web/src/app/[domain]/settings/connections/[id]/page.tsx +++ b/packages/web/src/app/(app)/settings/connections/[id]/page.tsx @@ -1,10 +1,9 @@ import { sew } from "@/middleware/sew"; -import { BackButton } from "@/app/[domain]/components/backButton"; -import { DisplayDate } from "@/app/[domain]/components/DisplayDate"; +import { BackButton } from "@/app/(app)/components/backButton"; +import { DisplayDate } from "@/app/(app)/components/DisplayDate"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Skeleton } from "@/components/ui/skeleton"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; -import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants"; import { notFound as notFoundServiceError, ServiceErrorException } from "@/lib/serviceError"; import { notFound } from "next/navigation"; import { isServiceError } from "@/lib/utils"; @@ -93,7 +92,7 @@ export default async function ConnectionDetailPage(props: ConnectionDetailPagePr return (
diff --git a/packages/web/src/app/[domain]/settings/connections/components/connectionJobsTable.tsx b/packages/web/src/app/(app)/settings/connections/components/connectionJobsTable.tsx similarity index 98% rename from packages/web/src/app/[domain]/settings/connections/components/connectionJobsTable.tsx rename to packages/web/src/app/(app)/settings/connections/components/connectionJobsTable.tsx index fd5df81e2..9277d81b7 100644 --- a/packages/web/src/app/[domain]/settings/connections/components/connectionJobsTable.tsx +++ b/packages/web/src/app/(app)/settings/connections/components/connectionJobsTable.tsx @@ -20,12 +20,12 @@ import { import { cva } from "class-variance-authority" import { AlertCircle, AlertTriangle, ArrowUpDown, PlusCircleIcon, RefreshCwIcon } from "lucide-react" import * as React from "react" -import { CopyIconButton } from "@/app/[domain]/components/copyIconButton" +import { CopyIconButton } from "@/app/(app)/components/copyIconButton" import { useMemo } from "react" -import { LightweightCodeHighlighter } from "@/app/[domain]/components/lightweightCodeHighlighter" +import { LightweightCodeHighlighter } from "@/app/(app)/components/lightweightCodeHighlighter" import { useRouter } from "next/navigation" import { useToast } from "@/components/hooks/use-toast" -import { DisplayDate } from "@/app/[domain]/components/DisplayDate" +import { DisplayDate } from "@/app/(app)/components/DisplayDate" import { LoadingButton } from "@/components/ui/loading-button" import { syncConnection } from "@/features/workerApi/actions" import { isServiceError } from "@/lib/utils" diff --git a/packages/web/src/app/[domain]/settings/connections/components/connectionsTable.tsx b/packages/web/src/app/(app)/settings/connections/components/connectionsTable.tsx similarity index 97% rename from packages/web/src/app/[domain]/settings/connections/components/connectionsTable.tsx rename to packages/web/src/app/(app)/settings/connections/components/connectionsTable.tsx index 3285c71fb..e52e7ef1e 100644 --- a/packages/web/src/app/[domain]/settings/connections/components/connectionsTable.tsx +++ b/packages/web/src/app/(app)/settings/connections/components/connectionsTable.tsx @@ -1,7 +1,7 @@ "use client" -import { DisplayDate } from "@/app/[domain]/components/DisplayDate" -import { NotificationDot } from "@/app/[domain]/components/notificationDot" +import { DisplayDate } from "@/app/(app)/components/DisplayDate" +import { NotificationDot } from "@/app/(app)/components/notificationDot" import { useToast } from "@/components/hooks/use-toast" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" @@ -9,7 +9,6 @@ import { Input } from "@/components/ui/input" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip" -import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants" import { getCodeHostIcon } from "@/lib/utils" import { ConnectionType } from "@sourcebot/db" import { @@ -92,7 +91,7 @@ export const columns: ColumnDef[] = [ width={20} height={20} /> - + {connection.name} {connection.isFirstTimeSync && ( diff --git a/packages/web/src/app/[domain]/settings/connections/layout.tsx b/packages/web/src/app/(app)/settings/connections/layout.tsx similarity index 72% rename from packages/web/src/app/[domain]/settings/connections/layout.tsx rename to packages/web/src/app/(app)/settings/connections/layout.tsx index 0143b4e82..3d14c7dd7 100644 --- a/packages/web/src/app/[domain]/settings/connections/layout.tsx +++ b/packages/web/src/app/(app)/settings/connections/layout.tsx @@ -1,5 +1,6 @@ import { getMe } from "@/actions"; -import { getOrgFromDomain } from "@/data/org"; +import { SINGLE_TENANT_ORG_ID } from "@/lib/constants"; +import { prisma } from "@/prisma"; import { ServiceErrorException } from "@/lib/serviceError"; import { notFound } from "next/navigation"; import { isServiceError } from "@/lib/utils"; @@ -8,15 +9,10 @@ import { OrgRole } from "@sourcebot/db"; interface ConnectionsLayoutProps { children: React.ReactNode; - params: Promise<{ - domain: string - }>; } -export default async function ConnectionsLayout({ children, params }: ConnectionsLayoutProps) { - const { domain } = await params; - - const org = await getOrgFromDomain(domain); +export default async function ConnectionsLayout({ children }: ConnectionsLayoutProps) { + const org = await prisma.org.findUnique({ where: { id: SINGLE_TENANT_ORG_ID } }); if (!org) { throw new Error("Organization not found"); } diff --git a/packages/web/src/app/[domain]/settings/connections/page.tsx b/packages/web/src/app/(app)/settings/connections/page.tsx similarity index 100% rename from packages/web/src/app/[domain]/settings/connections/page.tsx rename to packages/web/src/app/(app)/settings/connections/page.tsx diff --git a/packages/web/src/app/[domain]/settings/layout.tsx b/packages/web/src/app/(app)/settings/layout.tsx similarity index 80% rename from packages/web/src/app/[domain]/settings/layout.tsx rename to packages/web/src/app/(app)/settings/layout.tsx index 4dcd24224..417042b1d 100644 --- a/packages/web/src/app/[domain]/settings/layout.tsx +++ b/packages/web/src/app/(app)/settings/layout.tsx @@ -9,12 +9,10 @@ import { getConnectionStats, getOrgAccountRequests } from "@/actions"; import { ServiceErrorException } from "@/lib/serviceError"; import { OrgRole } from "@prisma/client"; import { env, hasEntitlement } from "@sourcebot/shared"; -import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants"; import { withAuth } from "@/middleware/withAuth"; interface LayoutProps { children: React.ReactNode; - params: Promise<{ domain: string }>; } export const metadata: Metadata = { @@ -24,19 +22,13 @@ export const metadata: Metadata = { export default async function SettingsLayout( props: LayoutProps ) { - const params = await props.params; - - const { - domain - } = params; - const { children } = props; const session = await auth(); if (!session) { - return redirect(`/${domain}`); + return redirect('/'); } const sidebarNavItems = await getSidebarNavItems(); @@ -46,7 +38,7 @@ export default async function SettingsLayout( return (
- +
@@ -86,44 +78,44 @@ export const getSidebarNavItems = async () => ...(role === OrgRole.OWNER ? [ { title: "Access", - href: `/${SINGLE_TENANT_ORG_DOMAIN}/settings/access`, + href: `/settings/access`, } ] : []), ...(role === OrgRole.OWNER ? [{ title: "Members", isNotificationDotVisible: numJoinRequests !== undefined && numJoinRequests > 0, - href: `/${SINGLE_TENANT_ORG_DOMAIN}/settings/members`, + href: `/settings/members`, }] : []), ...(role === OrgRole.OWNER ? [ { title: "Connections", - href: `/${SINGLE_TENANT_ORG_DOMAIN}/settings/connections`, - hrefRegex: `/${SINGLE_TENANT_ORG_DOMAIN}/settings/connections(/[^/]+)?$`, + href: `/settings/connections`, + hrefRegex: `/settings/connections(/[^/]+)?$`, isNotificationDotVisible: connectionStats.numberOfConnectionsWithFirstTimeSyncJobsInProgress > 0, } ] : []), ...(env.DISABLE_API_KEY_USAGE_FOR_NON_OWNER_USERS === 'false' || role === OrgRole.OWNER ? [ { title: "API Keys", - href: `/${SINGLE_TENANT_ORG_DOMAIN}/settings/apiKeys`, + href: `/settings/apiKeys`, } ] : []), ...(role === OrgRole.OWNER ? [ { title: "Analytics", - href: `/${SINGLE_TENANT_ORG_DOMAIN}/settings/analytics`, + href: `/settings/analytics`, }, ] : []), ...(hasEntitlement("sso") ? [ { title: "Linked Accounts", - href: `/${SINGLE_TENANT_ORG_DOMAIN}/settings/linked-accounts`, + href: `/settings/linked-accounts`, } ] : []), ...(role === OrgRole.OWNER ? [ { title: "License", - href: `/${SINGLE_TENANT_ORG_DOMAIN}/settings/license`, + href: `/settings/license`, } ] : []), ] diff --git a/packages/web/src/app/[domain]/settings/license/page.tsx b/packages/web/src/app/(app)/settings/license/page.tsx similarity index 94% rename from packages/web/src/app/[domain]/settings/license/page.tsx rename to packages/web/src/app/(app)/settings/license/page.tsx index 7413db9b8..bd4822b8b 100644 --- a/packages/web/src/app/[domain]/settings/license/page.tsx +++ b/packages/web/src/app/(app)/settings/license/page.tsx @@ -4,24 +4,13 @@ import { Info, Mail } from "lucide-react"; import { getMe, getOrgMembers } from "@/actions"; import { isServiceError } from "@/lib/utils"; import { ServiceErrorException } from "@/lib/serviceError"; -import { getOrgFromDomain } from "@/data/org"; +import { SINGLE_TENANT_ORG_ID } from "@/lib/constants"; +import { prisma } from "@/prisma"; import { OrgRole } from "@sourcebot/db"; import { redirect } from "next/navigation"; -interface LicensePageProps { - params: Promise<{ - domain: string; - }> -} - -export default async function LicensePage(props: LicensePageProps) { - const params = await props.params; - - const { - domain - } = params; - - const org = await getOrgFromDomain(domain); +export default async function LicensePage() { + const org = await prisma.org.findUnique({ where: { id: SINGLE_TENANT_ORG_ID } }); if (!org) { throw new Error("Organization not found"); } @@ -37,7 +26,7 @@ export default async function LicensePage(props: LicensePageProps) { } if (userRoleInOrg !== OrgRole.OWNER) { - redirect(`/${domain}/settings`); + redirect('/settings'); } const licenseKey = getLicenseKey(); diff --git a/packages/web/src/app/[domain]/settings/linked-accounts/page.tsx b/packages/web/src/app/(app)/settings/linked-accounts/page.tsx similarity index 100% rename from packages/web/src/app/[domain]/settings/linked-accounts/page.tsx rename to packages/web/src/app/(app)/settings/linked-accounts/page.tsx diff --git a/packages/web/src/app/[domain]/settings/members/components/inviteMemberCard.tsx b/packages/web/src/app/(app)/settings/members/components/inviteMemberCard.tsx similarity index 100% rename from packages/web/src/app/[domain]/settings/members/components/inviteMemberCard.tsx rename to packages/web/src/app/(app)/settings/members/components/inviteMemberCard.tsx diff --git a/packages/web/src/app/[domain]/settings/members/components/invitesList.tsx b/packages/web/src/app/(app)/settings/members/components/invitesList.tsx similarity index 100% rename from packages/web/src/app/[domain]/settings/members/components/invitesList.tsx rename to packages/web/src/app/(app)/settings/members/components/invitesList.tsx diff --git a/packages/web/src/app/[domain]/settings/members/components/membersList.tsx b/packages/web/src/app/(app)/settings/members/components/membersList.tsx similarity index 100% rename from packages/web/src/app/[domain]/settings/members/components/membersList.tsx rename to packages/web/src/app/(app)/settings/members/components/membersList.tsx diff --git a/packages/web/src/app/[domain]/settings/members/components/requestsList.tsx b/packages/web/src/app/(app)/settings/members/components/requestsList.tsx similarity index 100% rename from packages/web/src/app/[domain]/settings/members/components/requestsList.tsx rename to packages/web/src/app/(app)/settings/members/components/requestsList.tsx diff --git a/packages/web/src/app/[domain]/settings/members/page.tsx b/packages/web/src/app/(app)/settings/members/page.tsx similarity index 96% rename from packages/web/src/app/[domain]/settings/members/page.tsx rename to packages/web/src/app/(app)/settings/members/page.tsx index 4b384d149..fc1548933 100644 --- a/packages/web/src/app/[domain]/settings/members/page.tsx +++ b/packages/web/src/app/(app)/settings/members/page.tsx @@ -1,7 +1,8 @@ import { MembersList } from "./components/membersList"; import { getOrgMembers } from "@/actions"; import { isServiceError } from "@/lib/utils"; -import { getOrgFromDomain } from "@/data/org"; +import { SINGLE_TENANT_ORG_ID } from "@/lib/constants"; +import { prisma } from "@/prisma"; import { InviteMemberCard } from "./components/inviteMemberCard"; import { Tabs, TabsContent } from "@/components/ui/tabs"; import { TabSwitcher } from "@/components/ui/tab-switcher"; @@ -16,9 +17,6 @@ import { NotificationDot } from "../../components/notificationDot"; import { Badge } from "@/components/ui/badge"; interface MembersSettingsPageProps { - params: Promise<{ - domain: string - }>, searchParams: Promise<{ tab?: string }> @@ -31,13 +29,7 @@ export default async function MembersSettingsPage(props: MembersSettingsPageProp tab } = searchParams; - const params = await props.params; - - const { - domain - } = params; - - const org = await getOrgFromDomain(domain); + const org = await prisma.org.findUnique({ where: { id: SINGLE_TENANT_ORG_ID } }); if (!org) { throw new Error("Organization not found"); } @@ -53,7 +45,7 @@ export default async function MembersSettingsPage(props: MembersSettingsPageProp } if (userRoleInOrg !== OrgRole.OWNER) { - redirect(`/${domain}/settings`); + redirect('/settings'); } const members = await getOrgMembers(); diff --git a/packages/web/src/app/[domain]/settings/page.tsx b/packages/web/src/app/(app)/settings/page.tsx similarity index 81% rename from packages/web/src/app/[domain]/settings/page.tsx rename to packages/web/src/app/(app)/settings/page.tsx index a6f3ab762..e059b95c5 100644 --- a/packages/web/src/app/[domain]/settings/page.tsx +++ b/packages/web/src/app/(app)/settings/page.tsx @@ -3,12 +3,11 @@ import { getSidebarNavItems } from "./layout"; import { isServiceError } from "@/lib/utils"; import { ServiceErrorException } from "@/lib/serviceError"; import { auth } from "@/auth"; -import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants"; export default async function SettingsPage() { const session = await auth(); if (!session) { - return redirect(`/${SINGLE_TENANT_ORG_DOMAIN}`); + return redirect(`/`); } const items = await getSidebarNavItems(); diff --git a/packages/web/src/app/[domain]/settings/apiKeys/layout.tsx b/packages/web/src/app/[domain]/settings/apiKeys/layout.tsx deleted file mode 100644 index 9a3a2f8fa..000000000 --- a/packages/web/src/app/[domain]/settings/apiKeys/layout.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { getMe } from "@/actions"; -import { ServiceErrorException } from "@/lib/serviceError"; -import { notFound } from "next/navigation"; -import { isServiceError } from "@/lib/utils"; -import { OrgRole } from "@sourcebot/db"; -import { getOrgFromDomain } from "@/data/org"; -import { StatusCodes } from "http-status-codes"; -import { ErrorCode } from "@/lib/errorCodes"; -import { env } from "@sourcebot/shared"; - -export default async function ApiKeysLayout({ children, params }: { children: React.ReactNode, params: Promise<{ domain: string }> }) { - const { domain } = await params; - - const org = await getOrgFromDomain(domain); - if (!org) { - throw new ServiceErrorException({ - statusCode: StatusCodes.NOT_FOUND, - errorCode: ErrorCode.ORG_NOT_FOUND, - message: "Organization not found", - }); - } - - const me = await getMe(); - if (isServiceError(me)) { - throw new ServiceErrorException(me); - } - - const userRoleInOrg = me.memberships.find((membership) => membership.id === org.id)?.role; - if (!userRoleInOrg) { - throw new ServiceErrorException({ - statusCode: StatusCodes.INTERNAL_SERVER_ERROR, - errorCode: ErrorCode.UNEXPECTED_ERROR, - message: "User role not found", - }); - } - - if (env.DISABLE_API_KEY_USAGE_FOR_NON_OWNER_USERS === 'true' && userRoleInOrg !== OrgRole.OWNER) { - return notFound(); - } - - return children; -} \ No newline at end of file diff --git a/packages/web/src/app/api/(server)/repo-status/[repoId]/route.ts b/packages/web/src/app/api/(server)/repo-status/[repoId]/route.ts index 3bd54bf9b..12b8aa724 100644 --- a/packages/web/src/app/api/(server)/repo-status/[repoId]/route.ts +++ b/packages/web/src/app/api/(server)/repo-status/[repoId]/route.ts @@ -1,4 +1,4 @@ -import { getRepoInfo } from "@/app/[domain]/askgh/[owner]/[repo]/api"; +import { getRepoInfo } from "@/app/(app)/askgh/[owner]/[repo]/api"; import { apiHandler } from "@/lib/apiHandler"; import { serviceErrorResponse } from "@/lib/serviceError"; import { isServiceError } from "@/lib/utils"; diff --git a/packages/web/src/app/api/(server)/repos/listReposApi.ts b/packages/web/src/app/api/(server)/repos/listReposApi.ts index c85098ae5..6844feb7e 100644 --- a/packages/web/src/app/api/(server)/repos/listReposApi.ts +++ b/packages/web/src/app/api/(server)/repos/listReposApi.ts @@ -2,7 +2,7 @@ import { sew } from "@/middleware/sew"; import { getAuditService } from "@/ee/features/audit/factory"; import { ListReposQueryParams, RepositoryQuery } from "@/lib/types"; import { withOptionalAuth } from "@/middleware/withAuth"; -import { getBrowsePath } from "@/app/[domain]/browse/hooks/utils"; +import { getBrowsePath } from "@/app/(app)/browse/hooks/utils"; import { env } from "@sourcebot/shared"; import { headers } from "next/headers"; diff --git a/packages/web/src/app/api/[domain]/repos/[repoId]/image/route.ts b/packages/web/src/app/api/repos/[repoId]/image/route.ts similarity index 90% rename from packages/web/src/app/api/[domain]/repos/[repoId]/image/route.ts rename to packages/web/src/app/api/repos/[repoId]/image/route.ts index 93afdf6ba..bcc1911ad 100644 --- a/packages/web/src/app/api/[domain]/repos/[repoId]/image/route.ts +++ b/packages/web/src/app/api/repos/[repoId]/image/route.ts @@ -5,7 +5,7 @@ import { NextRequest } from "next/server"; export const GET = apiHandler(async ( _request: NextRequest, - { params }: { params: Promise<{ domain: string; repoId: string }> } + { params }: { params: Promise<{ repoId: string }> } ) => { const { repoId } = await params; const repoIdNum = parseInt(repoId); @@ -25,4 +25,4 @@ export const GET = apiHandler(async ( 'Cache-Control': 'public, max-age=3600', }, }); -}); \ No newline at end of file +}); diff --git a/packages/web/src/app/components/organizationAccessSettings.tsx b/packages/web/src/app/components/organizationAccessSettings.tsx index adb039d60..906c13aa2 100644 --- a/packages/web/src/app/components/organizationAccessSettings.tsx +++ b/packages/web/src/app/components/organizationAccessSettings.tsx @@ -1,13 +1,13 @@ import { createInviteLink } from "@/lib/utils" import { AnonymousAccessToggle } from "./anonymousAccessToggle" import { OrganizationAccessSettingsWrapper } from "./organizationAccessSettingsWrapper" -import { getOrgFromDomain } from "@/data/org" import { getOrgMetadata } from "@/lib/utils" -import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants" +import { SINGLE_TENANT_ORG_ID } from "@/lib/constants" +import { prisma } from "@/prisma" import { hasEntitlement, env } from "@sourcebot/shared" export async function OrganizationAccessSettings() { - const org = await getOrgFromDomain(SINGLE_TENANT_ORG_DOMAIN); + const org = await prisma.org.findUnique({ where: { id: SINGLE_TENANT_ORG_ID } }); if (!org) { return
Error loading organization
} diff --git a/packages/web/src/app/invite/page.tsx b/packages/web/src/app/invite/page.tsx index 539bf781a..e44cc01e1 100644 --- a/packages/web/src/app/invite/page.tsx +++ b/packages/web/src/app/invite/page.tsx @@ -1,7 +1,6 @@ import { auth } from "@/auth"; import { prisma } from "@/prisma"; -import { getOrgFromDomain } from "@/data/org"; -import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants"; +import { SINGLE_TENANT_ORG_ID } from "@/lib/constants"; import { notFound, redirect } from "next/navigation"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { SourcebotLogo } from "@/app/components/sourcebotLogo"; @@ -18,7 +17,7 @@ interface InvitePageProps { export default async function InvitePage(props: InvitePageProps) { const searchParams = await props.searchParams; - const org = await getOrgFromDomain(SINGLE_TENANT_ORG_DOMAIN); + const org = await prisma.org.findUnique({ where: { id: SINGLE_TENANT_ORG_ID } }); if (!org || !org.isOnboarded) { return redirect("/onboard"); } @@ -45,7 +44,7 @@ export default async function InvitePage(props: InvitePageProps) { // If already a member, redirect to the organization if (membership) { - redirect(`/${SINGLE_TENANT_ORG_DOMAIN}`); + redirect(`/`); } // User is logged in but not a member, show join invitation diff --git a/packages/web/src/app/login/page.tsx b/packages/web/src/app/login/page.tsx index c78fa3ee6..7d163d370 100644 --- a/packages/web/src/app/login/page.tsx +++ b/packages/web/src/app/login/page.tsx @@ -3,8 +3,8 @@ import { LoginForm } from "./components/loginForm"; import { redirect } from "next/navigation"; import { Footer } from "@/app/components/footer"; import { getIdentityProviderMetadata } from "@/lib/identityProviders"; -import { getOrgFromDomain } from "@/data/org"; -import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants"; +import { SINGLE_TENANT_ORG_ID } from "@/lib/constants"; +import { prisma } from "@/prisma"; import { getAnonymousAccessStatus } from "@/actions"; import { isServiceError } from "@/lib/utils"; import { env } from "@sourcebot/shared"; @@ -23,13 +23,13 @@ export default async function Login(props: LoginProps) { return redirect("/"); } - const org = await getOrgFromDomain(SINGLE_TENANT_ORG_DOMAIN); + const org = await prisma.org.findUnique({ where: { id: SINGLE_TENANT_ORG_ID } }); if (!org || !org.isOnboarded) { return redirect("/onboard"); } const providers = getIdentityProviderMetadata(); - const anonymousAccessStatus = await getAnonymousAccessStatus(SINGLE_TENANT_ORG_DOMAIN); + const anonymousAccessStatus = await getAnonymousAccessStatus(); const isAnonymousAccessEnabled = !isServiceError(anonymousAccessStatus) && anonymousAccessStatus; return ( diff --git a/packages/web/src/app/not-found.tsx b/packages/web/src/app/not-found.tsx index 2c0b233b7..b4a1f8391 100644 --- a/packages/web/src/app/not-found.tsx +++ b/packages/web/src/app/not-found.tsx @@ -1,4 +1,4 @@ -import { PageNotFound } from "./[domain]/components/pageNotFound"; +import { PageNotFound } from "./(app)/components/pageNotFound"; export default function NotFoundPage() { return ( diff --git a/packages/web/src/app/onboard/page.tsx b/packages/web/src/app/onboard/page.tsx index 5f22cddd6..0c5c99623 100644 --- a/packages/web/src/app/onboard/page.tsx +++ b/packages/web/src/app/onboard/page.tsx @@ -9,8 +9,7 @@ import { auth } from "@/auth"; import { getIdentityProviderMetadata } from "@/lib/identityProviders"; import { OrganizationAccessSettings } from "@/app/components/organizationAccessSettings"; import { CompleteOnboardingButton } from "./components/completeOnboardingButton"; -import { getOrgFromDomain } from "@/data/org"; -import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants"; +import { SINGLE_TENANT_ORG_ID } from "@/lib/constants"; import { prisma } from "@/prisma"; import { OrgRole } from "@sourcebot/db"; import { LogoutEscapeHatch } from "@/app/components/logoutEscapeHatch"; @@ -18,7 +17,7 @@ import { redirect } from "next/navigation"; import { BetweenHorizontalStart, Brain, GitBranchIcon, LockIcon } from "lucide-react"; import { hasEntitlement } from "@sourcebot/shared"; import { env } from "@sourcebot/shared"; -import { GcpIapAuth } from "@/app/[domain]/components/gcpIapAuth"; +import { GcpIapAuth } from "@/app/(app)/components/gcpIapAuth"; interface OnboardingProps { searchParams?: Promise<{ step?: string }>; @@ -42,7 +41,7 @@ interface ResourceCard { export default async function Onboarding(props: OnboardingProps) { const searchParams = await props.searchParams; const providers = getIdentityProviderMetadata(); - const org = await getOrgFromDomain(SINGLE_TENANT_ORG_DOMAIN); + const org = await prisma.org.findUnique({ where: { id: SINGLE_TENANT_ORG_ID } }); const session = await auth(); if (!org) { diff --git a/packages/web/src/app/page.tsx b/packages/web/src/app/page.tsx deleted file mode 100644 index defd08d6f..000000000 --- a/packages/web/src/app/page.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { auth } from "@/auth"; -import { redirect } from "next/navigation"; -import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants"; -import { prisma } from "@/prisma"; - -// @note: we were hitting `PrismaClientInitializationError` errors during -// build time. Next.js performs a static generation probe on all pages during -// `next build`, running each page component to determine if it's static or -// dynamic. `force-dynamic` skips the probe entirely so this page is always -// rendered at request time. -export const dynamic = 'force-dynamic'; - -export default async function Page() { - const org = await prisma.org.findUnique({ - where: { - domain: SINGLE_TENANT_ORG_DOMAIN - } - }); - - if (!org || !org.isOnboarded) { - return redirect("/onboard"); - } - - const session = await auth(); - if (!session) { - return redirect("/login"); - } - - return redirect(`/${SINGLE_TENANT_ORG_DOMAIN}`); -} \ No newline at end of file diff --git a/packages/web/src/app/redeem/components/acceptInviteCard.tsx b/packages/web/src/app/redeem/components/acceptInviteCard.tsx index 4d9b3ce23..fcfc36ee0 100644 --- a/packages/web/src/app/redeem/components/acceptInviteCard.tsx +++ b/packages/web/src/app/redeem/components/acceptInviteCard.tsx @@ -17,7 +17,6 @@ import { isServiceError } from "@/lib/utils"; interface AcceptInviteCardProps { inviteId: string; orgName: string; - orgDomain: string; orgImageUrl?: string; host: { name?: string; @@ -30,7 +29,7 @@ interface AcceptInviteCardProps { }; } -export const AcceptInviteCard = ({ inviteId, orgName, orgDomain, orgImageUrl, host, recipient }: AcceptInviteCardProps) => { +export const AcceptInviteCard = ({ inviteId, orgName, orgImageUrl, host, recipient }: AcceptInviteCardProps) => { const [isLoading, setIsLoading] = useState(false); const router = useRouter(); const { toast } = useToast(); @@ -48,13 +47,13 @@ export const AcceptInviteCard = ({ inviteId, orgName, orgDomain, orgImageUrl, ho toast({ description: `✅ You are now a member of the ${orgName} organization.`, }); - router.push(`/${orgDomain}`); + router.push('/'); } }) .finally(() => { setIsLoading(false); }); - }, [inviteId, orgDomain, orgName, router, toast]); + }, [inviteId, orgName, router, toast]); return ( diff --git a/packages/web/src/app/redeem/page.tsx b/packages/web/src/app/redeem/page.tsx index 77c482f2b..31f91562d 100644 --- a/packages/web/src/app/redeem/page.tsx +++ b/packages/web/src/app/redeem/page.tsx @@ -5,8 +5,8 @@ import { isServiceError } from "@/lib/utils"; import { AcceptInviteCard } from './components/acceptInviteCard'; import { LogoutEscapeHatch } from '../components/logoutEscapeHatch'; import { InviteNotFoundCard } from './components/inviteNotFoundCard'; -import { getOrgFromDomain } from '@/data/org'; -import { SINGLE_TENANT_ORG_DOMAIN } from '@/lib/constants'; +import { SINGLE_TENANT_ORG_ID } from '@/lib/constants'; +import { prisma } from '@/prisma'; interface RedeemPageProps { searchParams: Promise<{ @@ -16,7 +16,7 @@ interface RedeemPageProps { export default async function RedeemPage(props: RedeemPageProps) { const searchParams = await props.searchParams; - const org = await getOrgFromDomain(SINGLE_TENANT_ORG_DOMAIN); + const org = await prisma.org.findUnique({ where: { id: SINGLE_TENANT_ORG_ID } }); if (!org || !org.isOnboarded) { return redirect("/onboard"); } @@ -42,7 +42,6 @@ export default async function RedeemPage(props: RedeemPageProps) { { - const org = await prisma.org.findUnique({ - where: { - domain: domain - } - }); - - return org; -} \ No newline at end of file diff --git a/packages/web/src/ee/features/analytics/analyticsContent.tsx b/packages/web/src/ee/features/analytics/analyticsContent.tsx index fb9d5c15b..f408c92d3 100644 --- a/packages/web/src/ee/features/analytics/analyticsContent.tsx +++ b/packages/web/src/ee/features/analytics/analyticsContent.tsx @@ -6,7 +6,6 @@ import { Users, LucideIcon, Search, ArrowRight, Activity, Calendar, MessageCircl import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { ChartContainer } from "@/components/ui/chart" import { useQuery } from "@tanstack/react-query" -import { useDomain } from "@/hooks/useDomain" import { unwrapServiceError } from "@/lib/utils" import { Skeleton } from "@/components/ui/skeleton" import { AnalyticsRow } from "./types" @@ -383,7 +382,6 @@ function LoadingSkeleton() { } export function AnalyticsContent() { - const domain = useDomain() const { theme } = useTheme() // Time period selector state @@ -395,7 +393,7 @@ export function AnalyticsContent() { isError, error } = useQuery({ - queryKey: ["analytics", domain], + queryKey: ["analytics"], queryFn: () => unwrapServiceError(getAnalytics()), }) diff --git a/packages/web/src/ee/features/codeNav/components/exploreMenu/index.tsx b/packages/web/src/ee/features/codeNav/components/exploreMenu/index.tsx index 0fa8a5c26..e5e4b9719 100644 --- a/packages/web/src/ee/features/codeNav/components/exploreMenu/index.tsx +++ b/packages/web/src/ee/features/codeNav/components/exploreMenu/index.tsx @@ -1,13 +1,12 @@ 'use client'; -import { useBrowseState } from "@/app/[domain]/browse/hooks/useBrowseState"; +import { useBrowseState } from "@/app/(app)/browse/hooks/useBrowseState"; import { findSearchBasedSymbolDefinitions, findSearchBasedSymbolReferences } from "@/app/api/(client)/client"; import { AnimatedResizableHandle } from "@/components/ui/animatedResizableHandle"; import { Badge } from "@/components/ui/badge"; import { ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable"; import { Toggle } from "@/components/ui/toggle"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; -import { useDomain } from "@/hooks/useDomain"; import { measure, unwrapServiceError } from "@/lib/utils"; import { useQuery } from "@tanstack/react-query"; import clsx from "clsx"; @@ -31,7 +30,6 @@ interface ExploreMenuProps { export const ExploreMenu = ({ selectedSymbolInfo, }: ExploreMenuProps) => { - const domain = useDomain(); const captureEvent = useCaptureEvent(); const { state: { activeExploreMenuTab }, @@ -46,7 +44,7 @@ export const ExploreMenu = ({ isPending: isReferencesResponsePending, isLoading: isReferencesResponseLoading, } = useQuery({ - queryKey: ["references", selectedSymbolInfo.symbolName, selectedSymbolInfo.repoName, selectedSymbolInfo.revisionName, selectedSymbolInfo.language, domain, isGlobalSearchEnabled], + queryKey: ["references", selectedSymbolInfo.symbolName, selectedSymbolInfo.repoName, selectedSymbolInfo.revisionName, selectedSymbolInfo.language, isGlobalSearchEnabled], queryFn: async () => { const response = await measure(() => unwrapServiceError( findSearchBasedSymbolReferences({ @@ -72,7 +70,7 @@ export const ExploreMenu = ({ isPending: isDefinitionsResponsePending, isLoading: isDefinitionsResponseLoading, } = useQuery({ - queryKey: ["definitions", selectedSymbolInfo.symbolName, selectedSymbolInfo.repoName, selectedSymbolInfo.revisionName, selectedSymbolInfo.language, domain, isGlobalSearchEnabled], + queryKey: ["definitions", selectedSymbolInfo.symbolName, selectedSymbolInfo.repoName, selectedSymbolInfo.revisionName, selectedSymbolInfo.language, isGlobalSearchEnabled], queryFn: async () => { const response = await measure(() => unwrapServiceError( findSearchBasedSymbolDefinitions({ diff --git a/packages/web/src/ee/features/codeNav/components/exploreMenu/referenceList.tsx b/packages/web/src/ee/features/codeNav/components/exploreMenu/referenceList.tsx index 3a65d967c..4d1695c03 100644 --- a/packages/web/src/ee/features/codeNav/components/exploreMenu/referenceList.tsx +++ b/packages/web/src/ee/features/codeNav/components/exploreMenu/referenceList.tsx @@ -1,8 +1,8 @@ 'use client'; -import { getBrowsePath } from "@/app/[domain]/browse/hooks/utils"; -import { PathHeader } from "@/app/[domain]/components/pathHeader"; -import { LightweightCodeHighlighter } from "@/app/[domain]/components/lightweightCodeHighlighter"; +import { getBrowsePath } from "@/app/(app)/browse/hooks/utils"; +import { PathHeader } from "@/app/(app)/components/pathHeader"; +import { LightweightCodeHighlighter } from "@/app/(app)/components/lightweightCodeHighlighter"; import { FindRelatedSymbolsResponse } from "@/features/codeNav/types"; import { RepositoryInfo, SourceRange } from "@/features/search"; import { useMemo, useRef } from "react"; diff --git a/packages/web/src/ee/features/codeNav/components/symbolHoverPopup/index.tsx b/packages/web/src/ee/features/codeNav/components/symbolHoverPopup/index.tsx index 1e3616f7d..7e7bcc252 100644 --- a/packages/web/src/ee/features/codeNav/components/symbolHoverPopup/index.tsx +++ b/packages/web/src/ee/features/codeNav/components/symbolHoverPopup/index.tsx @@ -1,4 +1,4 @@ -import { useBrowseNavigation } from "@/app/[domain]/browse/hooks/useBrowseNavigation"; +import { useBrowseNavigation } from "@/app/(app)/browse/hooks/useBrowseNavigation"; import { KeyboardShortcutHint } from "@/app/components/keyboardShortcutHint"; import { useToast } from "@/components/hooks/use-toast"; import { Button } from "@/components/ui/button"; diff --git a/packages/web/src/ee/features/codeNav/components/symbolHoverPopup/symbolDefinitionPreview.tsx b/packages/web/src/ee/features/codeNav/components/symbolHoverPopup/symbolDefinitionPreview.tsx index a087273c6..eaa384dbc 100644 --- a/packages/web/src/ee/features/codeNav/components/symbolHoverPopup/symbolDefinitionPreview.tsx +++ b/packages/web/src/ee/features/codeNav/components/symbolHoverPopup/symbolDefinitionPreview.tsx @@ -1,6 +1,6 @@ import { Badge } from "@/components/ui/badge"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; -import { LightweightCodeHighlighter } from "@/app/[domain]/components/lightweightCodeHighlighter"; +import { LightweightCodeHighlighter } from "@/app/(app)/components/lightweightCodeHighlighter"; import { useMemo } from "react"; import { SourceRange } from "@/features/search"; diff --git a/packages/web/src/ee/features/codeNav/components/symbolHoverPopup/useHoveredOverSymbolInfo.ts b/packages/web/src/ee/features/codeNav/components/symbolHoverPopup/useHoveredOverSymbolInfo.ts index bca813165..da5652696 100644 --- a/packages/web/src/ee/features/codeNav/components/symbolHoverPopup/useHoveredOverSymbolInfo.ts +++ b/packages/web/src/ee/features/codeNav/components/symbolHoverPopup/useHoveredOverSymbolInfo.ts @@ -1,7 +1,6 @@ import { findSearchBasedSymbolDefinitions } from "@/app/api/(client)/client"; import { SourceRange } from "@/features/search"; import useCaptureEvent from "@/hooks/useCaptureEvent"; -import { useDomain } from "@/hooks/useDomain"; import { measure, unwrapServiceError } from "@/lib/utils"; import { useQuery } from "@tanstack/react-query"; import { ReactCodeMirrorRef } from "@uiw/react-codemirror"; @@ -46,7 +45,6 @@ export const useHoveredOverSymbolInfo = ({ const mouseOverTimerRef = useRef(null); const mouseOutTimerRef = useRef(null); - const domain = useDomain(); const [isVisible, setIsVisible] = useState(false); const [symbolElement, setSymbolElement] = useState(null); @@ -57,7 +55,7 @@ export const useHoveredOverSymbolInfo = ({ const captureEvent = useCaptureEvent(); const { data: symbolDefinitions, isLoading: isSymbolDefinitionsLoading } = useQuery({ - queryKey: ["definitions", symbolName, revisionName, language, domain, repoName], + queryKey: ["definitions", symbolName, revisionName, language, repoName], queryFn: async () => { const response = await measure(() => unwrapServiceError( findSearchBasedSymbolDefinitions({ @@ -137,7 +135,7 @@ export const useHoveredOverSymbolInfo = ({ view.dom.removeEventListener("mouseover", handleMouseOver); view.dom.removeEventListener("mouseout", handleMouseOut); }; - }, [editorRef, domain, clearTimers]); + }, [editorRef, clearTimers]); // Extract the highlight range of the symbolElement from the editor view. const highlightRange = useMemo((): SourceRange | undefined => { diff --git a/packages/web/src/emails/joinRequestApprovedEmail.tsx b/packages/web/src/emails/joinRequestApprovedEmail.tsx index 696d19259..f43a26320 100644 --- a/packages/web/src/emails/joinRequestApprovedEmail.tsx +++ b/packages/web/src/emails/joinRequestApprovedEmail.tsx @@ -23,17 +23,15 @@ interface JoinRequestApprovedEmailProps { avatarUrl?: string; }, orgName: string; - orgDomain: string; } export const JoinRequestApprovedEmail = ({ baseUrl, user, orgName, - orgDomain, }: JoinRequestApprovedEmailProps) => { const previewText = `Your request to join ${orgName} on Sourcebot has been approved`; - const orgLink = `${baseUrl}/${orgDomain}`; + const orgLink = baseUrl; return ( @@ -90,7 +88,6 @@ JoinRequestApprovedEmail.PreviewProps = { avatarUrl: SOURCEBOT_PLACEHOLDER_AVATAR_URL, }, orgName: 'Enigma', - orgDomain: '~', } satisfies JoinRequestApprovedEmailProps; export default JoinRequestApprovedEmail; \ No newline at end of file diff --git a/packages/web/src/emails/joinRequestSubmittedEmail.tsx b/packages/web/src/emails/joinRequestSubmittedEmail.tsx index dd690fcf8..45091438c 100644 --- a/packages/web/src/emails/joinRequestSubmittedEmail.tsx +++ b/packages/web/src/emails/joinRequestSubmittedEmail.tsx @@ -25,7 +25,6 @@ interface JoinRequestSubmittedEmailProps { avatarUrl?: string; }, orgName: string; - orgDomain: string; orgImageUrl?: string; } @@ -33,11 +32,10 @@ export const JoinRequestSubmittedEmail = ({ baseUrl, requestor, orgName, - orgDomain, orgImageUrl, }: JoinRequestSubmittedEmailProps) => { const previewText = `${requestor.name ?? requestor.email} has requested to join ${orgName} on Sourcebot`; - const reviewLink = `${baseUrl}/${encodeURIComponent(orgDomain)}/settings/members`; + const reviewLink = `${baseUrl}/settings/members`; return ( @@ -133,7 +131,6 @@ JoinRequestSubmittedEmail.PreviewProps = { email: 'alan.turing@example.com', }, orgName: 'Enigma', - orgDomain: '~', } satisfies JoinRequestSubmittedEmailProps; export default JoinRequestSubmittedEmail; diff --git a/packages/web/src/features/chat/components/chatBox/useSuggestionsData.ts b/packages/web/src/features/chat/components/chatBox/useSuggestionsData.ts index c1aeff8bf..77b360f12 100644 --- a/packages/web/src/features/chat/components/chatBox/useSuggestionsData.ts +++ b/packages/web/src/features/chat/components/chatBox/useSuggestionsData.ts @@ -2,7 +2,6 @@ import { useQuery } from "@tanstack/react-query"; import { FileSuggestion, RefineSuggestion, Suggestion, SuggestionMode } from "./types"; -import { useDomain } from "@/hooks/useDomain"; import { unwrapServiceError } from "@/lib/utils"; import { search } from "@/app/api/(client)/client"; import { useMemo } from "react"; @@ -27,10 +26,8 @@ export const useSuggestionsData = ({ suggestionQuery, selectedRepos, }: Props): { isLoading: boolean, suggestions: Suggestion[] } => { - const domain = useDomain(); - const { data: fileSuggestions, isLoading: _isLoadingFileSuggestions } = useQuery({ - queryKey: ["fileSuggestions-agentic", suggestionQuery, domain, selectedRepos], + queryKey: ["fileSuggestions-agentic", suggestionQuery, selectedRepos], queryFn: () => { const query = []; if (suggestionQuery.length > 0) { diff --git a/packages/web/src/features/chat/components/chatThread/answerCard.tsx b/packages/web/src/features/chat/components/chatThread/answerCard.tsx index 27f62c0bf..3f0a62a0d 100644 --- a/packages/web/src/features/chat/components/chatThread/answerCard.tsx +++ b/packages/web/src/features/chat/components/chatThread/answerCard.tsx @@ -9,7 +9,7 @@ import { MarkdownRenderer } from "./markdownRenderer"; import { forwardRef, memo, useCallback, useImperativeHandle, useRef, useState } from "react"; import { Toggle } from "@/components/ui/toggle"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; -import { CopyIconButton } from "@/app/[domain]/components/copyIconButton"; +import { CopyIconButton } from "@/app/(app)/components/copyIconButton"; import { useToast } from "@/components/hooks/use-toast"; import { convertLLMOutputToPortableMarkdown } from "../../utils"; import { submitFeedback } from "../../actions"; diff --git a/packages/web/src/features/chat/components/chatThread/chatThread.tsx b/packages/web/src/features/chat/components/chatThread/chatThread.tsx index b817d6cc8..1cdf8ef0b 100644 --- a/packages/web/src/features/chat/components/chatThread/chatThread.tsx +++ b/packages/web/src/features/chat/components/chatThread/chatThread.tsx @@ -27,11 +27,10 @@ import { isServiceError } from '@/lib/utils'; import { NotConfiguredErrorBanner } from '../notConfiguredErrorBanner'; import useCaptureEvent from '@/hooks/useCaptureEvent'; import { SignInPromptBanner } from './signInPromptBanner'; -import { DuplicateChatDialog } from '@/app/[domain]/chat/components/duplicateChatDialog'; +import { DuplicateChatDialog } from '@/app/(app)/chat/components/duplicateChatDialog'; import { LoginModal } from '@/app/components/loginModal'; import type { IdentityProviderMetadata } from '@/lib/identityProviders'; import { getAskGhLoginWallData } from '../../actions'; -import { SINGLE_TENANT_ORG_DOMAIN } from '@/lib/constants'; type ChatHistoryState = { scrollOffset?: number; @@ -340,7 +339,7 @@ export const ChatThread = ({ } captureEvent('wa_chat_duplicated', { chatId: defaultChatId }); - router.push(`/${SINGLE_TENANT_ORG_DOMAIN}/chat/${result.id}`); + router.push(`/chat/${result.id}`); return result.id; }, [defaultChatId, toast, router, captureEvent]); diff --git a/packages/web/src/features/chat/components/chatThread/codeBlock.tsx b/packages/web/src/features/chat/components/chatThread/codeBlock.tsx index 45b02c07f..c3a2121b0 100644 --- a/packages/web/src/features/chat/components/chatThread/codeBlock.tsx +++ b/packages/web/src/features/chat/components/chatThread/codeBlock.tsx @@ -1,6 +1,6 @@ 'use client'; -import { LightweightCodeHighlighter } from '@/app/[domain]/components/lightweightCodeHighlighter'; +import { LightweightCodeHighlighter } from '@/app/(app)/components/lightweightCodeHighlighter'; import { cn } from '@/lib/utils'; import { DoubleArrowDownIcon, DoubleArrowUpIcon } from '@radix-ui/react-icons'; import { useMemo, useState } from 'react'; diff --git a/packages/web/src/features/chat/components/chatThread/markdownRenderer.tsx b/packages/web/src/features/chat/components/chatThread/markdownRenderer.tsx index b7e5daeb5..427c7fd40 100644 --- a/packages/web/src/features/chat/components/chatThread/markdownRenderer.tsx +++ b/packages/web/src/features/chat/components/chatThread/markdownRenderer.tsx @@ -19,7 +19,6 @@ import { visit } from 'unist-util-visit'; import { CodeBlock } from './codeBlock'; import { FILE_REFERENCE_REGEX } from '@/features/chat/constants'; import { createFileReference } from '@/features/chat/utils'; -import { SINGLE_TENANT_ORG_DOMAIN } from '@/lib/constants'; import isEqual from "fast-deep-equal/react"; export const REFERENCE_PAYLOAD_ATTRIBUTE = 'data-reference-payload'; @@ -203,7 +202,7 @@ const MarkdownRendererComponent = forwardRef { e.preventDefault(); e.stopPropagation(); - const url = createPathWithQueryParams(`/${SINGLE_TENANT_ORG_DOMAIN}/search`, [SearchQueryParams.query, `"${text}"`]) + const url = createPathWithQueryParams(`/search`, [SearchQueryParams.query, `"${text}"`]) router.push(url); }} title="Search for snippet" diff --git a/packages/web/src/features/chat/components/chatThread/referencedFileSourceListItem.tsx b/packages/web/src/features/chat/components/chatThread/referencedFileSourceListItem.tsx index 75ecdd63d..ab8957868 100644 --- a/packages/web/src/features/chat/components/chatThread/referencedFileSourceListItem.tsx +++ b/packages/web/src/features/chat/components/chatThread/referencedFileSourceListItem.tsx @@ -1,6 +1,6 @@ 'use client'; -import { PathHeader } from "@/app/[domain]/components/pathHeader"; +import { PathHeader } from "@/app/(app)/components/pathHeader"; import { SymbolHoverPopup } from '@/ee/features/codeNav/components/symbolHoverPopup'; import { symbolHoverTargetsExtension } from "@/ee/features/codeNav/components/symbolHoverPopup/symbolHoverTargetsExtension"; import { useHasEntitlement } from "@/features/entitlements/useHasEntitlement"; diff --git a/packages/web/src/features/chat/components/chatThread/tools/fileRow.tsx b/packages/web/src/features/chat/components/chatThread/tools/fileRow.tsx index f2485e6cb..2428c06cb 100644 --- a/packages/web/src/features/chat/components/chatThread/tools/fileRow.tsx +++ b/packages/web/src/features/chat/components/chatThread/tools/fileRow.tsx @@ -1,7 +1,7 @@ 'use client'; import { VscodeFileIcon } from "@/app/components/vscodeFileIcon"; -import { getBrowsePath } from "@/app/[domain]/browse/hooks/utils"; +import { getBrowsePath } from "@/app/(app)/browse/hooks/utils"; import Link from "next/link"; type FileInfo = { diff --git a/packages/web/src/features/chat/components/chatThread/tools/listTreeToolComponent.tsx b/packages/web/src/features/chat/components/chatThread/tools/listTreeToolComponent.tsx index e7021c8c8..c990e21cc 100644 --- a/packages/web/src/features/chat/components/chatThread/tools/listTreeToolComponent.tsx +++ b/packages/web/src/features/chat/components/chatThread/tools/listTreeToolComponent.tsx @@ -3,7 +3,7 @@ import { ListTreeMetadata, ToolResult } from "@/features/tools"; import { RepoBadge } from "./repoBadge"; import { Separator } from "@/components/ui/separator"; -import { getBrowsePath } from "@/app/[domain]/browse/hooks/utils"; +import { getBrowsePath } from "@/app/(app)/browse/hooks/utils"; import { FolderIcon } from "lucide-react"; import Link from "next/link"; diff --git a/packages/web/src/features/chat/components/chatThread/tools/readFileToolComponent.tsx b/packages/web/src/features/chat/components/chatThread/tools/readFileToolComponent.tsx index 586afc276..4503892aa 100644 --- a/packages/web/src/features/chat/components/chatThread/tools/readFileToolComponent.tsx +++ b/packages/web/src/features/chat/components/chatThread/tools/readFileToolComponent.tsx @@ -2,7 +2,7 @@ import { ReadFileMetadata, ToolResult } from "@/features/tools"; import { VscodeFileIcon } from "@/app/components/vscodeFileIcon"; -import { getBrowsePath } from "@/app/[domain]/browse/hooks/utils"; +import { getBrowsePath } from "@/app/(app)/browse/hooks/utils"; import { Separator } from "@/components/ui/separator"; import Link from "next/link"; import { RepoBadge } from "./repoBadge"; diff --git a/packages/web/src/features/chat/components/chatThread/tools/repoBadge.tsx b/packages/web/src/features/chat/components/chatThread/tools/repoBadge.tsx index a5a352957..a8ab2e20a 100644 --- a/packages/web/src/features/chat/components/chatThread/tools/repoBadge.tsx +++ b/packages/web/src/features/chat/components/chatThread/tools/repoBadge.tsx @@ -1,6 +1,6 @@ 'use client'; -import { getBrowsePath } from "@/app/[domain]/browse/hooks/utils"; +import { getBrowsePath } from "@/app/(app)/browse/hooks/utils"; import { getCodeHostIcon } from "@/lib/utils"; import { CodeHostType } from "@sourcebot/db"; import Image from "next/image"; diff --git a/packages/web/src/features/chat/components/chatThread/tools/repoHeader.tsx b/packages/web/src/features/chat/components/chatThread/tools/repoHeader.tsx index 13f69cb42..12a956fde 100644 --- a/packages/web/src/features/chat/components/chatThread/tools/repoHeader.tsx +++ b/packages/web/src/features/chat/components/chatThread/tools/repoHeader.tsx @@ -1,6 +1,6 @@ 'use client'; -import { getBrowsePath } from "@/app/[domain]/browse/hooks/utils"; +import { getBrowsePath } from "@/app/(app)/browse/hooks/utils"; import { cn, getCodeHostIcon } from "@/lib/utils"; import { CodeHostType } from "@sourcebot/db"; import Image from "next/image"; diff --git a/packages/web/src/features/chat/components/chatThread/tools/toolOutputGuard.tsx b/packages/web/src/features/chat/components/chatThread/tools/toolOutputGuard.tsx index b31540b6f..aac756f4a 100644 --- a/packages/web/src/features/chat/components/chatThread/tools/toolOutputGuard.tsx +++ b/packages/web/src/features/chat/components/chatThread/tools/toolOutputGuard.tsx @@ -1,7 +1,7 @@ 'use client'; import { SBChatMessageToolTypes } from "@/features/chat/types"; -import { CopyIconButton } from "@/app/[domain]/components/copyIconButton"; +import { CopyIconButton } from "@/app/(app)/components/copyIconButton"; import { ToolUIPart } from "ai"; import { ChevronDown } from "lucide-react"; import { cn } from "@/lib/utils"; diff --git a/packages/web/src/features/chat/useCreateNewChatThread.ts b/packages/web/src/features/chat/useCreateNewChatThread.ts index e43f5063b..63ead0249 100644 --- a/packages/web/src/features/chat/useCreateNewChatThread.ts +++ b/packages/web/src/features/chat/useCreateNewChatThread.ts @@ -12,7 +12,6 @@ import { createPathWithQueryParams } from "@/lib/utils"; import { SearchScope, SetChatStatePayload } from "./types"; import { SET_CHAT_STATE_SESSION_STORAGE_KEY } from "./constants"; import { useSessionStorage } from "usehooks-ts"; -import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants"; import type { IdentityProviderMetadata } from "@/lib/identityProviders"; import useCaptureEvent from "@/hooks/useCaptureEvent"; @@ -52,7 +51,7 @@ export const useCreateNewChatThread = ({ isAuthenticated = false }: UseCreateNew selectedSearchScopes, }); - const url = createPathWithQueryParams(`/${SINGLE_TENANT_ORG_DOMAIN}/chat/${response.id}`); + const url = createPathWithQueryParams(`/chat/${response.id}`); router.push(url); }, [router, toast, setChatState]); diff --git a/packages/web/src/features/chat/utils.ts b/packages/web/src/features/chat/utils.ts index c1cbaaf2f..b4f80bec2 100644 --- a/packages/web/src/features/chat/utils.ts +++ b/packages/web/src/features/chat/utils.ts @@ -1,4 +1,4 @@ -import { BrowseHighlightRange, getBrowsePath } from "@/app/[domain]/browse/hooks/utils"; +import { BrowseHighlightRange, getBrowsePath } from "@/app/(app)/browse/hooks/utils"; import { CreateUIMessage, TextUIPart, UIMessagePart } from "ai"; import { Descendant, Editor, Point, Range, Transforms } from "slate"; import { ANSWER_TAG, FILE_REFERENCE_PREFIX, FILE_REFERENCE_REGEX } from "./constants"; diff --git a/packages/web/src/features/git/getFileSourceApi.ts b/packages/web/src/features/git/getFileSourceApi.ts index 03399d995..f5787e86e 100644 --- a/packages/web/src/features/git/getFileSourceApi.ts +++ b/packages/web/src/features/git/getFileSourceApi.ts @@ -1,5 +1,5 @@ import { sew } from "@/middleware/sew"; -import { getBrowsePath } from '@/app/[domain]/browse/hooks/utils'; +import { getBrowsePath } from '@/app/(app)/browse/hooks/utils'; import { getAuditService } from '@/ee/features/audit/factory'; import { parseGitAttributes, resolveLanguageFromGitAttributes } from '@/lib/gitattributes'; import { detectLanguageFromFilename } from '@/lib/languageDetection'; diff --git a/packages/web/src/features/mcp/askCodebase.ts b/packages/web/src/features/mcp/askCodebase.ts index 059eef3c7..229727747 100644 --- a/packages/web/src/features/mcp/askCodebase.ts +++ b/packages/web/src/features/mcp/askCodebase.ts @@ -195,7 +195,7 @@ export const askCodebase = (params: AskCodebaseParams): Promise { - const { domain } = useParams<{ domain: string }>(); - return domain; -} \ No newline at end of file diff --git a/packages/web/src/initialize.ts b/packages/web/src/initialize.ts index 3f1f2bc2a..979105610 100644 --- a/packages/web/src/initialize.ts +++ b/packages/web/src/initialize.ts @@ -2,8 +2,7 @@ import { createGuestUser } from '@/lib/authUtils'; import { prisma } from "@/prisma"; import { OrgRole } from '@sourcebot/db'; import { createLogger, env, hasEntitlement, loadConfig } from "@sourcebot/shared"; -import { getOrgFromDomain } from './data/org'; -import { SINGLE_TENANT_ORG_DOMAIN, SINGLE_TENANT_ORG_ID, SOURCEBOT_GUEST_USER_ID } from './lib/constants'; +import { SINGLE_TENANT_ORG_ID, SOURCEBOT_GUEST_USER_ID } from './lib/constants'; import { ServiceErrorException } from './lib/serviceError'; import { getOrgMetadata, isServiceError } from './lib/utils'; @@ -41,13 +40,13 @@ const init = async () => { const hasAnonymousAccessEntitlement = hasEntitlement("anonymous-access"); if (hasAnonymousAccessEntitlement) { - const res = await createGuestUser(SINGLE_TENANT_ORG_DOMAIN); + const res = await createGuestUser(); if (isServiceError(res)) { throw new ServiceErrorException(res); } } else { // If anonymous access entitlement is not enabled, set the flag to false in the org on init - const org = await getOrgFromDomain(SINGLE_TENANT_ORG_DOMAIN); + const org = await prisma.org.findUnique({ where: { id: SINGLE_TENANT_ORG_ID } }); if (org) { const currentMetadata = getOrgMetadata(org); const mergedMetadata = { @@ -81,7 +80,7 @@ const init = async () => { if (!hasAnonymousAccessEntitlement) { logger.warn(`FORCE_ENABLE_ANONYMOUS_ACCESS env var is set to true but anonymous access entitlement is not available. Setting will be ignored.`); } else { - const org = await getOrgFromDomain(SINGLE_TENANT_ORG_DOMAIN); + const org = await prisma.org.findUnique({ where: { id: SINGLE_TENANT_ORG_ID } }); if (org) { const currentMetadata = getOrgMetadata(org); const mergedMetadata = { @@ -103,7 +102,7 @@ const init = async () => { // Sync member approval setting from env var (only if explicitly set) if (env.REQUIRE_APPROVAL_NEW_MEMBERS !== undefined) { const requireApprovalNewMembers = env.REQUIRE_APPROVAL_NEW_MEMBERS === 'true'; - const org = await getOrgFromDomain(SINGLE_TENANT_ORG_DOMAIN); + const org = await prisma.org.findUnique({ where: { id: SINGLE_TENANT_ORG_ID } }); if (org && org.memberApprovalRequired !== requireApprovalNewMembers) { await prisma.org.update({ where: { id: org.id }, diff --git a/packages/web/src/lib/authUtils.ts b/packages/web/src/lib/authUtils.ts index 6326be64b..46f41d8fd 100644 --- a/packages/web/src/lib/authUtils.ts +++ b/packages/web/src/lib/authUtils.ts @@ -9,7 +9,6 @@ import { createLogger } from "@sourcebot/shared"; import { getAuditService } from "@/ee/features/audit/factory"; import { StatusCodes } from "http-status-codes"; import { ErrorCode } from "./errorCodes"; -import { getOrgFromDomain } from "@/data/org"; const logger = createLogger('web-auth-utils'); const auditService = getAuditService(); @@ -106,7 +105,7 @@ export const onCreateUser = async ({ user }: { user: AuthJsUser }) => { } }); } else if (!defaultOrg.memberApprovalRequired) { - const hasAvailability = await orgHasAvailability(defaultOrg.domain); + const hasAvailability = await orgHasAvailability(); if (!hasAvailability) { logger.warn(`onCreateUser: org ${SINGLE_TENANT_ORG_ID} has reached max capacity. User ${user.id} was not added to the org.`); return; @@ -128,7 +127,7 @@ export const onCreateUser = async ({ user }: { user: AuthJsUser }) => { }; -export const createGuestUser = async (domain: string): Promise => { +export const createGuestUser = async (): Promise => { const hasAnonymousAccessEntitlement = hasEntitlement("anonymous-access"); if (!hasAnonymousAccessEntitlement) { console.error(`Anonymous access isn't supported in your current plan: ${getPlan()}. For support, contact ${SOURCEBOT_SUPPORT_EMAIL}.`); @@ -139,7 +138,9 @@ export const createGuestUser = async (domain: string): Promise => { +export const orgHasAvailability = async (): Promise => { const org = await prisma.org.findUnique({ where: { - domain, + id: SINGLE_TENANT_ORG_ID, }, }); if (!org) { - logger.error(`orgHasAvailability: org not found for domain ${domain}`); + logger.error(`orgHasAvailability: org not found for id ${SINGLE_TENANT_ORG_ID}`); return false; } const members = await prisma.userToOrg.findMany({ @@ -242,7 +243,7 @@ export const addUserToOrganization = async (userId: string, orgId: number): Prom return orgNotFound(); } - const hasAvailability = await orgHasAvailability(org.domain); + const hasAvailability = await orgHasAvailability(); if (!hasAvailability) { return { statusCode: StatusCodes.BAD_REQUEST, diff --git a/packages/web/src/lib/constants.ts b/packages/web/src/lib/constants.ts index 168dd1a7a..d9e6d7052 100644 --- a/packages/web/src/lib/constants.ts +++ b/packages/web/src/lib/constants.ts @@ -7,7 +7,6 @@ export const OPTIONAL_PROVIDERS_LINK_SKIPPED_COOKIE_NAME = 'sb.optional-provider export const SOURCEBOT_GUEST_USER_ID = '1'; export const SOURCEBOT_GUEST_USER_EMAIL = 'guest@sourcebot.dev'; export const SINGLE_TENANT_ORG_ID = 1; -export const SINGLE_TENANT_ORG_DOMAIN = '~'; export const SINGLE_TENANT_ORG_NAME = 'default'; export { SOURCEBOT_SUPPORT_EMAIL } from "@sourcebot/shared/client"; \ No newline at end of file diff --git a/packages/web/src/lib/utils.ts b/packages/web/src/lib/utils.ts index 2f6dedfe6..6a13ca9e7 100644 --- a/packages/web/src/lib/utils.ts +++ b/packages/web/src/lib/utils.ts @@ -16,7 +16,6 @@ import jumpcloudLogo from "@/public/jumpcloud.svg"; import { ServiceError } from "./serviceError"; import { ConnectionType, Org } from "@sourcebot/db"; import { OrgMetadata, orgMetadataSchema } from "@/types"; -import { SINGLE_TENANT_ORG_DOMAIN } from "./constants"; import { CodeHostType } from "@sourcebot/db"; export function cn(...inputs: ClassValue[]) { @@ -571,7 +570,7 @@ export const getRepoImageSrc = (imageUrl: string | undefined, repoId: number): s return imageUrl; } else { // Use the proxied route for self-hosted instances - return `/api/${SINGLE_TENANT_ORG_DOMAIN}/repos/${repoId}/image`; + return `/api/repos/${repoId}/image`; } } catch { // If URL parsing fails, use the original URL diff --git a/packages/web/src/proxy.ts b/packages/web/src/proxy.ts deleted file mode 100644 index f8c71c363..000000000 --- a/packages/web/src/proxy.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { NextResponse } from 'next/server' -import type { NextRequest } from 'next/server' -import { SINGLE_TENANT_ORG_DOMAIN } from '@/lib/constants' - -export async function proxy(request: NextRequest) { - const url = request.nextUrl.clone(); - - if ( - url.pathname.startsWith('/login') || - url.pathname.startsWith('/redeem') || - url.pathname.startsWith('/signup') || - url.pathname.startsWith('/invite') || - url.pathname.startsWith('/onboard') || - url.pathname.startsWith('/oauth') - ) { - return NextResponse.next(); - } - - const pathSegments = url.pathname.split('/').filter(Boolean); - const currentDomain = pathSegments[0]; - - // If we're already on the correct domain path, allow - if (currentDomain === SINGLE_TENANT_ORG_DOMAIN) { - return NextResponse.next(); - } - - url.pathname = `/${SINGLE_TENANT_ORG_DOMAIN}${pathSegments.length > 1 ? '/' + pathSegments.slice(1).join('/') : ''}`; - return NextResponse.redirect(url); -} - -export const config = { - // https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher - matcher: [ - '/((?!api|_next/static|.well-known/oauth-authorization-server|.well-known/oauth-protected-resource|register|ingest|_next/image|favicon.ico|sitemap.xml|robots.txt|manifest.json|logo_192.png|logo_512.png|sb_logo_light_large.png|arrow.png|placeholder_avatar.png|sb_logo_dark_small.png|sb_logo_light_small.png).*)', - ], -} \ No newline at end of file From 53ffb9f1753ee6dd72e6fd10a565879e338687b7 Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Fri, 3 Apr 2026 16:31:48 -0700 Subject: [PATCH 2/9] refactor(web): add authenticatedPage HOC, remove SINGLE_TENANT_ORG_ID from settings pages Introduces `authenticatedPage` HOC in `middleware/authenticatedPage.tsx` for server component pages. It resolves the auth context (user, org, role, prisma) and optionally gates by role, replacing the manual org-lookup-and-role-check boilerplate in settings pages. Migrates all settings pages to use authenticatedPage, removing direct references to SINGLE_TENANT_ORG_ID from within the (app) route group. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/web/src/app/(app)/CLAUDE.md | 25 +++++ packages/web/src/app/(app)/README.md | 52 +++++++++ .../src/app/(app)/settings/access/page.tsx | 33 +----- .../src/app/(app)/settings/analytics/page.tsx | 38 ++----- .../src/app/(app)/settings/apiKeys/layout.tsx | 29 +---- .../src/app/(app)/settings/apiKeys/page.tsx | 15 +-- .../app/(app)/settings/connections/layout.tsx | 37 +------ .../src/app/(app)/settings/license/page.tsx | 29 +---- .../src/app/(app)/settings/members/page.tsx | 38 ++----- .../web/src/middleware/authenticatedPage.tsx | 104 ++++++++++++++++++ 10 files changed, 222 insertions(+), 178 deletions(-) create mode 100644 packages/web/src/app/(app)/CLAUDE.md create mode 100644 packages/web/src/app/(app)/README.md create mode 100644 packages/web/src/middleware/authenticatedPage.tsx diff --git a/packages/web/src/app/(app)/CLAUDE.md b/packages/web/src/app/(app)/CLAUDE.md new file mode 100644 index 000000000..8c4793dc6 --- /dev/null +++ b/packages/web/src/app/(app)/CLAUDE.md @@ -0,0 +1,25 @@ +# (app) Route Group + +## Auth in pages + +Use `authenticatedPage` from `@/middleware/authenticatedPage` for all pages in this directory. Do NOT use `SINGLE_TENANT_ORG_ID` or direct `prisma` imports from `@/prisma` to look up the org — use the `org` and `prisma` provided by the auth context instead. + +```tsx +import { authenticatedPage } from "@/middleware/authenticatedPage"; + +export default authenticatedPage(async ({ org, role, prisma }, props) => { + // ... +}); +``` + +Options: +- `{ minRole: OrgRole.OWNER, redirectTo: '/settings' }` — gate by role +- `{ allowAnonymous: true }` — allow unauthenticated access (user may be undefined) + +## Layout + +The `layout.tsx` in this directory handles authentication, org membership, onboarding, and SSO account linking. Pages do not need to re-check these. See `README.md` for the full guard pipeline. + +## Adding new routes + +New pages automatically inherit the layout's auth/membership guard. Use `authenticatedPage` if the page needs the auth context (org, user, role, prisma) or role-based gating. diff --git a/packages/web/src/app/(app)/README.md b/packages/web/src/app/(app)/README.md new file mode 100644 index 000000000..d281a021f --- /dev/null +++ b/packages/web/src/app/(app)/README.md @@ -0,0 +1,52 @@ +# (app) Route Group + +This is a Next.js [route group](https://nextjs.org/docs/app/building-your-application/routing/route-groups). The parenthesized folder name does not affect the URL structure. Routes here are served at the root (e.g., `/search`, `/chat`, `/settings`). + +## Why this route group exists + +Routes outside `(app)/` (like `/login`, `/signup`, `/invite`, `/onboard`) are accessible without authentication. Routes inside `(app)/` go through the layout's auth and membership guards before rendering. + +## What the layout does + +The `layout.tsx` acts as a gate and app shell. It runs the following checks in order, short-circuiting if any condition is met: + +1. **Org existence** - Looks up the single-tenant org by `SINGLE_TENANT_ORG_ID`. Returns 404 if missing. +2. **Authentication** - If the user is not logged in and anonymous access is not enabled, redirects to `/login` (or renders GCP IAP auth if configured). +3. **Membership** - If the user is logged in but not a member of the org, renders one of: + - `JoinOrganizationCard` if the org does not require approval + - `SubmitJoinRequest` / `PendingApprovalCard` if the org requires approval +4. **Onboarding** - If the org has not completed onboarding, wraps children in `OnboardGuard`. +5. **SSO account linking** - If required SSO providers are not linked, renders `ConnectAccountsCard`. +6. **Mobile splash screen** - Shows an unsupported screen on mobile devices (dismissible via cookie). + +After all guards pass, the layout wraps children with shared UI: `SyntaxGuideProvider`, `PermissionSyncBanner`, `GitHubStarToast`, and `UpgradeToast`. + +## What the layout does NOT do + +- **Role-based access control** - The layout does not check `OWNER` vs `MEMBER`. Pages that require a specific role should use `authenticatedPage` with the `minRole` option. +- **Guarantee a user exists** - When anonymous access is enabled, the user may be undefined. + +## Writing pages in (app) + +Use the `authenticatedPage` HOC from `@/middleware/authenticatedPage`. It resolves the auth context (`user`, `org`, `role`, `prisma`) and handles redirects on auth failure. This avoids manual org lookups with `SINGLE_TENANT_ORG_ID` — pages inside `(app)/` should not reference that constant directly. + +```tsx +import { authenticatedPage } from "@/middleware/authenticatedPage"; + +// Basic authenticated page +export default authenticatedPage(async ({ prisma }) => { + const data = await prisma.repo.findMany(); + return ; +}); + +// Owner-only page +export default authenticatedPage(async ({ org }) => { + return ; +}, { minRole: OrgRole.OWNER, redirectTo: '/settings' }); + +// Page that allows anonymous access +export default authenticatedPage(async ({ user, prisma }) => { + // user may be undefined + return ; +}, { allowAnonymous: true }); +``` diff --git a/packages/web/src/app/(app)/settings/access/page.tsx b/packages/web/src/app/(app)/settings/access/page.tsx index b88a01560..580fb123c 100644 --- a/packages/web/src/app/(app)/settings/access/page.tsx +++ b/packages/web/src/app/(app)/settings/access/page.tsx @@ -1,32 +1,8 @@ -import { SINGLE_TENANT_ORG_ID } from "@/lib/constants"; -import { prisma } from "@/prisma"; import { OrganizationAccessSettings } from "@/app/components/organizationAccessSettings"; -import { isServiceError } from "@/lib/utils"; -import { ServiceErrorException } from "@/lib/serviceError"; -import { getMe } from "@/actions"; +import { authenticatedPage } from "@/middleware/authenticatedPage"; import { OrgRole } from "@sourcebot/db"; -import { redirect } from "next/navigation"; - -export default async function AccessPage() { - const org = await prisma.org.findUnique({ where: { id: SINGLE_TENANT_ORG_ID } }); - if (!org) { - throw new Error("Organization not found"); - } - - const me = await getMe(); - if (isServiceError(me)) { - throw new ServiceErrorException(me); - } - - const userRoleInOrg = me.memberships.find((membership) => membership.id === org.id)?.role; - if (!userRoleInOrg) { - throw new Error("User role not found"); - } - - if (userRoleInOrg !== OrgRole.OWNER) { - redirect('/settings'); - } +export default authenticatedPage(async () => { return (
@@ -46,4 +22,7 @@ export default async function AccessPage() {
) -} \ No newline at end of file +}, { + minRole: OrgRole.OWNER, + redirectTo: '/settings', +}); diff --git a/packages/web/src/app/(app)/settings/analytics/page.tsx b/packages/web/src/app/(app)/settings/analytics/page.tsx index 9883d9718..363e11285 100644 --- a/packages/web/src/app/(app)/settings/analytics/page.tsx +++ b/packages/web/src/app/(app)/settings/analytics/page.tsx @@ -1,39 +1,15 @@ -import { getMe } from "@/actions"; -import { SINGLE_TENANT_ORG_ID } from "@/lib/constants"; -import { prisma } from "@/prisma"; import { AnalyticsContent } from "@/ee/features/analytics/analyticsContent"; import { AnalyticsEntitlementMessage } from "@/ee/features/analytics/analyticsEntitlementMessage"; -import { ServiceErrorException } from "@/lib/serviceError"; -import { isServiceError } from "@/lib/utils"; +import { authenticatedPage } from "@/middleware/authenticatedPage"; import { OrgRole } from "@sourcebot/db"; import { hasEntitlement } from "@sourcebot/shared"; -import { redirect } from "next/navigation"; -export default async function AnalyticsPage() { - const org = await prisma.org.findUnique({ where: { id: SINGLE_TENANT_ORG_ID } }); - if (!org) { - throw new Error("Organization not found"); - } - - const me = await getMe(); - if (isServiceError(me)) { - throw new ServiceErrorException(me); - } +export default authenticatedPage(async () => { + const hasAnalyticsEntitlement = hasEntitlement("analytics"); - const userRoleInOrg = me.memberships.find((membership) => membership.id === org.id)?.role; - if (!userRoleInOrg) { - throw new Error("User role not found"); + if (!hasAnalyticsEntitlement) { + return ; } - if (userRoleInOrg !== OrgRole.OWNER) { - redirect('/settings'); - } - - const hasAnalyticsEntitlement = hasEntitlement("analytics"); - - if (!hasAnalyticsEntitlement) { - return ; - } - - return ; -} + return ; +}, { minRole: OrgRole.OWNER, redirectTo: '/settings' }); diff --git a/packages/web/src/app/(app)/settings/apiKeys/layout.tsx b/packages/web/src/app/(app)/settings/apiKeys/layout.tsx index e6da0a6bf..dfc24fa63 100644 --- a/packages/web/src/app/(app)/settings/apiKeys/layout.tsx +++ b/packages/web/src/app/(app)/settings/apiKeys/layout.tsx @@ -1,31 +1,12 @@ -import { getMe } from "@/actions"; -import { ServiceErrorException } from "@/lib/serviceError"; import { notFound } from "next/navigation"; -import { isServiceError } from "@/lib/utils"; import { OrgRole } from "@sourcebot/db"; -import { SINGLE_TENANT_ORG_ID } from "@/lib/constants"; -import { prisma } from "@/prisma"; import { env } from "@sourcebot/shared"; +import { authenticatedPage } from "@/middleware/authenticatedPage"; -export default async function ApiKeysLayout({ children }: { children: React.ReactNode }) { - const org = await prisma.org.findUnique({ where: { id: SINGLE_TENANT_ORG_ID } }); - if (!org) { - throw new Error("Organization not found"); - } - - const me = await getMe(); - if (isServiceError(me)) { - throw new ServiceErrorException(me); - } - - const userRoleInOrg = me.memberships.find((membership) => membership.id === org.id)?.role; - if (!userRoleInOrg) { - throw new Error("User role not found"); - } - - if (env.DISABLE_API_KEY_USAGE_FOR_NON_OWNER_USERS === 'true' && userRoleInOrg !== OrgRole.OWNER) { +export default authenticatedPage<{ children: React.ReactNode }>(async ({ role }, { children }) => { + if (env.DISABLE_API_KEY_USAGE_FOR_NON_OWNER_USERS === 'true' && role !== OrgRole.OWNER) { return notFound(); } - return children; -} \ No newline at end of file + return <>{children}; +}); diff --git a/packages/web/src/app/(app)/settings/apiKeys/page.tsx b/packages/web/src/app/(app)/settings/apiKeys/page.tsx index 9bba20912..7bd5aea3f 100644 --- a/packages/web/src/app/(app)/settings/apiKeys/page.tsx +++ b/packages/web/src/app/(app)/settings/apiKeys/page.tsx @@ -1,20 +1,13 @@ -import { getMe } from "@/actions"; -import { isServiceError } from "@/lib/utils"; import { env } from "@sourcebot/shared"; import { OrgRole } from "@sourcebot/db"; -import { SINGLE_TENANT_ORG_ID } from "@/lib/constants"; -import { prisma } from "@/prisma"; import { ApiKeysPage } from "./apiKeysPage"; +import { authenticatedPage } from "@/middleware/authenticatedPage"; -export default async function Page() { +export default authenticatedPage(async ({ role }) => { let canCreateApiKey = true; if (env.DISABLE_API_KEY_CREATION_FOR_NON_OWNER_USERS === 'true') { - const [org, me] = await Promise.all([prisma.org.findUnique({ where: { id: SINGLE_TENANT_ORG_ID } }), getMe()]); - if (org && !isServiceError(me)) { - const role = me.memberships.find((m) => m.id === org.id)?.role; - canCreateApiKey = role === OrgRole.OWNER; - } + canCreateApiKey = role === OrgRole.OWNER; } return ; -} +}); diff --git a/packages/web/src/app/(app)/settings/connections/layout.tsx b/packages/web/src/app/(app)/settings/connections/layout.tsx index 3d14c7dd7..ec521d14e 100644 --- a/packages/web/src/app/(app)/settings/connections/layout.tsx +++ b/packages/web/src/app/(app)/settings/connections/layout.tsx @@ -1,35 +1,6 @@ -import { getMe } from "@/actions"; -import { SINGLE_TENANT_ORG_ID } from "@/lib/constants"; -import { prisma } from "@/prisma"; -import { ServiceErrorException } from "@/lib/serviceError"; -import { notFound } from "next/navigation"; -import { isServiceError } from "@/lib/utils"; +import { authenticatedPage } from "@/middleware/authenticatedPage"; import { OrgRole } from "@sourcebot/db"; - -interface ConnectionsLayoutProps { - children: React.ReactNode; -} - -export default async function ConnectionsLayout({ children }: ConnectionsLayoutProps) { - const org = await prisma.org.findUnique({ where: { id: SINGLE_TENANT_ORG_ID } }); - if (!org) { - throw new Error("Organization not found"); - } - - const me = await getMe(); - if (isServiceError(me)) { - throw new ServiceErrorException(me); - } - - const userRoleInOrg = me.memberships.find((membership) => membership.id === org.id)?.role; - if (!userRoleInOrg) { - throw new Error("User role not found"); - } - - if (userRoleInOrg !== OrgRole.OWNER) { - return notFound(); - } - - return children; -} \ No newline at end of file +export default authenticatedPage<{ children: React.ReactNode }>(async (_auth, { children }) => { + return <>{children}; +}, { minRole: OrgRole.OWNER, redirectTo: '/settings' }); diff --git a/packages/web/src/app/(app)/settings/license/page.tsx b/packages/web/src/app/(app)/settings/license/page.tsx index bd4822b8b..fa4e6ba9f 100644 --- a/packages/web/src/app/(app)/settings/license/page.tsx +++ b/packages/web/src/app/(app)/settings/license/page.tsx @@ -1,34 +1,13 @@ import { getLicenseKey, getEntitlements, getPlan, SOURCEBOT_UNLIMITED_SEATS } from "@sourcebot/shared"; import { Button } from "@/components/ui/button"; import { Info, Mail } from "lucide-react"; -import { getMe, getOrgMembers } from "@/actions"; +import { getOrgMembers } from "@/actions"; import { isServiceError } from "@/lib/utils"; import { ServiceErrorException } from "@/lib/serviceError"; -import { SINGLE_TENANT_ORG_ID } from "@/lib/constants"; -import { prisma } from "@/prisma"; +import { authenticatedPage } from "@/middleware/authenticatedPage"; import { OrgRole } from "@sourcebot/db"; -import { redirect } from "next/navigation"; - -export default async function LicensePage() { - const org = await prisma.org.findUnique({ where: { id: SINGLE_TENANT_ORG_ID } }); - if (!org) { - throw new Error("Organization not found"); - } - - const me = await getMe(); - if (isServiceError(me)) { - throw new ServiceErrorException(me); - } - - const userRoleInOrg = me.memberships.find((membership) => membership.id === org.id)?.role; - if (!userRoleInOrg) { - throw new Error("User role not found"); - } - - if (userRoleInOrg !== OrgRole.OWNER) { - redirect('/settings'); - } +export default authenticatedPage(async () => { const licenseKey = getLicenseKey(); const entitlements = getEntitlements(); const plan = getPlan(); @@ -136,4 +115,4 @@ export default async function LicensePage() {
) -} \ No newline at end of file +}, { minRole: OrgRole.OWNER, redirectTo: '/settings' }); diff --git a/packages/web/src/app/(app)/settings/members/page.tsx b/packages/web/src/app/(app)/settings/members/page.tsx index fc1548933..b8e0a834e 100644 --- a/packages/web/src/app/(app)/settings/members/page.tsx +++ b/packages/web/src/app/(app)/settings/members/page.tsx @@ -1,8 +1,6 @@ import { MembersList } from "./components/membersList"; import { getOrgMembers } from "@/actions"; import { isServiceError } from "@/lib/utils"; -import { SINGLE_TENANT_ORG_ID } from "@/lib/constants"; -import { prisma } from "@/prisma"; import { InviteMemberCard } from "./components/inviteMemberCard"; import { Tabs, TabsContent } from "@/components/ui/tabs"; import { TabSwitcher } from "@/components/ui/tab-switcher"; @@ -11,43 +9,29 @@ import { getOrgInvites, getMe, getOrgAccountRequests } from "@/actions"; import { ServiceErrorException } from "@/lib/serviceError"; import { getSeats, hasEntitlement, SOURCEBOT_UNLIMITED_SEATS } from "@sourcebot/shared"; import { RequestsList } from "./components/requestsList"; -import { OrgRole } from "@prisma/client"; -import { redirect } from "next/navigation"; +import { OrgRole } from "@sourcebot/db"; import { NotificationDot } from "../../components/notificationDot"; import { Badge } from "@/components/ui/badge"; +import { authenticatedPage } from "@/middleware/authenticatedPage"; -interface MembersSettingsPageProps { +type MembersSettingsPageProps = { searchParams: Promise<{ tab?: string }> } -export default async function MembersSettingsPage(props: MembersSettingsPageProps) { +export default authenticatedPage(async ({ org, role }, props) => { const searchParams = await props.searchParams; const { tab } = searchParams; - const org = await prisma.org.findUnique({ where: { id: SINGLE_TENANT_ORG_ID } }); - if (!org) { - throw new Error("Organization not found"); - } - const me = await getMe(); if (isServiceError(me)) { throw new ServiceErrorException(me); } - const userRoleInOrg = me.memberships.find((membership) => membership.id === org.id)?.role; - if (!userRoleInOrg) { - throw new Error("User role not found"); - } - - if (userRoleInOrg !== OrgRole.OWNER) { - redirect('/settings'); - } - const members = await getOrgMembers(); if (isServiceError(members)) { throw new ServiceErrorException(members); @@ -89,7 +73,7 @@ export default async function MembersSettingsPage(props: MembersSettingsPageProp
@@ -109,7 +93,7 @@ export default async function MembersSettingsPage(props: MembersSettingsPageProp ), value: "members" }, - ...(userRoleInOrg === OrgRole.OWNER ? [ + ...(role === OrgRole.OWNER ? [ { label: (
@@ -148,25 +132,25 @@ export default async function MembersSettingsPage(props: MembersSettingsPageProp - {userRoleInOrg === OrgRole.OWNER && ( + {role === OrgRole.OWNER && ( <> @@ -174,4 +158,4 @@ export default async function MembersSettingsPage(props: MembersSettingsPageProp
) -} +}, { minRole: OrgRole.OWNER, redirectTo: '/settings' }); diff --git a/packages/web/src/middleware/authenticatedPage.tsx b/packages/web/src/middleware/authenticatedPage.tsx new file mode 100644 index 000000000..1000a726c --- /dev/null +++ b/packages/web/src/middleware/authenticatedPage.tsx @@ -0,0 +1,104 @@ +import { withAuth, withOptionalAuth } from "./withAuth"; +import { isServiceError } from "@/lib/utils"; +import { Org, OrgRole, PrismaClient, UserWithAccounts } from "@sourcebot/db"; +import { redirect } from "next/navigation"; + +type RequiredPageAuthContext = { + user: UserWithAccounts; + org: Org; + role: Exclude; + prisma: PrismaClient; +} + +type OptionalPageAuthContext = { + user?: UserWithAccounts; + org: Org; + role: OrgRole; + prisma: PrismaClient; +} + +type RequiredAuthOptions = { + allowAnonymous?: false; + minRole?: OrgRole; + redirectTo?: string; +} + +type OptionalAuthOptions = { + allowAnonymous: true; + minRole?: never; + redirectTo?: never; +} + +type AuthenticatedPageOptions = RequiredAuthOptions | OptionalAuthOptions; + +type AuthContextFor = + O extends { allowAnonymous: true } ? OptionalPageAuthContext : RequiredPageAuthContext; + +const ROLE_PRECEDENCE: Record = { + [OrgRole.GUEST]: 0, + [OrgRole.MEMBER]: 1, + [OrgRole.OWNER]: 2, +}; + +/** + * HOC that wraps a page component with auth. The page receives the + * auth context as its first argument, and the original page props + * as its second. + * + * The auth context type narrows based on the options: + * - Default: `user` is always defined, `role` excludes GUEST + * - `{ allowAnonymous: true }`: `user` may be undefined, allows anonymous access + * + * @example + * // Required auth (user is always defined) + * export default authenticatedPage(async ({ org, prisma }) => { + * const repos = await prisma.repo.findMany({ ... }); + * return ; + * }); + * + * @example + * // With role gating + * export default authenticatedPage(async ({ org }) => { + * return ; + * }, { minRole: OrgRole.OWNER, redirectTo: '/settings' }); + * + * @example + * // Anonymous access allowed (user may be undefined) + * export default authenticatedPage(async ({ user, prisma }) => { + * return ; + * }, { allowAnonymous: true }); + */ +export function authenticatedPage< + P extends Record = Record, + O extends AuthenticatedPageOptions = RequiredAuthOptions, +>( + fn: (auth: AuthContextFor, props: P) => Promise, + opts?: O, +) { + return async (props: P) => { + if (opts && 'allowAnonymous' in opts && opts.allowAnonymous) { + const result = await withOptionalAuth(async (ctx) => ctx); + + if (isServiceError(result)) { + redirect('/login'); + } + + return fn(result as AuthContextFor, props); + } else { + const result = await withAuth(async (ctx) => ctx); + + if (isServiceError(result)) { + redirect('/login'); + } + + const requiredOpts = opts as RequiredAuthOptions | undefined; + if (requiredOpts?.minRole) { + if (ROLE_PRECEDENCE[result.role] < ROLE_PRECEDENCE[requiredOpts.minRole]) { + redirect(requiredOpts.redirectTo ?? '/'); + } + } + + return fn(result as AuthContextFor, props); + } + }; +} From 727ee7117a2eecb4b65eb0c07dd78a545e80b4fe Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Fri, 3 Apr 2026 16:43:28 -0700 Subject: [PATCH 3/9] rename prisma to __unsafePrisma --- packages/web/src/actions.ts | 16 +++++----- .../app/(app)/askgh/[owner]/[repo]/page.tsx | 4 +-- .../app/(app)/chat/[id]/opengraph-image.tsx | 4 +-- packages/web/src/app/(app)/chat/[id]/page.tsx | 6 ++-- packages/web/src/app/(app)/layout.tsx | 8 ++--- .../api/(server)/ee/oauth/register/route.ts | 4 +-- .../components/organizationAccessSettings.tsx | 4 +-- packages/web/src/app/invite/page.tsx | 6 ++-- packages/web/src/app/login/page.tsx | 4 +-- packages/web/src/app/oauth/authorize/page.tsx | 4 +-- packages/web/src/app/onboard/page.tsx | 6 ++-- packages/web/src/app/redeem/page.tsx | 4 +-- packages/web/src/app/signup/page.tsx | 4 +-- packages/web/src/auth.ts | 14 ++++---- .../web/src/ee/features/audit/auditService.ts | 4 +-- packages/web/src/ee/features/oauth/server.ts | 32 +++++++++---------- packages/web/src/ee/features/sso/sso.ts | 6 ++-- .../src/features/userManagement/actions.ts | 5 ++- packages/web/src/initialize.ts | 20 ++++++------ packages/web/src/lib/authUtils.ts | 24 +++++++------- packages/web/src/middleware/withAuth.ts | 2 +- packages/web/src/prisma.ts | 8 ++--- 22 files changed, 92 insertions(+), 97 deletions(-) diff --git a/packages/web/src/actions.ts b/packages/web/src/actions.ts index 854f3ed72..6df7296cc 100644 --- a/packages/web/src/actions.ts +++ b/packages/web/src/actions.ts @@ -6,7 +6,7 @@ import { addUserToOrganization, orgHasAvailability } from "@/lib/authUtils"; import { ErrorCode } from "@/lib/errorCodes"; import { notFound, orgNotFound, ServiceError } from "@/lib/serviceError"; import { getOrgMetadata, isHttpError, isServiceError } from "@/lib/utils"; -import { prisma } from "@/prisma"; +import { __unsafePrisma } from "@/prisma"; import { render } from "@react-email/components"; import { generateApiKey, getTokenFromConfig } from "@sourcebot/shared"; import { ConnectionSyncJobStatus, OrgRole, Prisma, RepoIndexingJobStatus, RepoIndexingJobType } from "@sourcebot/db"; @@ -928,7 +928,7 @@ export const getOrgAccountRequests = async () => sew(() => })); export const createAccountRequest = async (userId: string) => sew(async () => { - const user = await prisma.user.findUnique({ + const user = await __unsafePrisma.user.findUnique({ where: { id: userId, }, @@ -938,7 +938,7 @@ export const createAccountRequest = async (userId: string) => sew(async () => { return notFound("User not found"); } - const org = await prisma.org.findUnique({ + const org = await __unsafePrisma.org.findUnique({ where: { id: SINGLE_TENANT_ORG_ID, }, @@ -948,7 +948,7 @@ export const createAccountRequest = async (userId: string) => sew(async () => { return notFound("Organization not found"); } - const existingRequest = await prisma.accountRequest.findUnique({ + const existingRequest = await __unsafePrisma.accountRequest.findUnique({ where: { requestedById_orgId: { requestedById: userId, @@ -966,7 +966,7 @@ export const createAccountRequest = async (userId: string) => sew(async () => { } if (!existingRequest) { - await prisma.accountRequest.create({ + await __unsafePrisma.accountRequest.create({ data: { requestedById: userId, orgId: org.id, @@ -979,7 +979,7 @@ export const createAccountRequest = async (userId: string) => sew(async () => { // on user creation (the header isn't set when next-auth calls onCreateUser for some reason) const deploymentUrl = env.AUTH_URL; - const owner = await prisma.user.findFirst({ + const owner = await __unsafePrisma.user.findFirst({ where: { orgs: { some: { @@ -1030,7 +1030,7 @@ export const createAccountRequest = async (userId: string) => sew(async () => { }); export const getMemberApprovalRequired = async (): Promise => sew(async () => { - const org = await prisma.org.findUnique({ + const org = await __unsafePrisma.org.findUnique({ where: { id: SINGLE_TENANT_ORG_ID, }, @@ -1277,7 +1277,7 @@ export const getRepoImage = async (repoId: number): Promise => sew(async () => { - const org = await prisma.org.findUnique({ + const org = await __unsafePrisma.org.findUnique({ where: { id: SINGLE_TENANT_ORG_ID }, }); if (!org) { diff --git a/packages/web/src/app/(app)/askgh/[owner]/[repo]/page.tsx b/packages/web/src/app/(app)/askgh/[owner]/[repo]/page.tsx index 477c4279e..389cbc42f 100644 --- a/packages/web/src/app/(app)/askgh/[owner]/[repo]/page.tsx +++ b/packages/web/src/app/(app)/askgh/[owner]/[repo]/page.tsx @@ -1,7 +1,7 @@ import { addGithubRepo } from "@/features/workerApi/actions"; import { isServiceError } from "@/lib/utils"; import { ServiceErrorException } from "@/lib/serviceError"; -import { prisma } from "@/prisma"; +import { __unsafePrisma } from "@/prisma"; import { getRepoInfo } from "./api"; import { CustomSlateEditor } from "@/features/chat/customSlateEditor"; import { RepoIndexedGuard } from "./components/repoIndexedGuard"; @@ -21,7 +21,7 @@ export default async function GitHubRepoPage(props: PageProps) { const repoId = await (async () => { // 1. Look up repo by owner/repo const displayName = `${owner}/${repo}`; - const existingRepo = await prisma.repo.findFirst({ + const existingRepo = await __unsafePrisma.repo.findFirst({ where: { displayName: displayName, external_codeHostType: 'github', diff --git a/packages/web/src/app/(app)/chat/[id]/opengraph-image.tsx b/packages/web/src/app/(app)/chat/[id]/opengraph-image.tsx index 4ac1ee5e8..3f41d37c8 100644 --- a/packages/web/src/app/(app)/chat/[id]/opengraph-image.tsx +++ b/packages/web/src/app/(app)/chat/[id]/opengraph-image.tsx @@ -1,6 +1,6 @@ import { ImageResponse } from 'next/og'; import { notFound } from 'next/navigation'; -import { prisma } from '@/prisma'; +import { __unsafePrisma } from '@/prisma'; import { ChatVisibility } from '@sourcebot/db'; import { env } from "@sourcebot/shared"; import { minidenticon } from 'minidenticons'; @@ -22,7 +22,7 @@ interface ImageProps { export default async function Image({ params }: ImageProps) { const { id } = await params; - const chat = await prisma.chat.findUnique({ + const chat = await __unsafePrisma.chat.findUnique({ where: { id, }, diff --git a/packages/web/src/app/(app)/chat/[id]/page.tsx b/packages/web/src/app/(app)/chat/[id]/page.tsx index 686444fc5..9add839cc 100644 --- a/packages/web/src/app/(app)/chat/[id]/page.tsx +++ b/packages/web/src/app/(app)/chat/[id]/page.tsx @@ -13,7 +13,7 @@ import { auth } from '@/auth'; import { AnimatedResizableHandle } from '@/components/ui/animatedResizableHandle'; import { ChatSidePanel } from '../components/chatSidePanel'; import { ResizablePanelGroup } from '@/components/ui/resizable'; -import { prisma } from '@/prisma'; +import { __unsafePrisma } from '@/prisma'; import { ChatVisibility } from '@sourcebot/db'; import { Metadata } from 'next'; import { SBChatMessage } from '@/features/chat/types'; @@ -26,10 +26,10 @@ interface PageProps { }>; } -export async function generateMetadata({ params }: PageProps): Promise { +export const generateMetadata = async ({ params }: PageProps): Promise => { const { id } = await params; - const chat = await prisma.chat.findUnique({ + const chat = await __unsafePrisma.chat.findUnique({ where: { id, }, diff --git a/packages/web/src/app/(app)/layout.tsx b/packages/web/src/app/(app)/layout.tsx index 16587524f..62fe4c9b2 100644 --- a/packages/web/src/app/(app)/layout.tsx +++ b/packages/web/src/app/(app)/layout.tsx @@ -1,4 +1,4 @@ -import { prisma } from "@/prisma"; +import { __unsafePrisma } from "@/prisma"; import { auth } from "@/auth"; import { isServiceError } from "@/lib/utils"; import { SINGLE_TENANT_ORG_ID } from "@/lib/constants"; @@ -35,7 +35,7 @@ export default async function Layout(props: LayoutProps) { children } = props; - const org = await prisma.org.findUnique({ + const org = await __unsafePrisma.org.findUnique({ where: { id: SINGLE_TENANT_ORG_ID }, }); @@ -59,7 +59,7 @@ export default async function Layout(props: LayoutProps) { // If the user is authenticated, we must check if they're a member of the org if (session) { - const membership = await prisma.userToOrg.findUnique({ + const membership = await __unsafePrisma.userToOrg.findUnique({ where: { orgId_userId: { orgId: org.id, @@ -85,7 +85,7 @@ export default async function Layout(props: LayoutProps) {
) } else { - const hasPendingApproval = await prisma.accountRequest.findFirst({ + const hasPendingApproval = await __unsafePrisma.accountRequest.findFirst({ where: { orgId: org.id, requestedById: session.user.id diff --git a/packages/web/src/app/api/(server)/ee/oauth/register/route.ts b/packages/web/src/app/api/(server)/ee/oauth/register/route.ts index 932ef5318..a8d223f9e 100644 --- a/packages/web/src/app/api/(server)/ee/oauth/register/route.ts +++ b/packages/web/src/app/api/(server)/ee/oauth/register/route.ts @@ -1,6 +1,6 @@ import { apiHandler } from '@/lib/apiHandler'; import { requestBodySchemaValidationError, serviceErrorResponse } from '@/lib/serviceError'; -import { prisma } from '@/prisma'; +import { __unsafePrisma } from '@/prisma'; import { hasEntitlement } from '@sourcebot/shared'; import { NextRequest } from 'next/server'; import { z } from 'zod'; @@ -39,7 +39,7 @@ export const POST = apiHandler(async (request: NextRequest) => { ); } - const client = await prisma.oAuthClient.create({ + const client = await __unsafePrisma.oAuthClient.create({ data: { name: client_name, logoUri: logo_uri ?? null, diff --git a/packages/web/src/app/components/organizationAccessSettings.tsx b/packages/web/src/app/components/organizationAccessSettings.tsx index 906c13aa2..d6846e933 100644 --- a/packages/web/src/app/components/organizationAccessSettings.tsx +++ b/packages/web/src/app/components/organizationAccessSettings.tsx @@ -3,11 +3,11 @@ import { AnonymousAccessToggle } from "./anonymousAccessToggle" import { OrganizationAccessSettingsWrapper } from "./organizationAccessSettingsWrapper" import { getOrgMetadata } from "@/lib/utils" import { SINGLE_TENANT_ORG_ID } from "@/lib/constants" -import { prisma } from "@/prisma" +import { __unsafePrisma } from "@/prisma" import { hasEntitlement, env } from "@sourcebot/shared" export async function OrganizationAccessSettings() { - const org = await prisma.org.findUnique({ where: { id: SINGLE_TENANT_ORG_ID } }); + const org = await __unsafePrisma.org.findUnique({ where: { id: SINGLE_TENANT_ORG_ID } }); if (!org) { return
Error loading organization
} diff --git a/packages/web/src/app/invite/page.tsx b/packages/web/src/app/invite/page.tsx index e44cc01e1..4081ccc88 100644 --- a/packages/web/src/app/invite/page.tsx +++ b/packages/web/src/app/invite/page.tsx @@ -1,5 +1,5 @@ import { auth } from "@/auth"; -import { prisma } from "@/prisma"; +import { __unsafePrisma } from "@/prisma"; import { SINGLE_TENANT_ORG_ID } from "@/lib/constants"; import { notFound, redirect } from "next/navigation"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; @@ -17,7 +17,7 @@ interface InvitePageProps { export default async function InvitePage(props: InvitePageProps) { const searchParams = await props.searchParams; - const org = await prisma.org.findUnique({ where: { id: SINGLE_TENANT_ORG_ID } }); + const org = await __unsafePrisma.org.findUnique({ where: { id: SINGLE_TENANT_ORG_ID } }); if (!org || !org.isOnboarded) { return redirect("/onboard"); } @@ -33,7 +33,7 @@ export default async function InvitePage(props: InvitePageProps) { return ; } - const membership = await prisma.userToOrg.findUnique({ + const membership = await __unsafePrisma.userToOrg.findUnique({ where: { orgId_userId: { orgId: org.id, diff --git a/packages/web/src/app/login/page.tsx b/packages/web/src/app/login/page.tsx index 7d163d370..c827e6f2d 100644 --- a/packages/web/src/app/login/page.tsx +++ b/packages/web/src/app/login/page.tsx @@ -4,7 +4,7 @@ import { redirect } from "next/navigation"; import { Footer } from "@/app/components/footer"; import { getIdentityProviderMetadata } from "@/lib/identityProviders"; import { SINGLE_TENANT_ORG_ID } from "@/lib/constants"; -import { prisma } from "@/prisma"; +import { __unsafePrisma } from "@/prisma"; import { getAnonymousAccessStatus } from "@/actions"; import { isServiceError } from "@/lib/utils"; import { env } from "@sourcebot/shared"; @@ -23,7 +23,7 @@ export default async function Login(props: LoginProps) { return redirect("/"); } - const org = await prisma.org.findUnique({ where: { id: SINGLE_TENANT_ORG_ID } }); + const org = await __unsafePrisma.org.findUnique({ where: { id: SINGLE_TENANT_ORG_ID } }); if (!org || !org.isOnboarded) { return redirect("/onboard"); } diff --git a/packages/web/src/app/oauth/authorize/page.tsx b/packages/web/src/app/oauth/authorize/page.tsx index bfbb5ac5a..cc816fc3f 100644 --- a/packages/web/src/app/oauth/authorize/page.tsx +++ b/packages/web/src/app/oauth/authorize/page.tsx @@ -1,7 +1,7 @@ import { auth } from '@/auth'; import { LogoutEscapeHatch } from '@/app/components/logoutEscapeHatch'; import { ConsentScreen } from './components/consentScreen'; -import { prisma } from '@/prisma'; +import { __unsafePrisma } from '@/prisma'; import { hasEntitlement } from '@sourcebot/shared'; import { redirect } from 'next/navigation'; @@ -47,7 +47,7 @@ export default async function AuthorizePage({ searchParams }: AuthorizePageProps return ; } - const client = await prisma.oAuthClient.findUnique({ where: { id: client_id } }); + const client = await __unsafePrisma.oAuthClient.findUnique({ where: { id: client_id } }); if (!client) { return ; diff --git a/packages/web/src/app/onboard/page.tsx b/packages/web/src/app/onboard/page.tsx index 0c5c99623..b780dfbbb 100644 --- a/packages/web/src/app/onboard/page.tsx +++ b/packages/web/src/app/onboard/page.tsx @@ -10,7 +10,7 @@ import { getIdentityProviderMetadata } from "@/lib/identityProviders"; import { OrganizationAccessSettings } from "@/app/components/organizationAccessSettings"; import { CompleteOnboardingButton } from "./components/completeOnboardingButton"; import { SINGLE_TENANT_ORG_ID } from "@/lib/constants"; -import { prisma } from "@/prisma"; +import { __unsafePrisma } from "@/prisma"; import { OrgRole } from "@sourcebot/db"; import { LogoutEscapeHatch } from "@/app/components/logoutEscapeHatch"; import { redirect } from "next/navigation"; @@ -41,7 +41,7 @@ interface ResourceCard { export default async function Onboarding(props: OnboardingProps) { const searchParams = await props.searchParams; const providers = getIdentityProviderMetadata(); - const org = await prisma.org.findUnique({ where: { id: SINGLE_TENANT_ORG_ID } }); + const org = await __unsafePrisma.org.findUnique({ where: { id: SINGLE_TENANT_ORG_ID } }); const session = await auth(); if (!org) { @@ -55,7 +55,7 @@ export default async function Onboarding(props: OnboardingProps) { // Check if user is authenticated but not the owner if (session?.user) { if (org) { - const membership = await prisma.userToOrg.findUnique({ + const membership = await __unsafePrisma.userToOrg.findUnique({ where: { orgId_userId: { orgId: org.id, diff --git a/packages/web/src/app/redeem/page.tsx b/packages/web/src/app/redeem/page.tsx index 31f91562d..aae1ae392 100644 --- a/packages/web/src/app/redeem/page.tsx +++ b/packages/web/src/app/redeem/page.tsx @@ -6,7 +6,7 @@ import { AcceptInviteCard } from './components/acceptInviteCard'; import { LogoutEscapeHatch } from '../components/logoutEscapeHatch'; import { InviteNotFoundCard } from './components/inviteNotFoundCard'; import { SINGLE_TENANT_ORG_ID } from '@/lib/constants'; -import { prisma } from '@/prisma'; +import { __unsafePrisma } from '@/prisma'; interface RedeemPageProps { searchParams: Promise<{ @@ -16,7 +16,7 @@ interface RedeemPageProps { export default async function RedeemPage(props: RedeemPageProps) { const searchParams = await props.searchParams; - const org = await prisma.org.findUnique({ where: { id: SINGLE_TENANT_ORG_ID } }); + const org = await __unsafePrisma.org.findUnique({ where: { id: SINGLE_TENANT_ORG_ID } }); if (!org || !org.isOnboarded) { return redirect("/onboard"); } diff --git a/packages/web/src/app/signup/page.tsx b/packages/web/src/app/signup/page.tsx index 5a3c28cd0..a4a78ff1c 100644 --- a/packages/web/src/app/signup/page.tsx +++ b/packages/web/src/app/signup/page.tsx @@ -5,7 +5,7 @@ import { Footer } from "@/app/components/footer"; import { getIdentityProviderMetadata } from "@/lib/identityProviders"; import { createLogger, env } from "@sourcebot/shared"; import { SINGLE_TENANT_ORG_ID } from "@/lib/constants"; -import { prisma } from "@/prisma"; +import { __unsafePrisma } from "@/prisma"; import { getAnonymousAccessStatus } from "@/actions"; import { isServiceError } from "@/lib/utils"; @@ -26,7 +26,7 @@ export default async function Signup(props: LoginProps) { return redirect("/"); } - const org = await prisma.org.findUnique({ where: { id: SINGLE_TENANT_ORG_ID } }); + const org = await __unsafePrisma.org.findUnique({ where: { id: SINGLE_TENANT_ORG_ID } }); if (!org || !org.isOnboarded) { return redirect("/onboard"); } diff --git a/packages/web/src/auth.ts b/packages/web/src/auth.ts index cf2475284..001d11ef0 100644 --- a/packages/web/src/auth.ts +++ b/packages/web/src/auth.ts @@ -2,7 +2,7 @@ import 'next-auth/jwt'; import NextAuth, { DefaultSession, User as AuthJsUser } from "next-auth" import Credentials from "next-auth/providers/credentials" import EmailProvider from "next-auth/providers/nodemailer"; -import { prisma } from "@/prisma"; +import { __unsafePrisma } from "@/prisma"; import { env, getSMTPConnectionURL } from "@sourcebot/shared"; import { User } from '@sourcebot/db'; import 'next-auth/jwt'; @@ -96,14 +96,14 @@ export const getProviders = () => { } const { email, password } = body.data; - const user = await prisma.user.findUnique({ + const user = await __unsafePrisma.user.findUnique({ where: { email } }); // The user doesn't exist, so create a new one. if (!user) { const hashedPassword = bcrypt.hashSync(password, 10); - const newUser = await prisma.user.create({ + const newUser = await __unsafePrisma.user.create({ data: { email, hashedPassword, @@ -145,7 +145,7 @@ export const getProviders = () => { export const { handlers, signIn, signOut, auth } = NextAuth({ secret: env.AUTH_SECRET, - adapter: EncryptedPrismaAdapter(prisma), + adapter: EncryptedPrismaAdapter(__unsafePrisma), session: { strategy: "jwt", }, @@ -165,7 +165,7 @@ export const { handlers, signIn, signOut, auth } = NextAuth({ ) { const issuerUrl = await getIssuerUrlForAccount(account); - await prisma.account.update({ + await __unsafePrisma.account.update({ where: { provider_providerAccountId: { provider: account.provider, @@ -234,7 +234,7 @@ export const { handlers, signIn, signOut, auth } = NextAuth({ // // @see https://github.com/sourcebot-dev/sourcebot/pull/993 if (token.userId) { - const accountsWithoutIssuerUrl = await prisma.account.findMany({ + const accountsWithoutIssuerUrl = await __unsafePrisma.account.findMany({ where: { userId: token.userId, issuerUrl: null, @@ -244,7 +244,7 @@ export const { handlers, signIn, signOut, auth } = NextAuth({ for (const account of accountsWithoutIssuerUrl) { const issuerUrl = await getIssuerUrlForAccount(account); if (issuerUrl) { - await prisma.account.update({ + await __unsafePrisma.account.update({ where: { id: account.id, }, diff --git a/packages/web/src/ee/features/audit/auditService.ts b/packages/web/src/ee/features/audit/auditService.ts index 2925ea51a..9cb264108 100644 --- a/packages/web/src/ee/features/audit/auditService.ts +++ b/packages/web/src/ee/features/audit/auditService.ts @@ -1,5 +1,5 @@ import { IAuditService, AuditEvent } from '@/ee/features/audit/types'; -import { prisma } from '@/prisma'; +import { __unsafePrisma } from '@/prisma'; import { Audit } from '@prisma/client'; import { createLogger, SOURCEBOT_VERSION } from '@sourcebot/shared'; @@ -10,7 +10,7 @@ export class AuditService implements IAuditService { const sourcebotVersion = SOURCEBOT_VERSION; try { - const audit = await prisma.audit.create({ + const audit = await __unsafePrisma.audit.create({ data: { action: event.action, actorId: event.actor.id, diff --git a/packages/web/src/ee/features/oauth/server.ts b/packages/web/src/ee/features/oauth/server.ts index 0a7d6fab6..059e8fb0d 100644 --- a/packages/web/src/ee/features/oauth/server.ts +++ b/packages/web/src/ee/features/oauth/server.ts @@ -1,6 +1,6 @@ import 'server-only'; -import { prisma } from '@/prisma'; +import { __unsafePrisma } from '@/prisma'; import { Prisma } from '@prisma/client'; import { generateOAuthRefreshToken, @@ -35,7 +35,7 @@ export async function generateAndStoreAuthCode({ const rawCode = crypto.randomBytes(32).toString('hex'); const codeHash = hashSecret(rawCode); - await prisma.oAuthAuthorizationCode.create({ + await __unsafePrisma.oAuthAuthorizationCode.create({ data: { codeHash, clientId, @@ -67,7 +67,7 @@ export async function verifyAndExchangeCode({ }): Promise<{ token: string; refreshToken: string; expiresIn: number } | { error: string; errorDescription: string }> { const codeHash = hashSecret(rawCode); - const authCode = await prisma.oAuthAuthorizationCode.findUnique({ + const authCode = await __unsafePrisma.oAuthAuthorizationCode.findUnique({ where: { codeHash }, }); @@ -76,7 +76,7 @@ export async function verifyAndExchangeCode({ } if (authCode.expiresAt < new Date()) { - await prisma.oAuthAuthorizationCode.delete({ where: { codeHash } }); + await __unsafePrisma.oAuthAuthorizationCode.delete({ where: { codeHash } }); return { error: 'invalid_grant', errorDescription: 'Authorization code has expired.' }; } @@ -106,7 +106,7 @@ export async function verifyAndExchangeCode({ // Single-use: delete the auth code before issuing token. // Handle concurrent consume attempts gracefully. try { - await prisma.oAuthAuthorizationCode.delete({ where: { codeHash } }); + await __unsafePrisma.oAuthAuthorizationCode.delete({ where: { codeHash } }); } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2025') { return { error: 'invalid_grant', errorDescription: 'Authorization code has already been used.' }; @@ -117,8 +117,8 @@ export async function verifyAndExchangeCode({ const { token, hash } = generateOAuthToken(); const { token: refreshToken, hash: refreshHash } = generateOAuthRefreshToken(); - await prisma.$transaction([ - prisma.oAuthToken.create({ + await __unsafePrisma.$transaction([ + __unsafePrisma.oAuthToken.create({ data: { hash, clientId, @@ -127,7 +127,7 @@ export async function verifyAndExchangeCode({ expiresAt: new Date(Date.now() + ACCESS_TOKEN_TTL_MS), }, }), - prisma.oAuthRefreshToken.create({ + __unsafePrisma.oAuthRefreshToken.create({ data: { hash: refreshHash, clientId, @@ -159,7 +159,7 @@ export async function verifyAndRotateRefreshToken({ const hash = hashSecret(rawRefreshToken.slice(OAUTH_REFRESH_TOKEN_PREFIX.length)); - const existing = await prisma.oAuthRefreshToken.findUnique({ where: { hash } }); + const existing = await __unsafePrisma.oAuthRefreshToken.findUnique({ where: { hash } }); if (!existing) { return { error: 'invalid_grant', errorDescription: 'Refresh token is invalid or has already been used.' }; @@ -170,7 +170,7 @@ export async function verifyAndRotateRefreshToken({ } if (existing.expiresAt < new Date()) { - await prisma.oAuthRefreshToken.delete({ where: { hash } }); + await __unsafePrisma.oAuthRefreshToken.delete({ where: { hash } }); return { error: 'invalid_grant', errorDescription: 'Refresh token has expired.' }; } @@ -181,9 +181,9 @@ export async function verifyAndRotateRefreshToken({ const { token, hash: newTokenHash } = generateOAuthToken(); const { token: refreshToken, hash: newRefreshHash } = generateOAuthRefreshToken(); - await prisma.$transaction([ - prisma.oAuthRefreshToken.delete({ where: { hash } }), - prisma.oAuthToken.create({ + await __unsafePrisma.$transaction([ + __unsafePrisma.oAuthRefreshToken.delete({ where: { hash } }), + __unsafePrisma.oAuthToken.create({ data: { hash: newTokenHash, clientId, @@ -192,7 +192,7 @@ export async function verifyAndRotateRefreshToken({ expiresAt: new Date(Date.now() + ACCESS_TOKEN_TTL_MS), }, }), - prisma.oAuthRefreshToken.create({ + __unsafePrisma.oAuthRefreshToken.create({ data: { hash: newRefreshHash, clientId, @@ -212,10 +212,10 @@ export async function revokeToken(rawToken: string): Promise { if (rawToken.startsWith(OAUTH_ACCESS_TOKEN_PREFIX)) { const secret = rawToken.slice(OAUTH_ACCESS_TOKEN_PREFIX.length); const hash = hashSecret(secret); - await prisma.oAuthToken.deleteMany({ where: { hash } }); + await __unsafePrisma.oAuthToken.deleteMany({ where: { hash } }); } else if (rawToken.startsWith(OAUTH_REFRESH_TOKEN_PREFIX)) { const secret = rawToken.slice(OAUTH_REFRESH_TOKEN_PREFIX.length); const hash = hashSecret(secret); - await prisma.oAuthRefreshToken.deleteMany({ where: { hash } }); + await __unsafePrisma.oAuthRefreshToken.deleteMany({ where: { hash } }); } } diff --git a/packages/web/src/ee/features/sso/sso.ts b/packages/web/src/ee/features/sso/sso.ts index 365428f3f..2625709f1 100644 --- a/packages/web/src/ee/features/sso/sso.ts +++ b/packages/web/src/ee/features/sso/sso.ts @@ -1,6 +1,6 @@ import type { IdentityProvider } from "@/auth"; import { onCreateUser } from "@/lib/authUtils"; -import { prisma } from "@/prisma"; +import { __unsafePrisma } from "@/prisma"; import { AuthentikIdentityProviderConfig, BitbucketCloudIdentityProviderConfig, BitbucketServerIdentityProviderConfig, GCPIAPIdentityProviderConfig, GitHubIdentityProviderConfig, GitLabIdentityProviderConfig, GoogleIdentityProviderConfig, JumpCloudIdentityProviderConfig, KeycloakIdentityProviderConfig, MicrosoftEntraIDIdentityProviderConfig, OktaIdentityProviderConfig } from "@sourcebot/schemas/v3/index.type"; import type { IdentityProviderType } from "@sourcebot/shared"; import { createLogger, env, getTokenFromConfig, hasEntitlement, loadConfig } from "@sourcebot/shared"; @@ -485,12 +485,12 @@ const createGCPIAPProvider = (audience: string): Provider => { return null; } - const existingUser = await prisma.user.findUnique({ + const existingUser = await __unsafePrisma.user.findUnique({ where: { email } }); if (!existingUser) { - const newUser = await prisma.user.create({ + const newUser = await __unsafePrisma.user.create({ data: { email, name, diff --git a/packages/web/src/features/userManagement/actions.ts b/packages/web/src/features/userManagement/actions.ts index f19546ad9..1b82b7cd0 100644 --- a/packages/web/src/features/userManagement/actions.ts +++ b/packages/web/src/features/userManagement/actions.ts @@ -3,14 +3,13 @@ import { sew } from "@/middleware/sew"; import { ErrorCode } from "@/lib/errorCodes"; import { notFound, ServiceError } from "@/lib/serviceError"; -import { prisma } from "@/prisma"; import { withAuth } from "@/middleware/withAuth"; import { withMinimumOrgRole } from "@/middleware/withMinimumOrgRole"; import { OrgRole, Prisma } from "@sourcebot/db"; import { StatusCodes } from "http-status-codes"; export const removeMemberFromOrg = async (memberId: string): Promise<{ success: boolean } | ServiceError> => sew(() => - withAuth(async ({ org, role }) => + withAuth(async ({ org, role, prisma }) => withMinimumOrgRole(role, OrgRole.OWNER, async () => { const guardError = await prisma.$transaction(async (tx) => { const targetMember = await tx.userToOrg.findUnique({ @@ -64,7 +63,7 @@ export const removeMemberFromOrg = async (memberId: string): Promise<{ success: ); export const leaveOrg = async (): Promise<{ success: boolean } | ServiceError> => sew(() => - withAuth(async ({ user, org, role }) => { + withAuth(async ({ user, org, role, prisma }) => { const guardError = await prisma.$transaction(async (tx) => { if (role === OrgRole.OWNER) { const ownerCount = await tx.userToOrg.count({ diff --git a/packages/web/src/initialize.ts b/packages/web/src/initialize.ts index 979105610..0d872c58e 100644 --- a/packages/web/src/initialize.ts +++ b/packages/web/src/initialize.ts @@ -1,5 +1,5 @@ import { createGuestUser } from '@/lib/authUtils'; -import { prisma } from "@/prisma"; +import { __unsafePrisma } from "@/prisma"; import { OrgRole } from '@sourcebot/db'; import { createLogger, env, hasEntitlement, loadConfig } from "@sourcebot/shared"; import { SINGLE_TENANT_ORG_ID, SOURCEBOT_GUEST_USER_ID } from './lib/constants'; @@ -10,7 +10,7 @@ const logger = createLogger('web-initialize'); const pruneOldGuestUser = async () => { // The old guest user doesn't have the GUEST role - const guestUser = await prisma.userToOrg.findUnique({ + const guestUser = await __unsafePrisma.userToOrg.findUnique({ where: { orgId_userId: { orgId: SINGLE_TENANT_ORG_ID, @@ -23,7 +23,7 @@ const pruneOldGuestUser = async () => { }); if (guestUser) { - await prisma.user.delete({ + await __unsafePrisma.user.delete({ where: { id: guestUser.userId, }, @@ -46,14 +46,14 @@ const init = async () => { } } else { // If anonymous access entitlement is not enabled, set the flag to false in the org on init - const org = await prisma.org.findUnique({ where: { id: SINGLE_TENANT_ORG_ID } }); + const org = await __unsafePrisma.org.findUnique({ where: { id: SINGLE_TENANT_ORG_ID } }); if (org) { const currentMetadata = getOrgMetadata(org); const mergedMetadata = { ...(currentMetadata ?? {}), anonymousAccessEnabled: false, }; - await prisma.org.update({ + await __unsafePrisma.org.update({ where: { id: org.id }, data: { metadata: mergedMetadata }, }); @@ -65,7 +65,7 @@ const init = async () => { // the entitlement, synced search contexts, and then no longer had the entitlement const hasSearchContextEntitlement = hasEntitlement("search-contexts") if (!hasSearchContextEntitlement) { - await prisma.searchContext.deleteMany({ + await __unsafePrisma.searchContext.deleteMany({ where: { orgId: SINGLE_TENANT_ORG_ID, }, @@ -80,7 +80,7 @@ const init = async () => { if (!hasAnonymousAccessEntitlement) { logger.warn(`FORCE_ENABLE_ANONYMOUS_ACCESS env var is set to true but anonymous access entitlement is not available. Setting will be ignored.`); } else { - const org = await prisma.org.findUnique({ where: { id: SINGLE_TENANT_ORG_ID } }); + const org = await __unsafePrisma.org.findUnique({ where: { id: SINGLE_TENANT_ORG_ID } }); if (org) { const currentMetadata = getOrgMetadata(org); const mergedMetadata = { @@ -88,7 +88,7 @@ const init = async () => { anonymousAccessEnabled: true, }; - await prisma.org.update({ + await __unsafePrisma.org.update({ where: { id: org.id }, data: { metadata: mergedMetadata, @@ -102,9 +102,9 @@ const init = async () => { // Sync member approval setting from env var (only if explicitly set) if (env.REQUIRE_APPROVAL_NEW_MEMBERS !== undefined) { const requireApprovalNewMembers = env.REQUIRE_APPROVAL_NEW_MEMBERS === 'true'; - const org = await prisma.org.findUnique({ where: { id: SINGLE_TENANT_ORG_ID } }); + const org = await __unsafePrisma.org.findUnique({ where: { id: SINGLE_TENANT_ORG_ID } }); if (org && org.memberApprovalRequired !== requireApprovalNewMembers) { - await prisma.org.update({ + await __unsafePrisma.org.update({ where: { id: org.id }, data: { memberApprovalRequired: requireApprovalNewMembers }, }); diff --git a/packages/web/src/lib/authUtils.ts b/packages/web/src/lib/authUtils.ts index 46f41d8fd..d4bc3eefb 100644 --- a/packages/web/src/lib/authUtils.ts +++ b/packages/web/src/lib/authUtils.ts @@ -1,5 +1,5 @@ import type { User as AuthJsUser } from "next-auth"; -import { prisma } from "@/prisma"; +import { __unsafePrisma } from "@/prisma"; import { OrgRole } from "@sourcebot/db"; import { SINGLE_TENANT_ORG_ID, SOURCEBOT_GUEST_USER_EMAIL, SOURCEBOT_GUEST_USER_ID, SOURCEBOT_SUPPORT_EMAIL } from "@/lib/constants"; import { getPlan, getSeats, hasEntitlement, SOURCEBOT_UNLIMITED_SEATS } from "@sourcebot/shared"; @@ -34,7 +34,7 @@ export const onCreateUser = async ({ user }: { user: AuthJsUser }) => { throw new Error("User ID is undefined on user creation"); } - const defaultOrg = await prisma.org.findUnique({ + const defaultOrg = await __unsafePrisma.org.findUnique({ where: { id: SINGLE_TENANT_ORG_ID, }, @@ -72,7 +72,7 @@ export const onCreateUser = async ({ user }: { user: AuthJsUser }) => { // If this is the first user to sign up, we make them the owner of the default org. const isFirstUser = defaultOrg.members.length === 0; if (isFirstUser) { - await prisma.$transaction(async (tx) => { + await __unsafePrisma.$transaction(async (tx) => { await tx.org.update({ where: { id: SINGLE_TENANT_ORG_ID, @@ -111,7 +111,7 @@ export const onCreateUser = async ({ user }: { user: AuthJsUser }) => { return; } - await prisma.userToOrg.create({ + await __unsafePrisma.userToOrg.create({ data: { userId: user.id, orgId: SINGLE_TENANT_ORG_ID, @@ -138,7 +138,7 @@ export const createGuestUser = async (): Promise => { } satisfies ServiceError; } - const org = await prisma.org.findUnique({ + const org = await __unsafePrisma.org.findUnique({ where: { id: SINGLE_TENANT_ORG_ID }, }); if (!org) { @@ -149,7 +149,7 @@ export const createGuestUser = async (): Promise => { } satisfies ServiceError; } - const user = await prisma.user.upsert({ + const user = await __unsafePrisma.user.upsert({ where: { id: SOURCEBOT_GUEST_USER_ID, }, @@ -161,7 +161,7 @@ export const createGuestUser = async (): Promise => { }, }); - await prisma.org.update({ + await __unsafePrisma.org.update({ where: { id: org.id, }, @@ -190,7 +190,7 @@ export const createGuestUser = async (): Promise => { }; export const orgHasAvailability = async (): Promise => { - const org = await prisma.org.findUnique({ + const org = await __unsafePrisma.org.findUnique({ where: { id: SINGLE_TENANT_ORG_ID, }, @@ -200,7 +200,7 @@ export const orgHasAvailability = async (): Promise => { logger.error(`orgHasAvailability: org not found for id ${SINGLE_TENANT_ORG_ID}`); return false; } - const members = await prisma.userToOrg.findMany({ + const members = await __unsafePrisma.userToOrg.findMany({ where: { orgId: org.id, role: { @@ -221,7 +221,7 @@ export const orgHasAvailability = async (): Promise => { } export const addUserToOrganization = async (userId: string, orgId: number): Promise<{ success: boolean } | ServiceError> => { - const user = await prisma.user.findUnique({ + const user = await __unsafePrisma.user.findUnique({ where: { id: userId, }, @@ -232,7 +232,7 @@ export const addUserToOrganization = async (userId: string, orgId: number): Prom return userNotFound(); } - const org = await prisma.org.findUnique({ + const org = await __unsafePrisma.org.findUnique({ where: { id: orgId, }, @@ -252,7 +252,7 @@ export const addUserToOrganization = async (userId: string, orgId: number): Prom } satisfies ServiceError; } - const res = await prisma.$transaction(async (tx) => { + const res = await __unsafePrisma.$transaction(async (tx) => { await tx.userToOrg.create({ data: { userId: user.id, diff --git a/packages/web/src/middleware/withAuth.ts b/packages/web/src/middleware/withAuth.ts index 33e5be772..7dbbd0bd0 100644 --- a/packages/web/src/middleware/withAuth.ts +++ b/packages/web/src/middleware/withAuth.ts @@ -1,4 +1,4 @@ -import { prisma as __unsafePrisma, userScopedPrismaClientExtension } from "@/prisma"; +import { __unsafePrisma, userScopedPrismaClientExtension } from "@/prisma"; import { hashSecret, OAUTH_ACCESS_TOKEN_PREFIX, API_KEY_PREFIX, LEGACY_API_KEY_PREFIX, env } from "@sourcebot/shared"; import { ApiKey, Org, OrgRole, PrismaClient, UserWithAccounts } from "@sourcebot/db"; import { headers } from "next/headers"; diff --git a/packages/web/src/prisma.ts b/packages/web/src/prisma.ts index e7767a5d9..fa8ea1046 100644 --- a/packages/web/src/prisma.ts +++ b/packages/web/src/prisma.ts @@ -11,11 +11,7 @@ const dbConnectionString = getDBConnectionString(); // @NOTE: In almost all cases, the userScopedPrismaClientExtension should be used // (since actions & queries are scoped to a particular user). There are some exceptions // (e.g., in initialize.ts). -// -// @todo: we can mark this as `__unsafePrisma` in the future once we've migrated -// all of the actions & queries to use the userScopedPrismaClientExtension to avoid -// accidental misuse. -export const prisma = globalForPrisma.prisma || new PrismaClient({ +export const __unsafePrisma = globalForPrisma.prisma || new PrismaClient({ // @note: this code is evaluated at build time, and will throw exceptions if these env vars are not set. // Here we explicitly check if the DATABASE_URL or the individual database variables are set, and only ...(dbConnectionString !== undefined ? { @@ -26,7 +22,7 @@ export const prisma = globalForPrisma.prisma || new PrismaClient({ } } : {}), }) -if (env.NODE_ENV !== "production") globalForPrisma.prisma = prisma +if (env.NODE_ENV !== "production") globalForPrisma.prisma = __unsafePrisma /** * Creates a prisma client extension that scopes queries to striclty information From cf2a4024c2ca14e114ad2290e274831a7a6fa1e3 Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Fri, 3 Apr 2026 17:06:57 -0700 Subject: [PATCH 4/9] wip --- packages/web/src/actions.ts | 112 +--------- .../app/components/joinOrganizationButton.tsx | 3 +- packages/web/src/app/invite/actions.ts | 202 +++++++++++++++--- .../redeem/components/acceptInviteCard.tsx | 2 +- packages/web/src/app/redeem/page.tsx | 2 +- packages/web/src/middleware/withAuth.ts | 21 -- 6 files changed, 172 insertions(+), 170 deletions(-) diff --git a/packages/web/src/actions.ts b/packages/web/src/actions.ts index 6df7296cc..1eab359be 100644 --- a/packages/web/src/actions.ts +++ b/packages/web/src/actions.ts @@ -24,7 +24,7 @@ import JoinRequestApprovedEmail from "./emails/joinRequestApprovedEmail"; import JoinRequestSubmittedEmail from "./emails/joinRequestSubmittedEmail"; import { AGENTIC_SEARCH_TUTORIAL_DISMISSED_COOKIE_NAME, MOBILE_UNSUPPORTED_SPLASH_SCREEN_DISMISSED_COOKIE_NAME, SINGLE_TENANT_ORG_ID, SOURCEBOT_SUPPORT_EMAIL } from "./lib/constants"; import { RepositoryQuery } from "./lib/types"; -import { withAuth, withOptionalAuth, withAuth_skipOrgMembershipCheck } from "./middleware/withAuth"; +import { withAuth, withOptionalAuth } from "./middleware/withAuth"; import { withMinimumOrgRole } from "./middleware/withMinimumOrgRole"; import { getBrowsePath } from "./app/(app)/browse/hooks/utils"; import { sew } from "@/middleware/sew"; @@ -758,116 +758,6 @@ export const getMe = async () => sew(() => } })); -export const redeemInvite = async (inviteId: string): Promise<{ success: boolean } | ServiceError> => sew(() => - withAuth_skipOrgMembershipCheck(async ({ user, prisma }) => { - const invite = await prisma.invite.findUnique({ - where: { - id: inviteId, - }, - include: { - org: true, - } - }); - - if (!invite) { - return notFound(); - } - - const failAuditCallback = async (error: string) => { - await auditService.createAudit({ - action: "user.invite_accept_failed", - actor: { - id: user.id, - type: "user" - }, - target: { - id: inviteId, - type: "invite" - }, - orgId: invite.org.id, - metadata: { - message: error - } - }); - } - - - const hasAvailability = await orgHasAvailability(); - if (!hasAvailability) { - await failAuditCallback("Organization is at max capacity"); - return { - statusCode: StatusCodes.BAD_REQUEST, - errorCode: ErrorCode.ORG_SEAT_COUNT_REACHED, - message: "Organization is at max capacity", - } satisfies ServiceError; - } - - // Check if the user is the recipient of the invite - if (user.email !== invite.recipientEmail) { - await failAuditCallback("User is not the recipient of the invite"); - return notFound(); - } - - const addUserToOrgRes = await addUserToOrganization(user.id, invite.orgId); - if (isServiceError(addUserToOrgRes)) { - await failAuditCallback(addUserToOrgRes.message); - return addUserToOrgRes; - } - - await auditService.createAudit({ - action: "user.invite_accepted", - actor: { - id: user.id, - type: "user" - }, - orgId: invite.org.id, - target: { - id: inviteId, - type: "invite" - } - }); - - return { - success: true, - } - })); - -export const getInviteInfo = async (inviteId: string) => sew(() => - withAuth_skipOrgMembershipCheck(async ({ user, prisma }) => { - const invite = await prisma.invite.findUnique({ - where: { - id: inviteId, - }, - include: { - org: true, - host: true, - } - }); - - if (!invite) { - return notFound(); - } - - if (invite.recipientEmail !== user.email) { - return notFound(); - } - - return { - id: invite.id, - orgName: invite.org.name, - orgImageUrl: invite.org.imageUrl ?? undefined, - host: { - name: invite.host.name ?? undefined, - email: invite.host.email!, - avatarUrl: invite.host.image ?? undefined, - }, - recipient: { - name: user.name ?? undefined, - email: user.email!, - } - } - })); - export const getOrgMembers = async () => sew(() => withAuth(async ({ org, prisma }) => { const members = await prisma.userToOrg.findMany({ diff --git a/packages/web/src/app/components/joinOrganizationButton.tsx b/packages/web/src/app/components/joinOrganizationButton.tsx index eba3b8e8a..dda376cb7 100644 --- a/packages/web/src/app/components/joinOrganizationButton.tsx +++ b/packages/web/src/app/components/joinOrganizationButton.tsx @@ -7,7 +7,6 @@ import { useState } from "react"; import { Loader2 } from "lucide-react"; import { joinOrganization } from "../invite/actions"; import { isServiceError } from "@/lib/utils"; -import { SINGLE_TENANT_ORG_ID } from "@/lib/constants"; export function JoinOrganizationButton({ inviteLinkId }: { inviteLinkId?: string }) { const [isLoading, setIsLoading] = useState(false); @@ -18,7 +17,7 @@ export function JoinOrganizationButton({ inviteLinkId }: { inviteLinkId?: string setIsLoading(true); try { - const result = await joinOrganization(SINGLE_TENANT_ORG_ID, inviteLinkId); + const result = await joinOrganization(inviteLinkId); if (isServiceError(result)) { toast({ diff --git a/packages/web/src/app/invite/actions.ts b/packages/web/src/app/invite/actions.ts index 418a65530..466705044 100644 --- a/packages/web/src/app/invite/actions.ts +++ b/packages/web/src/app/invite/actions.ts @@ -1,51 +1,185 @@ "use server"; import { isServiceError } from "@/lib/utils"; -import { orgNotFound, ServiceError } from "@/lib/serviceError"; +import { notAuthenticated, notFound, orgNotFound, ServiceError } from "@/lib/serviceError"; import { sew } from "@/middleware/sew"; -import { addUserToOrganization } from "@/lib/authUtils"; -import { withAuth_skipOrgMembershipCheck } from "@/middleware/withAuth"; +import { addUserToOrganization, orgHasAvailability } from "@/lib/authUtils"; import { StatusCodes } from "http-status-codes"; import { ErrorCode } from "@/lib/errorCodes"; +import { getAuthenticatedUser } from "@/middleware/withAuth"; +import { __unsafePrisma } from "@/prisma"; +import { SINGLE_TENANT_ORG_ID } from "@/lib/constants"; +import { getAuditService } from "@/ee/features/audit/factory"; -export const joinOrganization = async (orgId: number, inviteLinkId?: string) => sew(async () => - withAuth_skipOrgMembershipCheck(async ({ user, prisma }) => { - const org = await prisma.org.findUnique({ - where: { - id: orgId, - }, - }); +const auditService = getAuditService(); + +export const joinOrganization = async (inviteLinkId?: string) => sew(async () => { + const authResult = await getAuthenticatedUser(); + if (!authResult) { + return notAuthenticated(); + } + + const { user } = authResult; - if (!org) { - return orgNotFound(); + const org = await __unsafePrisma.org.findUnique({ + where: { + id: SINGLE_TENANT_ORG_ID, + }, + }); + + if (!org) { + return orgNotFound(); + } + + + // If member approval is required we must be using a valid invite link + if (org.memberApprovalRequired) { + if (!org.inviteLinkEnabled) { + return { + statusCode: StatusCodes.BAD_REQUEST, + errorCode: ErrorCode.INVITE_LINK_NOT_ENABLED, + message: "Invite link is not enabled.", + } satisfies ServiceError; } - // If member approval is required we must be using a valid invite link - if (org.memberApprovalRequired) { - if (!org.inviteLinkEnabled) { - return { - statusCode: StatusCodes.BAD_REQUEST, - errorCode: ErrorCode.INVITE_LINK_NOT_ENABLED, - message: "Invite link is not enabled.", - } satisfies ServiceError; - } + if (org.inviteLinkId !== inviteLinkId) { + return { + statusCode: StatusCodes.BAD_REQUEST, + errorCode: ErrorCode.INVALID_INVITE_LINK, + message: "Invalid invite link.", + } satisfies ServiceError; + } + } + + const addUserToOrgRes = await addUserToOrganization(user.id, org.id); + if (isServiceError(addUserToOrgRes)) { + return addUserToOrgRes; + } + + return { + success: true, + } +}); + +export const redeemInvite = async (inviteId: string): Promise<{ success: boolean; } | ServiceError> => sew(async () => { + const authResult = await getAuthenticatedUser(); + if (!authResult) { + return notAuthenticated(); + } + + const { user } = authResult; + + const invite = await __unsafePrisma.invite.findUnique({ + where: { + id: inviteId, + }, + include: { + org: true, + } + }); + + if (!invite) { + return notFound(); + } - if (org.inviteLinkId !== inviteLinkId) { - return { - statusCode: StatusCodes.BAD_REQUEST, - errorCode: ErrorCode.INVALID_INVITE_LINK, - message: "Invalid invite link.", - } satisfies ServiceError; + const failAuditCallback = async (error: string) => { + await auditService.createAudit({ + action: "user.invite_accept_failed", + actor: { + id: user.id, + type: "user" + }, + target: { + id: inviteId, + type: "invite" + }, + orgId: invite.org.id, + metadata: { + message: error } + }); + }; + + const hasAvailability = await orgHasAvailability(); + if (!hasAvailability) { + await failAuditCallback("Organization is at max capacity"); + return { + statusCode: StatusCodes.BAD_REQUEST, + errorCode: ErrorCode.ORG_SEAT_COUNT_REACHED, + message: "Organization is at max capacity", + } satisfies ServiceError; + } + + // Check if the user is the recipient of the invite + if (user.email !== invite.recipientEmail) { + await failAuditCallback("User is not the recipient of the invite"); + return notFound(); + } + + const addUserToOrgRes = await addUserToOrganization(user.id, invite.orgId); + if (isServiceError(addUserToOrgRes)) { + await failAuditCallback(addUserToOrgRes.message); + return addUserToOrgRes; + } + + await auditService.createAudit({ + action: "user.invite_accepted", + actor: { + id: user.id, + type: "user" + }, + orgId: invite.org.id, + target: { + id: inviteId, + type: "invite" } + }); - const addUserToOrgRes = await addUserToOrganization(user.id, org.id); - if (isServiceError(addUserToOrgRes)) { - return addUserToOrgRes; + return { + success: true, + }; +}); + + +export const getInviteInfo = async (inviteId: string) => sew(async () => { + const authResult = await getAuthenticatedUser(); + if (!authResult) { + return notAuthenticated(); + } + + const { user } = authResult; + + const invite = await __unsafePrisma.invite.findUnique({ + where: { + id: inviteId, + }, + include: { + org: true, + host: true, } + }); - return { - success: true, + if (!invite) { + return notFound(); + } + + if (invite.recipientEmail !== user.email) { + return notFound(); + } + + return { + id: invite.id, + orgName: invite.org.name, + orgImageUrl: invite.org.imageUrl ?? undefined, + host: { + name: invite.host.name ?? undefined, + email: invite.host.email!, + avatarUrl: invite.host.image ?? undefined, + }, + recipient: { + name: user.name ?? undefined, + email: user.email!, } - }) -) \ No newline at end of file + }; +}); + diff --git a/packages/web/src/app/redeem/components/acceptInviteCard.tsx b/packages/web/src/app/redeem/components/acceptInviteCard.tsx index fcfc36ee0..295ec38af 100644 --- a/packages/web/src/app/redeem/components/acceptInviteCard.tsx +++ b/packages/web/src/app/redeem/components/acceptInviteCard.tsx @@ -9,7 +9,7 @@ import placeholderAvatar from "@/public/placeholder_avatar.png"; import { ArrowRight, Loader2 } from "lucide-react"; import { Button } from "@/components/ui/button"; import { useCallback, useState } from "react"; -import { redeemInvite } from "@/actions"; +import { redeemInvite } from "@/app/invite/actions"; import { useRouter } from "next/navigation"; import { useToast } from "@/components/hooks/use-toast"; import { isServiceError } from "@/lib/utils"; diff --git a/packages/web/src/app/redeem/page.tsx b/packages/web/src/app/redeem/page.tsx index aae1ae392..8710e27dc 100644 --- a/packages/web/src/app/redeem/page.tsx +++ b/packages/web/src/app/redeem/page.tsx @@ -1,6 +1,6 @@ import { notFound, redirect } from 'next/navigation'; import { auth } from "@/auth"; -import { getInviteInfo } from "@/actions"; +import { getInviteInfo } from "../invite/actions"; import { isServiceError } from "@/lib/utils"; import { AcceptInviteCard } from './components/acceptInviteCard'; import { LogoutEscapeHatch } from '../components/logoutEscapeHatch'; diff --git a/packages/web/src/middleware/withAuth.ts b/packages/web/src/middleware/withAuth.ts index 7dbbd0bd0..f3e40acfe 100644 --- a/packages/web/src/middleware/withAuth.ts +++ b/packages/web/src/middleware/withAuth.ts @@ -24,27 +24,6 @@ type RequiredAuthContext = { prisma: PrismaClient; } -/** - * Requires a logged-in user but does NOT check org membership. - * Use this for actions where the user may not yet be a member - * of the org (e.g. joining an org, redeeming an invite). - */ -export const withAuth_skipOrgMembershipCheck = async (fn: (params: Omit & { role: OrgRole; }) => Promise) => { - const authContext = await getAuthContext(); - - if (isServiceError(authContext)) { - return authContext; - } - - const { user, prisma, org, role } = authContext; - - if (!user) { - return notAuthenticated(); - } - - return fn({ user, prisma, org, role }); -}; - export const withAuth = async (fn: (params: RequiredAuthContext) => Promise) => { const authContext = await getAuthContext(); From 47a99130dc454c1d36410b736a3c5b0aa4b7b585 Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Fri, 3 Apr 2026 17:30:27 -0700 Subject: [PATCH 5/9] add proxy middleware --- packages/web/src/proxy.ts | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 packages/web/src/proxy.ts diff --git a/packages/web/src/proxy.ts b/packages/web/src/proxy.ts new file mode 100644 index 000000000..ed5cb73a1 --- /dev/null +++ b/packages/web/src/proxy.ts @@ -0,0 +1,39 @@ +import { NextResponse } from 'next/server' +import type { NextRequest } from 'next/server' + +/** + * As part of our original SaaS effort in April 2025, we introduced + * multi-tenancy into Sourcebot as part of v3.0.0. Organizations + * (or tenants) were assigned a domain suffix that would be appended + * as the root part of the URL (e.g., `https://sourcebot.dev/org-a/...`) + * allowing us to identify what the current organization context was. + * For self-hosted instances, multi-tenancy was irrelevant, and so the + * domain suffix was simple `~` for the single-tenant org. + * + * In v4.0.0, we scrapped multi-tenancy, but we were left with the + * `~` in the URL pathname. In v4.16.8, we removed the org domain + * prefix from the URL pathname and shifted all routes to be served + * at the root domain. + * + * The following proxy middleware is used to redirect URLs that were + * created between v3.0.0 and v4.16.8 to the new canonical URL structure. + * For example, `https://sourcebot.dev/~/invite/123` would be redirected + * to `https://sourcebot.dev/invite/123`. + * + * See: https://github.com/sourcebot-dev/sourcebot/pull/1076 + */ +export async function proxy(request: NextRequest) { + const url = request.nextUrl.clone(); + + if (url.pathname.startsWith('/~/')) { + url.pathname = url.pathname.replace(/^\/~/, ''); + return NextResponse.redirect(url, 308); + } + + if (url.pathname === '/~') { + url.pathname = '/'; + return NextResponse.redirect(url, 308); + } + + return NextResponse.next(); +} From b8ae015b902d062c8bf328bb9a78e6916874587f Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Fri, 3 Apr 2026 19:03:10 -0700 Subject: [PATCH 6/9] fix build failure --- packages/web/src/app/(app)/layout.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/web/src/app/(app)/layout.tsx b/packages/web/src/app/(app)/layout.tsx index 62fe4c9b2..9bd2facc5 100644 --- a/packages/web/src/app/(app)/layout.tsx +++ b/packages/web/src/app/(app)/layout.tsx @@ -1,3 +1,9 @@ +/** + * All routes under (app) are dynamic since the layout calls auth() and + * accesses headers. + */ +export const dynamic = 'force-dynamic'; + import { __unsafePrisma } from "@/prisma"; import { auth } from "@/auth"; import { isServiceError } from "@/lib/utils"; From 8f34dd7a2a4ec8ee0b2bae1dbf3afa3a27172b34 Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Fri, 3 Apr 2026 19:04:50 -0700 Subject: [PATCH 7/9] improve redemption flow --- packages/web/src/actions.ts | 39 ++++++++++--------- .../components/submitAccountRequestButton.tsx | 7 +--- .../(app)/components/submitJoinRequest.tsx | 10 +---- .../src/emails/joinRequestSubmittedEmail.tsx | 2 +- 4 files changed, 24 insertions(+), 34 deletions(-) diff --git a/packages/web/src/actions.ts b/packages/web/src/actions.ts index 1eab359be..939ba442c 100644 --- a/packages/web/src/actions.ts +++ b/packages/web/src/actions.ts @@ -4,7 +4,7 @@ import { getAuditService } from "@/ee/features/audit/factory"; import { env, getSMTPConnectionURL } from "@sourcebot/shared"; import { addUserToOrganization, orgHasAvailability } from "@/lib/authUtils"; import { ErrorCode } from "@/lib/errorCodes"; -import { notFound, orgNotFound, ServiceError } from "@/lib/serviceError"; +import { notAuthenticated, notFound, orgNotFound, ServiceError } from "@/lib/serviceError"; import { getOrgMetadata, isHttpError, isServiceError } from "@/lib/utils"; import { __unsafePrisma } from "@/prisma"; import { render } from "@react-email/components"; @@ -24,7 +24,7 @@ import JoinRequestApprovedEmail from "./emails/joinRequestApprovedEmail"; import JoinRequestSubmittedEmail from "./emails/joinRequestSubmittedEmail"; import { AGENTIC_SEARCH_TUTORIAL_DISMISSED_COOKIE_NAME, MOBILE_UNSUPPORTED_SPLASH_SCREEN_DISMISSED_COOKIE_NAME, SINGLE_TENANT_ORG_ID, SOURCEBOT_SUPPORT_EMAIL } from "./lib/constants"; import { RepositoryQuery } from "./lib/types"; -import { withAuth, withOptionalAuth } from "./middleware/withAuth"; +import { getAuthenticatedUser, withAuth, withOptionalAuth } from "./middleware/withAuth"; import { withMinimumOrgRole } from "./middleware/withMinimumOrgRole"; import { getBrowsePath } from "./app/(app)/browse/hooks/utils"; import { sew } from "@/middleware/sew"; @@ -817,17 +817,14 @@ export const getOrgAccountRequests = async () => sew(() => })); })); -export const createAccountRequest = async (userId: string) => sew(async () => { - const user = await __unsafePrisma.user.findUnique({ - where: { - id: userId, - }, - }); - - if (!user) { - return notFound("User not found"); +export const createAccountRequest = async () => sew(async () => { + const authResult = await getAuthenticatedUser(); + if (!authResult) { + return notAuthenticated(); } + const { user } = authResult; + const org = await __unsafePrisma.org.findUnique({ where: { id: SINGLE_TENANT_ORG_ID, @@ -841,14 +838,14 @@ export const createAccountRequest = async (userId: string) => sew(async () => { const existingRequest = await __unsafePrisma.accountRequest.findUnique({ where: { requestedById_orgId: { - requestedById: userId, + requestedById: user.id, orgId: org.id, }, }, }); if (existingRequest) { - logger.warn(`User ${userId} already has an account request for org ${org.id}. Skipping account request creation.`); + logger.warn(`User ${user.id} already has an account request for org ${org.id}. Skipping account request creation.`); return { success: true, existingRequest: true, @@ -858,7 +855,7 @@ export const createAccountRequest = async (userId: string) => sew(async () => { if (!existingRequest) { await __unsafePrisma.accountRequest.create({ data: { - requestedById: userId, + requestedById: user.id, orgId: org.id, }, }); @@ -869,7 +866,7 @@ export const createAccountRequest = async (userId: string) => sew(async () => { // on user creation (the header isn't set when next-auth calls onCreateUser for some reason) const deploymentUrl = env.AUTH_URL; - const owner = await __unsafePrisma.user.findFirst({ + const owners = await __unsafePrisma.user.findMany({ where: { orgs: { some: { @@ -880,8 +877,8 @@ export const createAccountRequest = async (userId: string) => sew(async () => { }, }); - if (!owner) { - logger.error(`Failed to find owner for org ${org.id} when drafting email for account request from ${userId}`); + if (owners.length === 0) { + logger.error(`Failed to find any owners for org ${org.id} when drafting email for account request from ${user.id}`); } else { const html = await render(JoinRequestSubmittedEmail({ baseUrl: deploymentUrl, @@ -894,9 +891,13 @@ export const createAccountRequest = async (userId: string) => sew(async () => { orgImageUrl: org.imageUrl ?? undefined, })); + const ownerEmails = owners + .map((owner) => owner.email) + .filter((email): email is string => email !== null); + const transport = createTransport(smtpConnectionUrl); const result = await transport.sendMail({ - to: owner.email!, + to: ownerEmails, from: env.EMAIL_FROM_ADDRESS, subject: `New account request for ${org.name} on Sourcebot`, html, @@ -905,7 +906,7 @@ export const createAccountRequest = async (userId: string) => sew(async () => { const failed = result.rejected.concat(result.pending).filter(Boolean); if (failed.length > 0) { - logger.error(`Failed to send account request email to ${owner.email}: ${failed}`); + logger.error(`Failed to send account request email to ${ownerEmails.join(', ')}: ${failed}`); } } } else { diff --git a/packages/web/src/app/(app)/components/submitAccountRequestButton.tsx b/packages/web/src/app/(app)/components/submitAccountRequestButton.tsx index 56a9a6b68..85398a7db 100644 --- a/packages/web/src/app/(app)/components/submitAccountRequestButton.tsx +++ b/packages/web/src/app/(app)/components/submitAccountRequestButton.tsx @@ -8,18 +8,15 @@ import { createAccountRequest } from "@/actions" import { isServiceError } from "@/lib/utils" import { useRouter } from "next/navigation" -interface SubmitButtonProps { - userId: string -} -export function SubmitAccountRequestButton({ userId }: SubmitButtonProps) { +export function SubmitAccountRequestButton() { const { toast } = useToast() const router = useRouter() const [isSubmitting, setIsSubmitting] = useState(false) const handleSubmit = async () => { setIsSubmitting(true) - const result = await createAccountRequest(userId) + const result = await createAccountRequest() if (!isServiceError(result)) { if (result.existingRequest) { toast({ diff --git a/packages/web/src/app/(app)/components/submitJoinRequest.tsx b/packages/web/src/app/(app)/components/submitJoinRequest.tsx index 05c05bc0a..fdfdc0a20 100644 --- a/packages/web/src/app/(app)/components/submitJoinRequest.tsx +++ b/packages/web/src/app/(app)/components/submitJoinRequest.tsx @@ -1,16 +1,8 @@ import { LogoutEscapeHatch } from "@/app/components/logoutEscapeHatch" import { SourcebotLogo } from "@/app/components/sourcebotLogo" -import { auth } from "@/auth" import { SubmitAccountRequestButton } from "./submitAccountRequestButton" export const SubmitJoinRequest = async () => { - const session = await auth() - const userId = session?.user?.id - - if (!userId) { - return null - } - return (
@@ -41,7 +33,7 @@ export const SubmitJoinRequest = async () => {
- +
diff --git a/packages/web/src/emails/joinRequestSubmittedEmail.tsx b/packages/web/src/emails/joinRequestSubmittedEmail.tsx index 45091438c..b10774af1 100644 --- a/packages/web/src/emails/joinRequestSubmittedEmail.tsx +++ b/packages/web/src/emails/joinRequestSubmittedEmail.tsx @@ -35,7 +35,7 @@ export const JoinRequestSubmittedEmail = ({ orgImageUrl, }: JoinRequestSubmittedEmailProps) => { const previewText = `${requestor.name ?? requestor.email} has requested to join ${orgName} on Sourcebot`; - const reviewLink = `${baseUrl}/settings/members`; + const reviewLink = `${baseUrl}/settings/members?tab=requests`; return ( From 38eddad451502cc4d7f14d2cdfdff1f0f32a41bc Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Fri, 3 Apr 2026 19:22:35 -0700 Subject: [PATCH 8/9] fix: export __unsafePrisma from prisma mock for tests Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/web/src/__mocks__/prisma.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/web/src/__mocks__/prisma.ts b/packages/web/src/__mocks__/prisma.ts index d142ad1e1..a2d11360e 100644 --- a/packages/web/src/__mocks__/prisma.ts +++ b/packages/web/src/__mocks__/prisma.ts @@ -8,6 +8,7 @@ beforeEach(() => { }); export const prisma = mockDeep(); +export const __unsafePrisma = prisma; export const MOCK_ORG: Org = { id: SINGLE_TENANT_ORG_ID, From 0afa16ea8248df97194a1afb282b2b6f32ef828c Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Fri, 3 Apr 2026 19:30:59 -0700 Subject: [PATCH 9/9] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ddf0a10c1..222887e9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Links in Ask Sourcebot chat responses now open in a new tab with a subtle external link icon indicator. [#1059](https://github.com/sourcebot-dev/sourcebot/pull/1059) +- Removed `/~` from the root of most app URLs. [#1076](https://github.com/sourcebot-dev/sourcebot/pull/1076) ## [4.16.7] - 2026-04-03