diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f5c650f6..051493cef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed +- Fixed issue where external links would use internal service DNS names in k8s deployments, making them inaccessible. [#844](https://github.com/sourcebot-dev/sourcebot/pull/844) + ## [4.10.23] - 2026-02-02 ### Added diff --git a/packages/web/src/actions.ts b/packages/web/src/actions.ts index 84ff76be3..35d4a2211 100644 --- a/packages/web/src/actions.ts +++ b/packages/web/src/actions.ts @@ -17,7 +17,7 @@ import { GithubConnectionConfig } from "@sourcebot/schemas/v3/github.type"; import { GitlabConnectionConfig } from "@sourcebot/schemas/v3/gitlab.type"; import { getPlan, hasEntitlement } from "@sourcebot/shared"; import { StatusCodes } from "http-status-codes"; -import { cookies, headers } from "next/headers"; +import { cookies } from "next/headers"; import { createTransport } from "nodemailer"; import { Octokit } from "octokit"; import { auth } from "./auth"; @@ -31,7 +31,6 @@ import { AGENTIC_SEARCH_TUTORIAL_DISMISSED_COOKIE_NAME, MOBILE_UNSUPPORTED_SPLAS import { orgDomainSchema, orgNameSchema, repositoryQuerySchema } from "./lib/schemas"; import { ApiKeyPayload, TenancyMode } from "./lib/types"; import { withAuthV2, withOptionalAuthV2 } from "./withAuthV2"; -import { getBaseUrl } from "./lib/utils.server"; import { getBrowsePath } from "./app/[domain]/browse/hooks/utils"; const logger = createLogger('web-actions'); @@ -478,8 +477,7 @@ export const getRepos = async ({ take, }); - const headersList = await headers(); - const baseUrl = getBaseUrl(headersList); + const baseUrl = env.AUTH_URL; return repos.map((repo) => repositoryQuerySchema.parse({ codeHostType: repo.external_codeHostType, @@ -893,7 +891,6 @@ export const createInvites = async (emails: string[], domain: string): Promise<{ // Send invites to recipients if (env.SMTP_CONNECTION_URL && env.EMAIL_FROM_ADDRESS) { - const origin = (await headers()).get('origin')!; await Promise.all(emails.map(async (email) => { const invite = await prisma.invite.findUnique({ where: { @@ -916,7 +913,7 @@ export const createInvites = async (emails: string[], domain: string): Promise<{ email, }, }); - const inviteLink = `${origin}/redeem?invite_id=${invite.id}`; + const inviteLink = `${env.AUTH_URL}/redeem?invite_id=${invite.id}`; const transport = createTransport(env.SMTP_CONNECTION_URL); const html = await render(InviteUserEmail({ host: { @@ -1585,10 +1582,8 @@ export const approveAccountRequest = async (requestId: string, domain: string) = // Send approval email to the user if (env.SMTP_CONNECTION_URL && env.EMAIL_FROM_ADDRESS) { - const origin = (await headers()).get('origin')!; - const html = await render(JoinRequestApprovedEmail({ - baseUrl: origin, + baseUrl: env.AUTH_URL, user: { name: request.requestedBy.name ?? undefined, email: request.requestedBy.email!, diff --git a/packages/web/src/app/api/(server)/chat/blocking/route.ts b/packages/web/src/app/api/(server)/chat/blocking/route.ts index f1073fa35..ec114d394 100644 --- a/packages/web/src/app/api/(server)/chat/blocking/route.ts +++ b/packages/web/src/app/api/(server)/chat/blocking/route.ts @@ -5,13 +5,11 @@ import { convertLLMOutputToPortableMarkdown, getAnswerPartFromAssistantMessage, import { ErrorCode } from "@/lib/errorCodes"; import { requestBodySchemaValidationError, ServiceError, ServiceErrorException, serviceErrorResponse } from "@/lib/serviceError"; import { isServiceError } from "@/lib/utils"; -import { getBaseUrl } from "@/lib/utils.server"; import { withOptionalAuthV2 } from "@/withAuthV2"; import { ChatVisibility, Prisma } from "@sourcebot/db"; -import { createLogger } from "@sourcebot/shared"; +import { createLogger, env } from "@sourcebot/shared"; import { randomUUID } from "crypto"; import { StatusCodes } from "http-status-codes"; -import { headers } from "next/headers"; import { NextRequest, NextResponse } from "next/server"; import { z } from "zod"; import { createMessageStream } from "../route"; @@ -188,8 +186,7 @@ export const POST = apiHandler(async (request: NextRequest) => { const portableAnswer = convertLLMOutputToPortableMarkdown(answerText); // Build the chat URL - const headersList = await headers(); - const baseUrl = getBaseUrl(headersList); + const baseUrl = env.AUTH_URL; const chatUrl = `${baseUrl}/${org.domain}/chat/${chat.id}`; logger.debug(`Completed blocking agent for chat ${chat.id}`, { diff --git a/packages/web/src/app/api/(server)/repos/listReposApi.ts b/packages/web/src/app/api/(server)/repos/listReposApi.ts index 14aa1c951..2bbe2906f 100644 --- a/packages/web/src/app/api/(server)/repos/listReposApi.ts +++ b/packages/web/src/app/api/(server)/repos/listReposApi.ts @@ -2,17 +2,14 @@ import { sew } from "@/actions"; import { repositoryQuerySchema } from "@/lib/schemas"; import { ListReposQueryParams } from "@/lib/types"; import { withOptionalAuthV2 } from "@/withAuthV2"; -import { headers } from "next/headers"; -import { getBaseUrl } from "@/lib/utils.server"; import { getBrowsePath } from "@/app/[domain]/browse/hooks/utils"; +import { env } from "@sourcebot/shared"; export const listRepos = async ({ query, page, perPage, sort, direction }: ListReposQueryParams) => sew(() => withOptionalAuthV2(async ({ org, prisma }) => { const skip = (page - 1) * perPage; const orderByField = sort === 'pushed' ? 'pushedAt' : 'name'; - - const headersList = await headers(); - const baseUrl = getBaseUrl(headersList); + const baseUrl = env.AUTH_URL; const [repos, totalCount] = await Promise.all([ prisma.repo.findMany({ diff --git a/packages/web/src/app/components/organizationAccessSettings.tsx b/packages/web/src/app/components/organizationAccessSettings.tsx index 7e826c4b7..94a8c0408 100644 --- a/packages/web/src/app/components/organizationAccessSettings.tsx +++ b/packages/web/src/app/components/organizationAccessSettings.tsx @@ -1,13 +1,10 @@ import { createInviteLink } from "@/lib/utils" -import { getBaseUrl } from "@/lib/utils.server" import { AnonymousAccessToggle } from "./anonymousAccessToggle" import { OrganizationAccessSettingsWrapper } from "./organizationAccessSettingsWrapper" import { getOrgFromDomain } from "@/data/org" import { getOrgMetadata } from "@/lib/utils" -import { headers } from "next/headers" import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants" -import { hasEntitlement } from "@sourcebot/shared" -import { env } from "@sourcebot/shared" +import { hasEntitlement, env } from "@sourcebot/shared" export async function OrganizationAccessSettings() { const org = await getOrgFromDomain(SINGLE_TENANT_ORG_DOMAIN); @@ -18,8 +15,7 @@ export async function OrganizationAccessSettings() { const metadata = getOrgMetadata(org); const anonymousAccessEnabled = metadata?.anonymousAccessEnabled ?? false; - const headersList = await headers(); - const baseUrl = getBaseUrl(headersList); + const baseUrl = env.AUTH_URL; const inviteLink = createInviteLink(baseUrl, org.inviteLinkId) const hasAnonymousAccessEntitlement = hasEntitlement("anonymous-access"); diff --git a/packages/web/src/features/search/zoektSearcher.ts b/packages/web/src/features/search/zoektSearcher.ts index 6cd8744d6..7203521ba 100644 --- a/packages/web/src/features/search/zoektSearcher.ts +++ b/packages/web/src/features/search/zoektSearcher.ts @@ -20,8 +20,6 @@ import { RepositoryInfo, SearchResponse, SearchResultFile, SearchStats, SourceRa import { captureEvent } from "@/lib/posthog"; import { getBrowsePath } from "@/app/[domain]/browse/hooks/utils"; import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants"; -import { headers } from "next/headers"; -import { getBaseUrl } from "@/lib/utils.server"; const logger = createLogger("zoekt-searcher"); @@ -383,9 +381,6 @@ const transformZoektSearchResponse = async (response: ZoektGrpcSearchResponse, r files: SearchResultFile[], repositoryInfo: RepositoryInfo[], }> => { - const headersList = await headers(); - const baseUrl = getBaseUrl(headersList); - const files = response.files.map((file) => { const fileNameChunks = file.chunk_matches.filter((chunk) => chunk.file_name); const repoId = getRepoIdForFile(file); @@ -431,7 +426,7 @@ const transformZoektSearchResponse = async (response: ZoektGrpcSearchResponse, r repository: repo.name, repositoryId: repo.id, language: file.language, - webUrl: `${baseUrl}${getBrowsePath({ + webUrl: `${env.AUTH_URL}${getBrowsePath({ repoName: repo.name, path: fileName, pathType: 'blob', diff --git a/packages/web/src/lib/utils.server.ts b/packages/web/src/lib/utils.server.ts deleted file mode 100644 index ce1147c05..000000000 --- a/packages/web/src/lib/utils.server.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { env } from "@sourcebot/shared"; - -/** - * Gets the base URL from Next.js headers, falling back to AUTH_URL environment variable - * @param headersList The headers from Next.js headers() function - * @returns The base URL (e.g., "https://example.com") - */ -export const getBaseUrl = (headersList: Headers): string => { - const host = headersList.get('host'); - const protocol = headersList.get('x-forwarded-proto'); - - // If we have both host and protocol from headers, use them - if (host && protocol) { - return `${protocol}://${host}`; - } - - // Fall back to AUTH_URL environment variable - return env.AUTH_URL; -}