From 581a5a0bd8bdbe4fe4c62cad9fca46308cdc8c38 Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Fri, 31 Oct 2025 13:08:51 -0700 Subject: [PATCH 1/3] fix(web): Fix `/settings/connections` throwing a error when there is a `git` connection present (#588) --- CHANGELOG.md | 1 + packages/backend/src/constants.ts | 3 +- packages/backend/src/repoCompileUtils.ts | 11 ++-- packages/backend/src/utils.ts | 2 +- .../migration.sql | 14 ++++ .../migration.sql | 22 +++++++ packages/db/prisma/schema.prisma | 31 ++++++++- .../[...path]/components/codePreviewPanel.tsx | 2 +- .../[domain]/chat/components/demoCards.tsx | 3 +- .../app/[domain]/components/configEditor.tsx | 2 +- .../components/importSecretDialog.tsx | 7 +- .../navigationMenu/progressIndicator.tsx | 23 ++----- .../app/[domain]/components/pathHeader.tsx | 22 +++---- .../components/repositoryCarousel.tsx | 20 ++---- .../web/src/app/[domain]/repos/[id]/page.tsx | 2 +- .../repos/components/repoBranchesTable.tsx | 7 +- .../[domain]/repos/components/reposTable.tsx | 11 ++-- .../settings/connections/[id]/page.tsx | 21 +++--- .../components/connectionsTable.tsx | 9 +-- .../[domain]/settings/connections/page.tsx | 4 +- .../secrets/components/importSecretCard.tsx | 3 +- .../referencedFileSourceListItem.tsx | 3 +- .../chat/components/searchScopeIcon.tsx | 3 +- packages/web/src/features/fileTree/actions.ts | 2 +- packages/web/src/features/search/schemas.ts | 5 +- packages/web/src/lib/schemas.ts | 3 +- packages/web/src/lib/utils.ts | 66 ++++++++----------- 27 files changed, 175 insertions(+), 127 deletions(-) create mode 100644 packages/db/prisma/migrations/20251031012851_change_connection_type_to_enum/migration.sql create mode 100644 packages/db/prisma/migrations/20251031014307_change_repo_code_host_type_to_enum/migration.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index d4cc114c..a0bf5e9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [ask sb] Fixed issue where reasoning tokens would appear in `text` content for openai compatible models. [#582](https://github.com/sourcebot-dev/sourcebot/pull/582) - Fixed issue with GitHub app token tracking and refreshing. [#583](https://github.com/sourcebot-dev/sourcebot/pull/583) - Fixed "The account is already associated with another user" errors with GitLab oauth provider. [#584](https://github.com/sourcebot-dev/sourcebot/pull/584) +- Fixed error when viewing a generic git connection in `/settings/connections`. [#588](https://github.com/sourcebot-dev/sourcebot/pull/588) ## [4.8.1] - 2025-10-29 diff --git a/packages/backend/src/constants.ts b/packages/backend/src/constants.ts index 9ba858de..f073bac5 100644 --- a/packages/backend/src/constants.ts +++ b/packages/backend/src/constants.ts @@ -1,9 +1,10 @@ +import { CodeHostType } from "@sourcebot/db"; import { env } from "./env.js"; import path from "path"; export const SINGLE_TENANT_ORG_ID = 1; -export const PERMISSION_SYNC_SUPPORTED_CODE_HOST_TYPES = [ +export const PERMISSION_SYNC_SUPPORTED_CODE_HOST_TYPES: CodeHostType[] = [ 'github', 'gitlab', ]; diff --git a/packages/backend/src/repoCompileUtils.ts b/packages/backend/src/repoCompileUtils.ts index 8e8b1f26..d3172543 100644 --- a/packages/backend/src/repoCompileUtils.ts +++ b/packages/backend/src/repoCompileUtils.ts @@ -7,7 +7,7 @@ import { BitbucketRepository, getBitbucketReposFromConfig } from "./bitbucket.js import { getAzureDevOpsReposFromConfig } from "./azuredevops.js"; import { SchemaRestRepository as BitbucketServerRepository } from "@coderabbitai/bitbucket/server/openapi"; import { SchemaRepository as BitbucketCloudRepository } from "@coderabbitai/bitbucket/cloud/openapi"; -import { Prisma, PrismaClient } from '@sourcebot/db'; +import { CodeHostType, Prisma, PrismaClient } from '@sourcebot/db'; import { WithRequired } from "./types.js" import { marshalBool } from "./utils.js"; import { createLogger } from '@sourcebot/logger'; @@ -392,7 +392,7 @@ export const compileBitbucketConfig = async ( const repos = bitbucketRepos.map((repo) => { const isServer = config.deploymentType === 'server'; - const codeHostType = isServer ? 'bitbucket-server' : 'bitbucket-cloud'; // zoekt expects bitbucket-server + const codeHostType: CodeHostType = isServer ? 'bitbucketServer' : 'bitbucketCloud'; const displayName = isServer ? (repo as BitbucketServerRepository).name! : (repo as BitbucketCloudRepository).full_name!; const externalId = isServer ? (repo as BitbucketServerRepository).id!.toString() : (repo as BitbucketCloudRepository).uuid!; const isPublic = isServer ? (repo as BitbucketServerRepository).public : (repo as BitbucketCloudRepository).is_private === false; @@ -425,7 +425,8 @@ export const compileBitbucketConfig = async ( }, metadata: { gitConfig: { - 'zoekt.web-url-type': codeHostType, + // zoekt expects bitbucket-server and bitbucket-cloud + 'zoekt.web-url-type': codeHostType === 'bitbucketServer' ? 'bitbucket-server' : 'bitbucket-cloud', 'zoekt.web-url': webUrl, 'zoekt.name': repoName, 'zoekt.archived': marshalBool(isArchived), @@ -507,7 +508,7 @@ export const compileGenericGitHostConfig_file = async ( const repoName = path.join(remoteUrl.host, remoteUrl.pathname.replace(/\.git$/, '')); const repo: RepoData = { - external_codeHostType: 'generic-git-host', + external_codeHostType: 'genericGitHost', external_codeHostUrl: remoteUrl.resource, external_id: remoteUrl.toString(), cloneUrl: `file://${repoPath}`, @@ -571,7 +572,7 @@ export const compileGenericGitHostConfig_url = async ( const repoName = path.join(remoteUrl.host, remoteUrl.pathname.replace(/\.git$/, '')); const repo: RepoData = { - external_codeHostType: 'generic-git-host', + external_codeHostType: 'genericGitHost', external_codeHostUrl: remoteUrl.origin, external_id: remoteUrl.toString(), cloneUrl: remoteUrl.toString(), diff --git a/packages/backend/src/utils.ts b/packages/backend/src/utils.ts index 4bb18549..0fe98098 100644 --- a/packages/backend/src/utils.ts +++ b/packages/backend/src/utils.ts @@ -59,7 +59,7 @@ export const getRepoPath = (repo: Repo): { path: string, isReadOnly: boolean } = // If we are dealing with a local repository, then use that as the path. // Mark as read-only since we aren't guaranteed to have write access to the local filesystem. const cloneUrl = new URL(repo.cloneUrl); - if (repo.external_codeHostType === 'generic-git-host' && cloneUrl.protocol === 'file:') { + if (repo.external_codeHostType === 'genericGitHost' && cloneUrl.protocol === 'file:') { return { path: cloneUrl.pathname, isReadOnly: true, diff --git a/packages/db/prisma/migrations/20251031012851_change_connection_type_to_enum/migration.sql b/packages/db/prisma/migrations/20251031012851_change_connection_type_to_enum/migration.sql new file mode 100644 index 00000000..6580b522 --- /dev/null +++ b/packages/db/prisma/migrations/20251031012851_change_connection_type_to_enum/migration.sql @@ -0,0 +1,14 @@ +/* + Migrates the `connectionType` column from text to a enum. The values in this field are known to + be one of the following: github, gitlab, gitea, gerrit, bitbucket, azuredevops, git. + + This is occording to what we would expect to be in a valid config file for the schema version at commit 4899c9fbc755851af2ddcce99f4a4200f2faa4f6. + See: https://github.com/sourcebot-dev/sourcebot/blob/4899c9fbc755851af2ddcce99f4a4200f2faa4f6/packages/schemas/src/v3/connection.type.ts#L3 +*/ +-- CreateEnum +CREATE TYPE "ConnectionType" AS ENUM ('github', 'gitlab', 'gitea', 'gerrit', 'bitbucket', 'azuredevops', 'git'); + +-- AlterTable - Convert existing column to enum type without dropping data +ALTER TABLE "Connection" + ALTER COLUMN "connectionType" TYPE "ConnectionType" + USING "connectionType"::text::"ConnectionType"; diff --git a/packages/db/prisma/migrations/20251031014307_change_repo_code_host_type_to_enum/migration.sql b/packages/db/prisma/migrations/20251031014307_change_repo_code_host_type_to_enum/migration.sql new file mode 100644 index 00000000..1d3fb23b --- /dev/null +++ b/packages/db/prisma/migrations/20251031014307_change_repo_code_host_type_to_enum/migration.sql @@ -0,0 +1,22 @@ +/* + Migrates the `external_codeHostType` column from text to a enum. The values in this field are known to + be one of the following: github, gitlab, gitea, gerrit, bitbucket-server, bitbucket-cloud, generic-git-host, azuredevops. + + This is occording to what we would expect to be in the database written as of commit 4899c9fbc755851af2ddcce99f4a4200f2faa4f6. + See: + - https://github.com/sourcebot-dev/sourcebot/blob/4899c9fbc755851af2ddcce99f4a4200f2faa4f6/packages/backend/src/repoCompileUtils.ts#L57 + - https://github.com/sourcebot-dev/sourcebot/blob/4899c9fbc755851af2ddcce99f4a4200f2faa4f6/packages/backend/src/repoCompileUtils.ts#L135 + - https://github.com/sourcebot-dev/sourcebot/blob/4899c9fbc755851af2ddcce99f4a4200f2faa4f6/packages/backend/src/repoCompileUtils.ts#L208 + - https://github.com/sourcebot-dev/sourcebot/blob/4899c9fbc755851af2ddcce99f4a4200f2faa4f6/packages/backend/src/repoCompileUtils.ts#L291 + - https://github.com/sourcebot-dev/sourcebot/blob/4899c9fbc755851af2ddcce99f4a4200f2faa4f6/packages/backend/src/repoCompileUtils.ts#L407 + - https://github.com/sourcebot-dev/sourcebot/blob/4899c9fbc755851af2ddcce99f4a4200f2faa4f6/packages/backend/src/repoCompileUtils.ts#L510 + - https://github.com/sourcebot-dev/sourcebot/blob/4899c9fbc755851af2ddcce99f4a4200f2faa4f6/packages/backend/src/repoCompileUtils.ts#L574 + - https://github.com/sourcebot-dev/sourcebot/blob/4899c9fbc755851af2ddcce99f4a4200f2faa4f6/packages/backend/src/repoCompileUtils.ts#L642 +*/ +-- CreateEnum +CREATE TYPE "CodeHostType" AS ENUM ('github', 'gitlab', 'gitea', 'gerrit', 'bitbucket-server', 'bitbucket-cloud', 'generic-git-host', 'azuredevops'); + +-- AlterTable - Convert existing column to enum type without dropping data +ALTER TABLE "Repo" + ALTER COLUMN "external_codeHostType" TYPE "CodeHostType" + USING "external_codeHostType"::text::"CodeHostType"; diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index 93adb717..313de621 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -29,6 +29,21 @@ enum ChatVisibility { PUBLIC } +/// @note: The @map annotation is required to maintain backwards compatibility +/// with the existing database. +/// @note: In the generated client, these mapped values will be in pascalCase. +/// This behaviour will change in prisma v7. See: https://github.com/prisma/prisma/issues/8446#issuecomment-3356119713 +enum CodeHostType { + github + gitlab + gitea + gerrit + bitbucketServer @map("bitbucket-server") + bitbucketCloud @map("bitbucket-cloud") + genericGitHost @map("generic-git-host") + azuredevops +} + model Repo { id Int @id @default(autoincrement()) name String /// Full repo name, including the vcs hostname (ex. github.com/sourcebot-dev/sourcebot) @@ -53,7 +68,7 @@ model Repo { indexedCommitHash String? /// The commit hash of the last indexed commit (on HEAD). external_id String /// The id of the repo in the external service - external_codeHostType String /// The type of the external service (e.g., github, gitlab, etc.) + external_codeHostType CodeHostType /// The type of the external service (e.g., github, gitlab, etc.) external_codeHostUrl String /// The base url of the external service (e.g., https://github.com) org Org @relation(fields: [orgId], references: [id], onDelete: Cascade) @@ -125,6 +140,18 @@ model SearchContext { @@unique([name, orgId]) } +/// Matches the union of `type` fields in the schema. +/// @see: schemas/v3/connection.type.ts +enum ConnectionType { + github + gitlab + gitea + gerrit + bitbucket + azuredevops + git +} + model Connection { id Int @id @default(autoincrement()) name String @@ -135,7 +162,7 @@ model Connection { repos RepoToConnection[] // The type of connection (e.g., github, gitlab, etc.) - connectionType String + connectionType ConnectionType syncJobs ConnectionSyncJob[] /// When the connection was last synced successfully. diff --git a/packages/web/src/app/[domain]/browse/[...path]/components/codePreviewPanel.tsx b/packages/web/src/app/[domain]/browse/[...path]/components/codePreviewPanel.tsx index 01a84447..b38d140b 100644 --- a/packages/web/src/app/[domain]/browse/[...path]/components/codePreviewPanel.tsx +++ b/packages/web/src/app/[domain]/browse/[...path]/components/codePreviewPanel.tsx @@ -52,7 +52,7 @@ export const CodePreviewPanel = async ({ path, repoName, revisionName }: CodePre branchDisplayName={revisionName} /> - {(fileWebUrl && codeHostInfo) && ( + {fileWebUrl && ( = (previous: T) => T; export type QuickAction = { diff --git a/packages/web/src/app/[domain]/components/importSecretDialog.tsx b/packages/web/src/app/[domain]/components/importSecretDialog.tsx index b67fea7a..23d8aefe 100644 --- a/packages/web/src/app/[domain]/components/importSecretDialog.tsx +++ b/packages/web/src/app/[domain]/components/importSecretDialog.tsx @@ -9,10 +9,11 @@ import { Input } from "@/components/ui/input"; import { Separator } from "@/components/ui/separator"; import useCaptureEvent from "@/hooks/useCaptureEvent"; import { useDomain } from "@/hooks/useDomain"; -import { CodeHostType, isServiceError } from "@/lib/utils"; +import { isServiceError } from "@/lib/utils"; import githubPatCreation from "@/public/github_pat_creation.png"; import gitlabPatCreation from "@/public/gitlab_pat_creation.png"; import giteaPatCreation from "@/public/gitea_pat_creation.png"; +import { CodeHostType } from "@sourcebot/db"; import { zodResolver } from "@hookform/resolvers/zod"; import { Eye, EyeOff, Loader2 } from "lucide-react"; import Image from "next/image"; @@ -88,9 +89,9 @@ export const ImportSecretDialog = ({ open, onOpenChange, onSecretCreated, codeHo return ; case 'gitlab': return ; - case 'bitbucket-cloud': + case 'bitbucketCloud': return ; - case 'bitbucket-server': + case 'bitbucketServer': return ; case 'gitea': return ; diff --git a/packages/web/src/app/[domain]/components/navigationMenu/progressIndicator.tsx b/packages/web/src/app/[domain]/components/navigationMenu/progressIndicator.tsx index 2584f11c..71e8285a 100644 --- a/packages/web/src/app/[domain]/components/navigationMenu/progressIndicator.tsx +++ b/packages/web/src/app/[domain]/components/navigationMenu/progressIndicator.tsx @@ -10,7 +10,7 @@ import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants"; import { RepositoryQuery } from "@/lib/types"; import { getCodeHostInfoForRepo, getShortenedNumberDisplayString } from "@/lib/utils"; import clsx from "clsx"; -import { FileIcon, Loader2Icon, RefreshCwIcon } from "lucide-react"; +import { Loader2Icon, RefreshCwIcon } from "lucide-react"; import Image from "next/image"; import Link from "next/link"; import { useRouter } from "next/navigation"; @@ -90,23 +90,14 @@ const RepoItem = ({ repo }: { repo: RepositoryQuery }) => { webUrl: repo.webUrl, }); - if (info) { - return { - repoIcon: {info.codeHostName}, - displayName: info.displayName, - } - } - return { - repoIcon: , - displayName: repo.repoName, + repoIcon: {info.codeHostName}, + displayName: info.displayName, } - - }, [repo.repoName, repo.codeHostType, repo.repoDisplayName, repo.webUrl]); diff --git a/packages/web/src/app/[domain]/components/pathHeader.tsx b/packages/web/src/app/[domain]/components/pathHeader.tsx index 11b5bf1d..d65d2c35 100644 --- a/packages/web/src/app/[domain]/components/pathHeader.tsx +++ b/packages/web/src/app/[domain]/components/pathHeader.tsx @@ -1,7 +1,6 @@ 'use client'; import { cn, getCodeHostInfoForRepo } from "@/lib/utils"; -import { LaptopIcon } from "@radix-ui/react-icons"; import Image from "next/image"; import { getBrowsePath } from "../browse/hooks/utils"; import { ChevronRight, MoreHorizontal } from "lucide-react"; @@ -17,6 +16,7 @@ import { VscodeFileIcon } from "@/app/components/vscodeFileIcon"; import { CopyIconButton } from "./copyIconButton"; import Link from "next/link"; import { useDomain } from "@/hooks/useDomain"; +import { CodeHostType } from "@sourcebot/db"; interface FileHeaderProps { path: string; @@ -27,7 +27,7 @@ interface FileHeaderProps { pathType?: 'blob' | 'tree'; repo: { name: string; - codeHostType: string; + codeHostType: CodeHostType; displayName?: string; webUrl?: string; }, @@ -202,17 +202,13 @@ export const PathHeader = ({
{isCodeHostIconVisible && ( <> - {info?.icon ? ( - - {info.codeHostName} - - ) : ( - - )} + + {info.codeHostName} + )} diff --git a/packages/web/src/app/[domain]/components/repositoryCarousel.tsx b/packages/web/src/app/[domain]/components/repositoryCarousel.tsx index a9d1239f..f5576aba 100644 --- a/packages/web/src/app/[domain]/components/repositoryCarousel.tsx +++ b/packages/web/src/app/[domain]/components/repositoryCarousel.tsx @@ -8,7 +8,6 @@ import { import { captureEvent } from "@/hooks/useCaptureEvent"; import { RepositoryQuery } from "@/lib/types"; import { getCodeHostInfoForRepo } from "@/lib/utils"; -import { FileIcon } from "@radix-ui/react-icons"; import clsx from "clsx"; import Autoscroll from "embla-carousel-auto-scroll"; import Image from "next/image"; @@ -121,20 +120,13 @@ const RepositoryBadge = ({ webUrl: repo.webUrl, }); - if (info) { - return { - repoIcon: {info.codeHostName}, - displayName: info.displayName, - } - } - return { - repoIcon: , - displayName: repo.repoName, + repoIcon: {info.codeHostName}, + displayName: info.displayName, } })(); diff --git a/packages/web/src/app/[domain]/repos/[id]/page.tsx b/packages/web/src/app/[domain]/repos/[id]/page.tsx index db14e50c..a3255c04 100644 --- a/packages/web/src/app/[domain]/repos/[id]/page.tsx +++ b/packages/web/src/app/[domain]/repos/[id]/page.tsx @@ -65,7 +65,7 @@ export default async function RepoDetailPage({ params }: { params: Promise<{ id:

{repo.displayName || repo.name}

{repo.name}

- {(codeHostInfo && codeHostInfo.repoLink) && ( + {codeHostInfo.repoLink && ( - - - - The secret value to store securely. - - - - )} - /> - - - - ( - - Key - - - - - A unique name to identify this secret. - - - - )} - /> - - -
- -
- - - - - ) -} - -const GitHubPATCreationStep = ({ step }: { step: number }) => { - return ( - Navigate here on github.com (or your enterprise instance) and create a new personal access token. Sourcebot needs the repo scope in order to access private repositories: - > - Create a personal access token - - ) -} - -const GitLabPATCreationStep = ({ step }: { step: number }) => { - return ( - Navigate here on gitlab.com (or your self-hosted instance) and create a new personal access token. Sourcebot needs the read_api scope in order to access private projects: - > - Create a personal access token - - ) -} - -const GiteaPATCreationStep = ({ step }: { step: number }) => { - return ( - Navigate here on gitea.com (or your self-hosted instance) and create a new access token. Sourcebot needs the read:repository, read:user, and read:organization scopes: - > - Create a personal access token - - ) -} - -const BitbucketCloudPATCreationStep = ({ step }: { step: number }) => { - return ( - Please check out our docs for more information on how to create auth credentials for Bitbucket Cloud. - > - - ) -} - -const BitbucketServerPATCreationStep = ({ step }: { step: number }) => { - return ( - Please check out our docs for more information on how to create auth credentials for Bitbucket Data Center. - > - - ) -} - -interface SecretCreationStepProps { - step: number; - title: string; - description: string | React.ReactNode; - children?: React.ReactNode; -} - -const SecretCreationStep = ({ step, title, description, children }: SecretCreationStepProps) => { - return ( -
-
- {step} - -
-

- {title} -

-

- {description} -

- {children} -
- ) -} \ No newline at end of file diff --git a/packages/web/src/app/[domain]/settings/layout.tsx b/packages/web/src/app/[domain]/settings/layout.tsx index 8367a4ba..ccac2e99 100644 --- a/packages/web/src/app/[domain]/settings/layout.tsx +++ b/packages/web/src/app/[domain]/settings/layout.tsx @@ -106,10 +106,6 @@ export default async function SettingsLayout( isNotificationDotVisible: connectionStats.numberOfConnectionsWithFirstTimeSyncJobsInProgress > 0, } ] : []), - { - title: "Secrets", - href: `/${domain}/settings/secrets`, - }, { title: "API Keys", href: `/${domain}/settings/apiKeys`, diff --git a/packages/web/src/app/[domain]/settings/secrets/components/importSecretCard.tsx b/packages/web/src/app/[domain]/settings/secrets/components/importSecretCard.tsx deleted file mode 100644 index f92e2712..00000000 --- a/packages/web/src/app/[domain]/settings/secrets/components/importSecretCard.tsx +++ /dev/null @@ -1,66 +0,0 @@ -'use client'; - -import { CodeHostIconButton } from "@/app/[domain]/components/codeHostIconButton"; -import { Card, CardTitle, CardDescription, CardHeader, CardContent } from "@/components/ui/card"; -import { getCodeHostIcon } from "@/lib/utils"; -import { cn, CodeHostType } from "@/lib/utils"; -import { useState } from "react"; -import { ImportSecretDialog } from "@/app/[domain]/components/importSecretDialog"; -import { useRouter } from "next/navigation"; - -interface ImportSecretCardProps { - className?: string; -} - -export const ImportSecretCard = ({ className }: ImportSecretCardProps) => { - const [selectedCodeHost, setSelectedCodeHost] = useState(null); - const [isImportSecretDialogOpen, setIsImportSecretDialogOpen] = useState(false); - const router = useRouter(); - - return ( - <> - - - Import a new secret - Import a secret from a code host to allow Sourcebot to sync private repositories. - - - { - setSelectedCodeHost("github"); - setIsImportSecretDialogOpen(true); - }} - /> - { - setSelectedCodeHost("gitlab"); - setIsImportSecretDialogOpen(true); - }} - /> - { - setSelectedCodeHost("gitea"); - setIsImportSecretDialogOpen(true); - }} - /> - - - {selectedCodeHost && ( - { - router.refresh(); - }} - codeHostType={selectedCodeHost ?? "github"} - /> - )} - - ) -} \ No newline at end of file diff --git a/packages/web/src/app/[domain]/settings/secrets/components/secretsList.tsx b/packages/web/src/app/[domain]/settings/secrets/components/secretsList.tsx deleted file mode 100644 index 92ed4df7..00000000 --- a/packages/web/src/app/[domain]/settings/secrets/components/secretsList.tsx +++ /dev/null @@ -1,158 +0,0 @@ -'use client'; - -import { Input } from "@/components/ui/input"; -import { LucideKeyRound, MoreVertical, Search, LucideTrash } from "lucide-react"; -import { useState, useMemo, useCallback } from "react"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { getFormattedDate, isServiceError } from "@/lib/utils"; -import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; -import { Button } from "@/components/ui/button"; -import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog"; -import { deleteSecret } from "@/actions"; -import { useDomain } from "@/hooks/useDomain"; -import { useToast } from "@/components/hooks/use-toast"; -import { useRouter } from "next/navigation"; -import { CodeSnippet } from "@/app/components/codeSnippet"; - -interface Secret { - key: string; - createdAt: Date; -} - -interface SecretsListProps { - secrets: Secret[]; -} - -export const SecretsList = ({ secrets }: SecretsListProps) => { - const [searchQuery, setSearchQuery] = useState(""); - const [dateSort, setDateSort] = useState<"newest" | "oldest">("newest"); - const [secretToDelete, setSecretToDelete] = useState(null); - const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); - const domain = useDomain(); - const { toast } = useToast(); - const router = useRouter(); - - const filteredSecrets = useMemo(() => { - return secrets - .filter((secret) => { - const searchLower = searchQuery.toLowerCase(); - const matchesSearch = secret.key.toLowerCase().includes(searchLower); - return matchesSearch; - }) - .sort((a, b) => { - return dateSort === "newest" - ? b.createdAt.getTime() - a.createdAt.getTime() - : a.createdAt.getTime() - b.createdAt.getTime() - }); - }, [secrets, searchQuery, dateSort]); - - const onDeleteSecret = useCallback(() => { - deleteSecret(secretToDelete?.key ?? "", domain) - .then((response) => { - if (isServiceError(response)) { - toast({ - description: `❌ Failed to delete secret. Reason: ${response.message}` - }) - } else { - toast({ - description: `✅ Secret deleted successfully.` - }); - router.refresh(); - } - }) - }, [domain, secretToDelete?.key, toast, router]); - - return ( -
-
-
- - setSearchQuery(e.target.value)} - /> -
- - -
- -
-
- {secrets.length === 0 || (filteredSecrets.length === 0 && searchQuery.length > 0) ? ( -
-

No Secrets Found

-

- {filteredSecrets.length === 0 && searchQuery.length > 0 ? "No secrets found matching your filters." : "Use the form above to create a new secret."} -

-
- ) : ( - filteredSecrets.map((secret) => ( -
-
- -

{secret.key}

-
-
-

- Created {getFormattedDate(secret.createdAt)} -

- - - - - - { - setSecretToDelete(secret); - setIsDeleteDialogOpen(true); - }} - > - - Delete secret - - - -
-
- )) - )} -
-
- - - - Delete Secret - - Are you sure you want to delete the secret {secretToDelete?.key}? Any connections that use this secret will fail to sync. - - - - Cancel - - Delete - - - - -
- ) -} diff --git a/packages/web/src/app/[domain]/settings/secrets/page.tsx b/packages/web/src/app/[domain]/settings/secrets/page.tsx deleted file mode 100644 index 02714f59..00000000 --- a/packages/web/src/app/[domain]/settings/secrets/page.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { getSecrets } from "@/actions"; -import { SecretsList } from "./components/secretsList"; -import { isServiceError } from "@/lib/utils"; -import { ImportSecretCard } from "./components/importSecretCard"; -import { ServiceErrorException } from "@/lib/serviceError"; - -interface SecretsPageProps { - params: Promise<{ - domain: string; - }> -} - -export default async function SecretsPage(props: SecretsPageProps) { - const params = await props.params; - - const { - domain - } = params; - - const secrets = await getSecrets(domain); - if (isServiceError(secrets)) { - throw new ServiceErrorException(secrets); - } - - return ( -
-
-

Manage Secrets

-

These secrets grant Sourcebot access to private code.

-
- - - -
- ) -} \ No newline at end of file diff --git a/packages/web/src/app/api/(server)/chat/route.ts b/packages/web/src/app/api/(server)/chat/route.ts index d7f9368b..8f045305 100644 --- a/packages/web/src/app/api/(server)/chat/route.ts +++ b/packages/web/src/app/api/(server)/chat/route.ts @@ -92,7 +92,7 @@ export async function POST(req: Request) { }); } - const { model, providerOptions } = await _getAISDKLanguageModelAndOptions(languageModelConfig, org.id); + const { model, providerOptions } = await _getAISDKLanguageModelAndOptions(languageModelConfig); return createMessageStreamResponse({ messages, diff --git a/packages/web/src/features/chat/actions.ts b/packages/web/src/features/chat/actions.ts index 0e9638c4..ad4e9f12 100644 --- a/packages/web/src/features/chat/actions.ts +++ b/packages/web/src/features/chat/actions.ts @@ -21,7 +21,7 @@ import { createXai } from '@ai-sdk/xai'; import { fromNodeProviderChain } from '@aws-sdk/credential-providers'; import { createOpenRouter } from '@openrouter/ai-sdk-provider'; import { getTokenFromConfig } from "@sourcebot/crypto"; -import { ChatVisibility, OrgRole, Prisma, PrismaClient } from "@sourcebot/db"; +import { ChatVisibility, OrgRole, Prisma } from "@sourcebot/db"; import { LanguageModel } from "@sourcebot/schemas/v3/languageModel.type"; import { Token } from "@sourcebot/schemas/v3/shared.type"; import { loadConfig } from "@sourcebot/shared"; @@ -204,7 +204,7 @@ export const generateAndUpdateChatNameFromMessage = async ({ chatId, languageMod }); } - const { model } = await _getAISDKLanguageModelAndOptions(languageModelConfig, org.id); + const { model } = await _getAISDKLanguageModelAndOptions(languageModelConfig); const prompt = `Convert this question into a short topic title (max 50 characters). @@ -374,7 +374,7 @@ export const _getConfiguredLanguageModelsFull = async (): Promise>, }> => { @@ -386,16 +386,16 @@ export const _getAISDKLanguageModelAndOptions = async (config: LanguageModel, or baseURL: config.baseUrl, region: config.region ?? env.AWS_REGION, accessKeyId: config.accessKeyId - ? await getTokenFromConfig(config.accessKeyId, orgId, prisma) + ? await getTokenFromConfig(config.accessKeyId) : env.AWS_ACCESS_KEY_ID, secretAccessKey: config.accessKeySecret - ? await getTokenFromConfig(config.accessKeySecret, orgId, prisma) + ? await getTokenFromConfig(config.accessKeySecret) : env.AWS_SECRET_ACCESS_KEY, sessionToken: config.sessionToken - ? await getTokenFromConfig(config.sessionToken, orgId, prisma) + ? await getTokenFromConfig(config.sessionToken) : env.AWS_SESSION_TOKEN, headers: config.headers - ? await extractLanguageModelKeyValuePairs(config.headers, orgId, prisma) + ? await extractLanguageModelKeyValuePairs(config.headers) : undefined, // Fallback to the default Node.js credential provider chain if no credentials are provided. // See: https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-aws-sdk-credential-providers/#fromnodeproviderchain @@ -412,10 +412,10 @@ export const _getAISDKLanguageModelAndOptions = async (config: LanguageModel, or const anthropic = createAnthropic({ baseURL: config.baseUrl, apiKey: config.token - ? await getTokenFromConfig(config.token, orgId, prisma) + ? await getTokenFromConfig(config.token) : env.ANTHROPIC_API_KEY, headers: config.headers - ? await extractLanguageModelKeyValuePairs(config.headers, orgId, prisma) + ? await extractLanguageModelKeyValuePairs(config.headers) : undefined, }); @@ -434,11 +434,11 @@ export const _getAISDKLanguageModelAndOptions = async (config: LanguageModel, or case 'azure': { const azure = createAzure({ baseURL: config.baseUrl, - apiKey: config.token ? (await getTokenFromConfig(config.token, orgId, prisma)) : env.AZURE_API_KEY, + apiKey: config.token ? (await getTokenFromConfig(config.token)) : env.AZURE_API_KEY, apiVersion: config.apiVersion, resourceName: config.resourceName ?? env.AZURE_RESOURCE_NAME, headers: config.headers - ? await extractLanguageModelKeyValuePairs(config.headers, orgId, prisma) + ? await extractLanguageModelKeyValuePairs(config.headers) : undefined, }); @@ -449,9 +449,9 @@ export const _getAISDKLanguageModelAndOptions = async (config: LanguageModel, or case 'deepseek': { const deepseek = createDeepSeek({ baseURL: config.baseUrl, - apiKey: config.token ? (await getTokenFromConfig(config.token, orgId, prisma)) : env.DEEPSEEK_API_KEY, + apiKey: config.token ? (await getTokenFromConfig(config.token)) : env.DEEPSEEK_API_KEY, headers: config.headers - ? await extractLanguageModelKeyValuePairs(config.headers, orgId, prisma) + ? await extractLanguageModelKeyValuePairs(config.headers) : undefined, }); @@ -463,10 +463,10 @@ export const _getAISDKLanguageModelAndOptions = async (config: LanguageModel, or const google = createGoogleGenerativeAI({ baseURL: config.baseUrl, apiKey: config.token - ? await getTokenFromConfig(config.token, orgId, prisma) + ? await getTokenFromConfig(config.token) : env.GOOGLE_GENERATIVE_AI_API_KEY, headers: config.headers - ? await extractLanguageModelKeyValuePairs(config.headers, orgId, prisma) + ? await extractLanguageModelKeyValuePairs(config.headers) : undefined, }); @@ -480,11 +480,11 @@ export const _getAISDKLanguageModelAndOptions = async (config: LanguageModel, or location: config.region ?? env.GOOGLE_VERTEX_REGION, ...(config.credentials ? { googleAuthOptions: { - keyFilename: await getTokenFromConfig(config.credentials, orgId, prisma), + keyFilename: await getTokenFromConfig(config.credentials), } } : {}), headers: config.headers - ? await extractLanguageModelKeyValuePairs(config.headers, orgId, prisma) + ? await extractLanguageModelKeyValuePairs(config.headers) : undefined, }); @@ -506,11 +506,11 @@ export const _getAISDKLanguageModelAndOptions = async (config: LanguageModel, or location: config.region ?? env.GOOGLE_VERTEX_REGION, ...(config.credentials ? { googleAuthOptions: { - keyFilename: await getTokenFromConfig(config.credentials, orgId, prisma), + keyFilename: await getTokenFromConfig(config.credentials), } } : {}), headers: config.headers - ? await extractLanguageModelKeyValuePairs(config.headers, orgId, prisma) + ? await extractLanguageModelKeyValuePairs(config.headers) : undefined, }); @@ -522,10 +522,10 @@ export const _getAISDKLanguageModelAndOptions = async (config: LanguageModel, or const mistral = createMistral({ baseURL: config.baseUrl, apiKey: config.token - ? await getTokenFromConfig(config.token, orgId, prisma) + ? await getTokenFromConfig(config.token) : env.MISTRAL_API_KEY, headers: config.headers - ? await extractLanguageModelKeyValuePairs(config.headers, orgId, prisma) + ? await extractLanguageModelKeyValuePairs(config.headers) : undefined, }); @@ -537,10 +537,10 @@ export const _getAISDKLanguageModelAndOptions = async (config: LanguageModel, or const openai = createOpenAI({ baseURL: config.baseUrl, apiKey: config.token - ? await getTokenFromConfig(config.token, orgId, prisma) + ? await getTokenFromConfig(config.token) : env.OPENAI_API_KEY, headers: config.headers - ? await extractLanguageModelKeyValuePairs(config.headers, orgId, prisma) + ? await extractLanguageModelKeyValuePairs(config.headers) : undefined, }); @@ -558,13 +558,13 @@ export const _getAISDKLanguageModelAndOptions = async (config: LanguageModel, or baseURL: config.baseUrl, name: config.displayName ?? modelId, apiKey: config.token - ? await getTokenFromConfig(config.token, orgId, prisma) + ? await getTokenFromConfig(config.token) : undefined, headers: config.headers - ? await extractLanguageModelKeyValuePairs(config.headers, orgId, prisma) + ? await extractLanguageModelKeyValuePairs(config.headers) : undefined, queryParams: config.queryParams - ? await extractLanguageModelKeyValuePairs(config.queryParams, orgId, prisma) + ? await extractLanguageModelKeyValuePairs(config.queryParams) : undefined, }); @@ -585,10 +585,10 @@ export const _getAISDKLanguageModelAndOptions = async (config: LanguageModel, or const openrouter = createOpenRouter({ baseURL: config.baseUrl, apiKey: config.token - ? await getTokenFromConfig(config.token, orgId, prisma) + ? await getTokenFromConfig(config.token) : env.OPENROUTER_API_KEY, headers: config.headers - ? await extractLanguageModelKeyValuePairs(config.headers, orgId, prisma) + ? await extractLanguageModelKeyValuePairs(config.headers) : undefined, }); @@ -600,10 +600,10 @@ export const _getAISDKLanguageModelAndOptions = async (config: LanguageModel, or const xai = createXai({ baseURL: config.baseUrl, apiKey: config.token - ? await getTokenFromConfig(config.token, orgId, prisma) + ? await getTokenFromConfig(config.token) : env.XAI_API_KEY, headers: config.headers - ? await extractLanguageModelKeyValuePairs(config.headers, orgId, prisma) + ? await extractLanguageModelKeyValuePairs(config.headers) : undefined, }); @@ -617,9 +617,7 @@ export const _getAISDKLanguageModelAndOptions = async (config: LanguageModel, or const extractLanguageModelKeyValuePairs = async ( pairs: { [k: string]: string | Token; - }, - orgId: number, - db: PrismaClient, + } ): Promise> => { const resolvedPairs: Record = {}; @@ -633,7 +631,7 @@ const extractLanguageModelKeyValuePairs = async ( continue; } - const value = await getTokenFromConfig(val, orgId, db); + const value = await getTokenFromConfig(val); resolvedPairs[key] = value; } diff --git a/packages/web/src/lib/strings.ts b/packages/web/src/lib/strings.ts deleted file mode 100644 index 9fbcfd47..00000000 --- a/packages/web/src/lib/strings.ts +++ /dev/null @@ -1,7 +0,0 @@ - -export const strings = { - connectionConfigDescription: "Configure what repositories, organizations, users, etc. you want to sync with Sourcebot. Use the quick actions below to help you configure your connection.", - createSecretDescription: "Secrets are used to authenticate with the code host, allowing Sourcebot to access private repositories.", -} - -export default strings; diff --git a/schemas/v2/index.json b/schemas/v2/index.json index 67334c2a..0c9a79a2 100644 --- a/schemas/v2/index.json +++ b/schemas/v2/index.json @@ -76,7 +76,6 @@ "$ref": "#/definitions/Token", "description": "A Personal Access Token (PAT).", "examples": [ - "secret-token", { "env": "ENV_VAR_CONTAINING_TOKEN" } ] }, @@ -210,7 +209,6 @@ "$ref": "#/definitions/Token", "description": "An authentication token.", "examples": [ - "secret-token", { "env": "ENV_VAR_CONTAINING_TOKEN" } ] }, @@ -332,7 +330,6 @@ "$ref": "#/definitions/Token", "description": "An access token.", "examples": [ - "secret-token", { "env": "ENV_VAR_CONTAINING_TOKEN" } ] }, diff --git a/schemas/v3/azuredevops.json b/schemas/v3/azuredevops.json index 6cc27833..573a7335 100644 --- a/schemas/v3/azuredevops.json +++ b/schemas/v3/azuredevops.json @@ -9,12 +9,7 @@ }, "token": { "$ref": "./shared.json#/definitions/Token", - "description": "A Personal Access Token (PAT).", - "examples": [ - { - "secret": "SECRET_KEY" - } - ] + "description": "A Personal Access Token (PAT)." }, "url": { "type": "string", diff --git a/schemas/v3/bitbucket.json b/schemas/v3/bitbucket.json index be2fdda9..a980a17c 100644 --- a/schemas/v3/bitbucket.json +++ b/schemas/v3/bitbucket.json @@ -13,12 +13,7 @@ }, "token": { "$ref": "./shared.json#/definitions/Token", - "description": "An authentication token.", - "examples": [ - { - "secret": "SECRET_KEY" - } - ] + "description": "An authentication token." }, "url": { "type": "string", diff --git a/schemas/v3/gitea.json b/schemas/v3/gitea.json index d5c87665..35f38fb1 100644 --- a/schemas/v3/gitea.json +++ b/schemas/v3/gitea.json @@ -9,12 +9,7 @@ }, "token": { "$ref": "./shared.json#/definitions/Token", - "description": "A Personal Access Token (PAT).", - "examples": [ - { - "secret": "SECRET_KEY" - } - ] + "description": "A Personal Access Token (PAT)." }, "url": { "type": "string", diff --git a/schemas/v3/github.json b/schemas/v3/github.json index ec4a9f4f..c2002782 100644 --- a/schemas/v3/github.json +++ b/schemas/v3/github.json @@ -9,12 +9,7 @@ }, "token": { "$ref": "./shared.json#/definitions/Token", - "description": "A Personal Access Token (PAT).", - "examples": [ - { - "secret": "SECRET_KEY" - } - ] + "description": "A Personal Access Token (PAT)." }, "url": { "type": "string", diff --git a/schemas/v3/gitlab.json b/schemas/v3/gitlab.json index ab5b4e62..9d3b1ca9 100644 --- a/schemas/v3/gitlab.json +++ b/schemas/v3/gitlab.json @@ -9,12 +9,7 @@ }, "token": { "$ref": "./shared.json#/definitions/Token", - "description": "An authentication token.", - "examples": [ - { - "secret": "SECRET_KEY" - } - ] + "description": "An authentication token." }, "url": { "type": "string", diff --git a/schemas/v3/shared.json b/schemas/v3/shared.json index baa6dae8..a290edd1 100644 --- a/schemas/v3/shared.json +++ b/schemas/v3/shared.json @@ -4,19 +4,6 @@ "definitions": { "Token": { "anyOf": [ - { - "type": "object", - "properties": { - "secret": { - "type": "string", - "description": "The name of the secret that contains the token." - } - }, - "required": [ - "secret" - ], - "additionalProperties": false - }, { "type": "object", "properties": { From 43662bc6d17f944e418e5e41f52648597e43f822 Mon Sep 17 00:00:00 2001 From: msukkari Date: Fri, 31 Oct 2025 13:51:42 -0700 Subject: [PATCH 3/3] changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4cc114c..aa387fdc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed issue with GitHub app token tracking and refreshing. [#583](https://github.com/sourcebot-dev/sourcebot/pull/583) - Fixed "The account is already associated with another user" errors with GitLab oauth provider. [#584](https://github.com/sourcebot-dev/sourcebot/pull/584) +## Removed +- Removed built-in secret manager. [#592](https://github.com/sourcebot-dev/sourcebot/pull/592) + ## [4.8.1] - 2025-10-29 ### Fixed