From 285c7ed30b890ff9590a30b41b3f6b87e3d01b11 Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Wed, 1 Apr 2026 18:30:56 -0700 Subject: [PATCH 1/4] refactor(web): rename withAuthV2 to withAuth and move to middleware/ Renames withAuthV2/withOptionalAuthV2 to withAuth/withOptionalAuth and relocates them from src/withAuthV2.ts to src/middleware/withAuth.ts. Extracts withMinimumOrgRole and sew into their own files under middleware/. Fixes 'use server' build error by removing logger export from actions.ts and fixing mock path in withAuth.test.ts. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/web/src/actions.ts | 78 +++++++------------ .../app/[domain]/askgh/[owner]/[repo]/api.ts | 6 +- .../web/src/app/[domain]/repos/[id]/page.tsx | 7 +- packages/web/src/app/[domain]/repos/page.tsx | 6 +- .../settings/connections/[id]/page.tsx | 6 +- .../[domain]/settings/connections/page.tsx | 6 +- .../web/src/app/[domain]/settings/layout.tsx | 4 +- .../web/src/app/api/(server)/chat/route.ts | 6 +- .../ee/accountPermissionSyncJobStatus/api.ts | 6 +- .../ee/chat/[chatId]/searchMembers/route.ts | 4 +- .../(server)/ee/permissionSyncStatus/api.ts | 6 +- .../web/src/app/api/(server)/ee/user/route.ts | 7 +- .../src/app/api/(server)/ee/users/route.ts | 5 +- .../web/src/app/api/(server)/mcp/route.ts | 8 +- .../web/src/app/api/(server)/models/route.ts | 6 +- .../app/api/(server)/repos/listReposApi.ts | 6 +- packages/web/src/app/invite/actions.ts | 6 +- .../web/src/ee/features/analytics/actions.ts | 7 +- packages/web/src/ee/features/audit/actions.ts | 9 ++- packages/web/src/ee/features/oauth/actions.ts | 8 +- packages/web/src/ee/features/sso/actions.ts | 9 ++- .../src/ee/features/userManagement/actions.ts | 9 ++- packages/web/src/features/chat/actions.ts | 30 +++---- packages/web/src/features/codeNav/api.ts | 8 +- packages/web/src/features/git/getDiffApi.ts | 6 +- .../web/src/features/git/getFileSourceApi.ts | 6 +- packages/web/src/features/git/getFilesApi.ts | 6 +- .../src/features/git/getFolderContentsApi.ts | 6 +- packages/web/src/features/git/getTreeApi.ts | 6 +- .../web/src/features/git/listCommitsApi.ts | 6 +- packages/web/src/features/mcp/askCodebase.ts | 6 +- packages/web/src/features/search/searchApi.ts | 8 +- .../web/src/features/searchAssist/actions.ts | 6 +- .../src/features/userManagement/actions.ts | 9 ++- .../web/src/features/workerApi/actions.ts | 13 ++-- packages/web/src/lib/posthog.ts | 2 +- packages/web/src/middleware/sew.ts | 27 +++++++ .../withAuth.test.ts} | 52 ++++++------- .../{withAuthV2.ts => middleware/withAuth.ts} | 44 ++--------- .../web/src/middleware/withMinimumOrgRole.ts | 32 ++++++++ 40 files changed, 254 insertions(+), 234 deletions(-) create mode 100644 packages/web/src/middleware/sew.ts rename packages/web/src/{withAuthV2.test.ts => middleware/withAuth.test.ts} (96%) rename packages/web/src/{withAuthV2.ts => middleware/withAuth.ts} (83%) create mode 100644 packages/web/src/middleware/withMinimumOrgRole.ts diff --git a/packages/web/src/actions.ts b/packages/web/src/actions.ts index 22550455c..927513eb4 100644 --- a/packages/web/src/actions.ts +++ b/packages/web/src/actions.ts @@ -4,11 +4,10 @@ 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, ServiceErrorException, unexpectedError } from "@/lib/serviceError"; +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 * as Sentry from '@sentry/nextjs'; import { generateApiKey, getTokenFromConfig, hashSecret } from "@sourcebot/shared"; import { ApiKey, ConnectionSyncJobStatus, OrgRole, Prisma, RepoIndexingJobStatus, RepoIndexingJobType } from "@sourcebot/db"; import { createLogger } from "@sourcebot/shared"; @@ -26,36 +25,17 @@ 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 { withAuthV2, withOptionalAuthV2, withMinimumOrgRole, withAuthV2_skipOrgMembershipCheck } from "./withAuthV2"; +import { withAuth, withOptionalAuth, withAuth_skipOrgMembershipCheck } from "./middleware/withAuth"; +import { withMinimumOrgRole } from "./middleware/withMinimumOrgRole"; import { getBrowsePath } from "./app/[domain]/browse/hooks/utils"; +import { sew } from "@/middleware/sew"; const logger = createLogger('web-actions'); const auditService = getAuditService(); -/** - * "Service Error Wrapper". - * - * Captures any thrown exceptions, logs them to the console and Sentry, - * and returns a generic unexpected service error. - */ -export const sew = async (fn: () => Promise): Promise => { - try { - return await fn(); - } catch (e) { - Sentry.captureException(e); - logger.error(e); - - if (e instanceof ServiceErrorException) { - return e.serviceError; - } - - return unexpectedError(`An unexpected error occurred. Please try again later.`); - } -} - ////// Actions /////// export const completeOnboarding = async (): Promise<{ success: boolean } | ServiceError> => sew(() => - withAuthV2(async ({ org, prisma }) => { + withAuth(async ({ org, prisma }) => { await prisma.org.update({ where: { id: org.id }, data: { @@ -122,7 +102,7 @@ export const verifyApiKey = async (apiKeyPayload: ApiKeyPayload): Promise<{ apiK export const createApiKey = async (name: string): Promise<{ key: string } | ServiceError> => sew(() => - withAuthV2(async ({ org, user, role, prisma }) => { + 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) { logger.error(`API key creation is disabled for non-admin users. User ${user.id} is not an owner.`); return { @@ -192,7 +172,7 @@ export const createApiKey = async (name: string): Promise<{ key: string } | Serv })); export const deleteApiKey = async (name: string): Promise<{ success: boolean } | ServiceError> => sew(() => - withAuthV2(async ({ org, user, prisma }) => { + withAuth(async ({ org, user, prisma }) => { const apiKey = await prisma.apiKey.findFirst({ where: { name, @@ -252,7 +232,7 @@ export const deleteApiKey = async (name: string): Promise<{ success: boolean } | })); export const getUserApiKeys = async (): Promise<{ name: string; createdAt: Date; lastUsedAt: Date | null }[] | ServiceError> => sew(() => - withAuthV2(async ({ org, user, prisma }) => { + withAuth(async ({ org, user, prisma }) => { const apiKeys = await prisma.apiKey.findMany({ where: { orgId: org.id, @@ -277,7 +257,7 @@ export const getRepos = async ({ where?: Prisma.RepoWhereInput, take?: number } = {}) => sew(() => - withOptionalAuthV2(async ({ org, prisma }) => { + withOptionalAuth(async ({ org, prisma }) => { const repos = await prisma.repo.findMany({ where: { orgId: org.id, @@ -313,7 +293,7 @@ export const getRepos = async ({ * Returns a set of aggregated stats about the repos in the org */ export const getReposStats = async () => sew(() => - withOptionalAuthV2(async ({ org, prisma }) => { + withOptionalAuth(async ({ org, prisma }) => { const [ // Total number of repos. numberOfRepos, @@ -364,7 +344,7 @@ export const getReposStats = async () => sew(() => ) export const getConnectionStats = async () => sew(() => - withAuthV2(async ({ org, prisma }) => { + withAuth(async ({ org, prisma }) => { const [ numberOfConnections, numberOfConnectionsWithFirstTimeSyncJobsInProgress, @@ -400,7 +380,7 @@ export const getConnectionStats = async () => sew(() => ); export const getRepoInfoByName = async (repoName: string) => sew(() => - withOptionalAuthV2(async ({ org, prisma }) => { + withOptionalAuth(async ({ org, prisma }) => { // @note: repo names are represented by their remote url // on the code host. E.g.,: // - github.com/sourcebot-dev/sourcebot @@ -459,7 +439,7 @@ export const getRepoInfoByName = async (repoName: string) => sew(() => })); export const experimental_addGithubRepositoryByUrl = async (repositoryUrl: string): Promise<{ connectionId: number } | ServiceError> => sew(() => - withOptionalAuthV2(async ({ org, prisma }) => { + withOptionalAuth(async ({ org, prisma }) => { if (env.EXPERIMENT_SELF_SERVE_REPO_INDEXING_ENABLED !== 'true') { return { statusCode: StatusCodes.BAD_REQUEST, @@ -595,12 +575,12 @@ export const experimental_addGithubRepositoryByUrl = async (repositoryUrl: strin })); export const getCurrentUserRole = async (): Promise => sew(() => - withOptionalAuthV2(async ({ role }) => { + withOptionalAuth(async ({ role }) => { return role; })); export const createInvites = async (emails: string[]): Promise<{ success: boolean } | ServiceError> => sew(() => - withAuthV2(async ({ org, user, role, prisma }) => + withAuth(async ({ org, user, role, prisma }) => withMinimumOrgRole(role, OrgRole.OWNER, async () => { const failAuditCallback = async (error: string) => { await auditService.createAudit({ @@ -776,7 +756,7 @@ export const createInvites = async (emails: string[]): Promise<{ success: boolea )); export const cancelInvite = async (inviteId: string): Promise<{ success: boolean } | ServiceError> => sew(() => - withAuthV2(async ({ org, role, prisma }) => + withAuth(async ({ org, role, prisma }) => withMinimumOrgRole(role, OrgRole.OWNER, async () => { const invite = await prisma.invite.findUnique({ where: { @@ -802,7 +782,7 @@ export const cancelInvite = async (inviteId: string): Promise<{ success: boolean )); export const getMe = async () => sew(() => - withAuthV2(async ({ user, prisma }) => { + withAuth(async ({ user, prisma }) => { const userWithOrgs = await prisma.user.findUnique({ where: { id: user.id, @@ -835,7 +815,7 @@ export const getMe = async () => sew(() => })); export const redeemInvite = async (inviteId: string): Promise<{ success: boolean } | ServiceError> => sew(() => - withAuthV2_skipOrgMembershipCheck(async ({ user, prisma }) => { + withAuth_skipOrgMembershipCheck(async ({ user, prisma }) => { const invite = await prisma.invite.findUnique({ where: { id: inviteId, @@ -909,7 +889,7 @@ export const redeemInvite = async (inviteId: string): Promise<{ success: boolean })); export const getInviteInfo = async (inviteId: string) => sew(() => - withAuthV2_skipOrgMembershipCheck(async ({ user, prisma }) => { + withAuth_skipOrgMembershipCheck(async ({ user, prisma }) => { const invite = await prisma.invite.findUnique({ where: { id: inviteId, @@ -946,7 +926,7 @@ export const getInviteInfo = async (inviteId: string) => sew(() => })); export const getOrgMembers = async () => sew(() => - withAuthV2(async ({ org, prisma }) => { + withAuth(async ({ org, prisma }) => { const members = await prisma.userToOrg.findMany({ where: { orgId: org.id, @@ -970,7 +950,7 @@ export const getOrgMembers = async () => sew(() => })); export const getOrgInvites = async () => sew(() => - withAuthV2(async ({ org, prisma }) => { + withAuth(async ({ org, prisma }) => { const invites = await prisma.invite.findMany({ where: { orgId: org.id, @@ -985,7 +965,7 @@ export const getOrgInvites = async () => sew(() => })); export const getOrgAccountRequests = async () => sew(() => - withAuthV2(async ({ org, prisma }) => { + withAuth(async ({ org, prisma }) => { const requests = await prisma.accountRequest.findMany({ where: { orgId: org.id, @@ -1122,7 +1102,7 @@ export const getMemberApprovalRequired = async (domain: string): Promise => sew(async () => - withAuthV2(async ({ org, role, prisma }) => + withAuth(async ({ org, role, prisma }) => withMinimumOrgRole(role, OrgRole.OWNER, async () => { await prisma.org.update({ where: { id: org.id }, @@ -1137,7 +1117,7 @@ export const setMemberApprovalRequired = async (required: boolean): Promise<{ su ); export const setInviteLinkEnabled = async (enabled: boolean): Promise<{ success: boolean } | ServiceError> => sew(async () => - withAuthV2(async ({ org, role, prisma }) => + withAuth(async ({ org, role, prisma }) => withMinimumOrgRole(role, OrgRole.OWNER, async () => { await prisma.org.update({ where: { id: org.id }, @@ -1152,7 +1132,7 @@ export const setInviteLinkEnabled = async (enabled: boolean): Promise<{ success: ); export const approveAccountRequest = async (requestId: string) => sew(async () => - withAuthV2(async ({ org, user, role, prisma }) => + withAuth(async ({ org, user, role, prisma }) => withMinimumOrgRole(role, OrgRole.OWNER, async () => { const failAuditCallback = async (error: string) => { await auditService.createAudit({ @@ -1242,7 +1222,7 @@ export const approveAccountRequest = async (requestId: string) => sew(async () = )); export const rejectAccountRequest = async (requestId: string) => sew(() => - withAuthV2(async ({ org, role, prisma }) => + withAuth(async ({ org, role, prisma }) => withMinimumOrgRole(role, OrgRole.OWNER, async () => { const request = await prisma.accountRequest.findUnique({ where: { @@ -1268,7 +1248,7 @@ export const rejectAccountRequest = async (requestId: string) => sew(() => export const getSearchContexts = async () => sew(() => - withOptionalAuthV2(async ({ org, prisma }) => { + withOptionalAuth(async ({ org, prisma }) => { const searchContexts = await prisma.searchContext.findMany({ where: { orgId: org.id, @@ -1287,7 +1267,7 @@ export const getSearchContexts = async () => sew(() => })); export const getRepoImage = async (repoId: number): Promise => sew(async () => { - return await withOptionalAuthV2(async ({ org, prisma }) => { + return await withOptionalAuth(async ({ org, prisma }) => { const repo = await prisma.repo.findUnique({ where: { id: repoId, @@ -1383,7 +1363,7 @@ export const getAnonymousAccessStatus = async (domain: string): Promise => sew(async () => { - return await withAuthV2(async ({ org, role, prisma }) => { + return await withAuth(async ({ org, role, prisma }) => { return await withMinimumOrgRole(role, OrgRole.OWNER, async () => { const hasAnonymousAccessEntitlement = hasEntitlement("anonymous-access"); if (!hasAnonymousAccessEntitlement) { diff --git a/packages/web/src/app/[domain]/askgh/[owner]/[repo]/api.ts b/packages/web/src/app/[domain]/askgh/[owner]/[repo]/api.ts index d481d67e3..64d37f2e6 100644 --- a/packages/web/src/app/[domain]/askgh/[owner]/[repo]/api.ts +++ b/packages/web/src/app/[domain]/askgh/[owner]/[repo]/api.ts @@ -1,12 +1,12 @@ import 'server-only'; -import { sew } from '@/actions'; +import { sew } from "@/middleware/sew"; import { notFound, ServiceError } from '@/lib/serviceError'; -import { withOptionalAuthV2 } from '@/withAuthV2'; +import { withOptionalAuth } from '@/middleware/withAuth'; import { RepoInfo } from './types'; export const getRepoInfo = async (repoId: number): Promise => sew(() => - withOptionalAuthV2(async ({ prisma }) => { + withOptionalAuth(async ({ prisma }) => { const repo = await prisma.repo.findUnique({ where: { id: repoId }, include: { diff --git a/packages/web/src/app/[domain]/repos/[id]/page.tsx b/packages/web/src/app/[domain]/repos/[id]/page.tsx index d16926d12..193c9a1c9 100644 --- a/packages/web/src/app/[domain]/repos/[id]/page.tsx +++ b/packages/web/src/app/[domain]/repos/[id]/page.tsx @@ -1,4 +1,5 @@ -import { getCurrentUserRole, sew } from "@/actions" +import { getCurrentUserRole } from "@/actions" +import { sew } from "@/middleware/sew" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" @@ -8,7 +9,7 @@ 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 { withOptionalAuthV2 } from "@/withAuthV2" +import { withOptionalAuth } from "@/middleware/withAuth" import { getConfigSettings, repoMetadataSchema } from "@sourcebot/shared" import { ExternalLink, Info } from "lucide-react" import Image from "next/image" @@ -190,7 +191,7 @@ export default async function RepoDetailPage({ params }: { params: Promise<{ id: } const getRepoWithJobs = async (repoId: number) => sew(() => - withOptionalAuthV2(async ({ prisma, org }) => { + withOptionalAuth(async ({ prisma, org }) => { const repo = await prisma.repo.findUnique({ where: { diff --git a/packages/web/src/app/[domain]/repos/page.tsx b/packages/web/src/app/[domain]/repos/page.tsx index e0963a590..5e123b735 100644 --- a/packages/web/src/app/[domain]/repos/page.tsx +++ b/packages/web/src/app/[domain]/repos/page.tsx @@ -1,7 +1,7 @@ -import { sew } from "@/actions"; +import { sew } from "@/middleware/sew"; import { ServiceErrorException } from "@/lib/serviceError"; import { isServiceError } from "@/lib/utils"; -import { withOptionalAuthV2 } from "@/withAuthV2"; +import { withOptionalAuth } from "@/middleware/withAuth"; import { ReposTable } from "./components/reposTable"; import { RepoIndexingJobStatus, Prisma } from "@sourcebot/db"; import z from "zod"; @@ -96,7 +96,7 @@ interface GetReposParams { } const getRepos = async ({ skip, take, search, status, sortBy, sortOrder }: GetReposParams) => sew(() => - withOptionalAuthV2(async ({ prisma }) => { + withOptionalAuth(async ({ prisma }) => { const whereClause: Prisma.RepoWhereInput = { ...(search ? { displayName: { contains: search, mode: 'insensitive' }, diff --git a/packages/web/src/app/[domain]/settings/connections/[id]/page.tsx b/packages/web/src/app/[domain]/settings/connections/[id]/page.tsx index b3c7fe799..3229112b2 100644 --- a/packages/web/src/app/[domain]/settings/connections/[id]/page.tsx +++ b/packages/web/src/app/[domain]/settings/connections/[id]/page.tsx @@ -1,4 +1,4 @@ -import { sew } from "@/actions"; +import { sew } from "@/middleware/sew"; import { BackButton } from "@/app/[domain]/components/backButton"; import { DisplayDate } from "@/app/[domain]/components/DisplayDate"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; @@ -8,7 +8,7 @@ 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"; -import { withAuthV2 } from "@/withAuthV2"; +import { withAuth } from "@/middleware/withAuth"; import { AzureDevOpsConnectionConfig, BitbucketConnectionConfig, GenericGitHostConnectionConfig, GerritConnectionConfig, GiteaConnectionConfig, GithubConnectionConfig, GitlabConnectionConfig } from "@sourcebot/schemas/v3/index.type"; import { env, getConfigSettings } from "@sourcebot/shared"; import { Info } from "lucide-react"; @@ -188,7 +188,7 @@ export default async function ConnectionDetailPage(props: ConnectionDetailPagePr } const getConnectionWithJobs = async (id: number) => sew(() => - withAuthV2(async ({ prisma, org }) => { + withAuth(async ({ prisma, org }) => { const connection = await prisma.connection.findUnique({ where: { id, diff --git a/packages/web/src/app/[domain]/settings/connections/page.tsx b/packages/web/src/app/[domain]/settings/connections/page.tsx index 62b0fddd9..2b046cabe 100644 --- a/packages/web/src/app/[domain]/settings/connections/page.tsx +++ b/packages/web/src/app/[domain]/settings/connections/page.tsx @@ -1,7 +1,7 @@ -import { sew } from "@/actions"; +import { sew } from "@/middleware/sew"; import { ServiceErrorException } from "@/lib/serviceError"; import { isServiceError } from "@/lib/utils"; -import { withAuthV2 } from "@/withAuthV2"; +import { withAuth } from "@/middleware/withAuth"; import Link from "next/link"; import { ConnectionsTable } from "./components/connectionsTable"; import { ConnectionSyncJobStatus } from "@prisma/client"; @@ -50,7 +50,7 @@ export default async function ConnectionsPage() { } const getConnectionsWithLatestJob = async () => sew(() => - withAuthV2(async ({ prisma, org }) => { + withAuth(async ({ prisma, org }) => { const connections = await prisma.connection.findMany({ where: { orgId: org.id, diff --git a/packages/web/src/app/[domain]/settings/layout.tsx b/packages/web/src/app/[domain]/settings/layout.tsx index d9e9f380b..4dcd24224 100644 --- a/packages/web/src/app/[domain]/settings/layout.tsx +++ b/packages/web/src/app/[domain]/settings/layout.tsx @@ -10,7 +10,7 @@ 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 { withAuthV2 } from "@/withAuthV2"; +import { withAuth } from "@/middleware/withAuth"; interface LayoutProps { children: React.ReactNode; @@ -67,7 +67,7 @@ export default async function SettingsLayout( } export const getSidebarNavItems = async () => - withAuthV2(async ({ role }) => { + withAuth(async ({ role }) => { let numJoinRequests: number | undefined; if (role === OrgRole.OWNER) { const requests = await getOrgAccountRequests(); diff --git a/packages/web/src/app/api/(server)/chat/route.ts b/packages/web/src/app/api/(server)/chat/route.ts index 6385b4106..bf4a5488f 100644 --- a/packages/web/src/app/api/(server)/chat/route.ts +++ b/packages/web/src/app/api/(server)/chat/route.ts @@ -1,4 +1,4 @@ -import { sew } from "@/actions"; +import { sew } from "@/middleware/sew"; import { createMessageStream } from "@/features/chat/agent"; import { additionalChatRequestParamsSchema } from "@/features/chat/types"; import { getLanguageModelKey } from "@/features/chat/utils"; @@ -8,7 +8,7 @@ import { ErrorCode } from "@/lib/errorCodes"; import { captureEvent } from "@/lib/posthog"; import { notFound, requestBodySchemaValidationError, ServiceError, serviceErrorResponse } from "@/lib/serviceError"; import { isServiceError } from "@/lib/utils"; -import { withOptionalAuthV2 } from "@/withAuthV2"; +import { withOptionalAuth } from "@/middleware/withAuth"; import * as Sentry from "@sentry/nextjs"; import { createLogger, env } from "@sourcebot/shared"; import { @@ -40,7 +40,7 @@ export const POST = apiHandler(async (req: NextRequest) => { const languageModel = _languageModel; const response = await sew(() => - withOptionalAuthV2(async ({ org, user, prisma }) => { + withOptionalAuth(async ({ org, user, prisma }) => { // Validate that the chat exists. const chat = await prisma.chat.findUnique({ where: { diff --git a/packages/web/src/app/api/(server)/ee/accountPermissionSyncJobStatus/api.ts b/packages/web/src/app/api/(server)/ee/accountPermissionSyncJobStatus/api.ts index 6a13fcadd..91074904d 100644 --- a/packages/web/src/app/api/(server)/ee/accountPermissionSyncJobStatus/api.ts +++ b/packages/web/src/app/api/(server)/ee/accountPermissionSyncJobStatus/api.ts @@ -1,16 +1,16 @@ 'use server'; import { ServiceError, notFound } from "@/lib/serviceError"; -import { withAuthV2 } from "@/withAuthV2"; +import { withAuth } from "@/middleware/withAuth"; import { AccountPermissionSyncJobStatus } from "@sourcebot/db"; -import { sew } from "@/actions"; +import { sew } from "@/middleware/sew"; export interface AccountSyncStatusResponse { isSyncing: boolean; } export const getAccountSyncStatus = async (jobId: string): Promise => - sew(() => withAuthV2(async ({ prisma, user }) => { + sew(() => withAuth(async ({ prisma, user }) => { const job = await prisma.accountPermissionSyncJob.findFirst({ where: { id: jobId, diff --git a/packages/web/src/app/api/(server)/ee/chat/[chatId]/searchMembers/route.ts b/packages/web/src/app/api/(server)/ee/chat/[chatId]/searchMembers/route.ts index 66a65fd31..c4cc7f245 100644 --- a/packages/web/src/app/api/(server)/ee/chat/[chatId]/searchMembers/route.ts +++ b/packages/web/src/app/api/(server)/ee/chat/[chatId]/searchMembers/route.ts @@ -3,7 +3,7 @@ import { SOURCEBOT_GUEST_USER_ID } from "@/lib/constants"; import { ErrorCode } from "@/lib/errorCodes"; import { notFound, queryParamsSchemaValidationError, serviceErrorResponse } from "@/lib/serviceError"; import { isServiceError } from "@/lib/utils"; -import { withAuthV2 } from "@/withAuthV2"; +import { withAuth } from "@/middleware/withAuth"; import { env, hasEntitlement } from "@sourcebot/shared"; import { StatusCodes } from "http-status-codes"; import { NextRequest } from "next/server"; @@ -68,7 +68,7 @@ export const GET = apiHandler(async ( const { query } = parsed.data; - const result = await withAuthV2(async ({ org, user, prisma }) => { + const result = await withAuth(async ({ org, user, prisma }) => { const chat = await prisma.chat.findUnique({ where: { id: chatId, diff --git a/packages/web/src/app/api/(server)/ee/permissionSyncStatus/api.ts b/packages/web/src/app/api/(server)/ee/permissionSyncStatus/api.ts index 29fbe256f..be23b2e92 100644 --- a/packages/web/src/app/api/(server)/ee/permissionSyncStatus/api.ts +++ b/packages/web/src/app/api/(server)/ee/permissionSyncStatus/api.ts @@ -1,12 +1,12 @@ 'use server'; import { ServiceError } from "@/lib/serviceError"; -import { withAuthV2 } from "@/withAuthV2"; +import { withAuth } from "@/middleware/withAuth"; import { env, getEntitlements } from "@sourcebot/shared"; import { AccountPermissionSyncJobStatus } from "@sourcebot/db"; import { StatusCodes } from "http-status-codes"; import { ErrorCode } from "@/lib/errorCodes"; -import { sew } from "@/actions"; +import { sew } from "@/middleware/sew"; export interface PermissionSyncStatusResponse { hasPendingFirstSync: boolean; @@ -17,7 +17,7 @@ export interface PermissionSyncStatusResponse { * synced for the first time. */ export const getPermissionSyncStatus = async (): Promise => sew(async () => - withAuthV2(async ({ prisma, user }) => { + withAuth(async ({ prisma, user }) => { const entitlements = getEntitlements(); if (!entitlements.includes('permission-syncing')) { return { diff --git a/packages/web/src/app/api/(server)/ee/user/route.ts b/packages/web/src/app/api/(server)/ee/user/route.ts index 79f3018c6..ea7789663 100644 --- a/packages/web/src/app/api/(server)/ee/user/route.ts +++ b/packages/web/src/app/api/(server)/ee/user/route.ts @@ -5,7 +5,8 @@ import { apiHandler } from "@/lib/apiHandler"; import { ErrorCode } from "@/lib/errorCodes"; import { serviceErrorResponse, missingQueryParam, notFound } from "@/lib/serviceError"; import { isServiceError } from "@/lib/utils"; -import { withAuthV2, withMinimumOrgRole } from "@/withAuthV2"; +import { withAuth } from "@/middleware/withAuth"; +import { withMinimumOrgRole } from "@/middleware/withMinimumOrgRole"; import { OrgRole } from "@sourcebot/db"; import { createLogger, hasEntitlement } from "@sourcebot/shared"; import { StatusCodes } from "http-status-codes"; @@ -30,7 +31,7 @@ export const GET = apiHandler(async (request: NextRequest) => { return serviceErrorResponse(missingQueryParam('userId')); } - const result = await withAuthV2(async ({ org, role, user, prisma }) => { + const result = await withAuth(async ({ org, role, user, prisma }) => { return withMinimumOrgRole(role, OrgRole.OWNER, async () => { try { const userData = await prisma.user.findUnique({ @@ -85,7 +86,7 @@ export const DELETE = apiHandler(async (request: NextRequest) => { return serviceErrorResponse(missingQueryParam('userId')); } - const result = await withAuthV2(async ({ org, role, user: currentUser, prisma }) => { + const result = await withAuth(async ({ org, role, user: currentUser, prisma }) => { return withMinimumOrgRole(role, OrgRole.OWNER, async () => { try { if (currentUser.id === userId) { diff --git a/packages/web/src/app/api/(server)/ee/users/route.ts b/packages/web/src/app/api/(server)/ee/users/route.ts index cc9da236f..eb52232f3 100644 --- a/packages/web/src/app/api/(server)/ee/users/route.ts +++ b/packages/web/src/app/api/(server)/ee/users/route.ts @@ -4,7 +4,8 @@ import { getAuditService } from "@/ee/features/audit/factory"; import { apiHandler } from "@/lib/apiHandler"; import { serviceErrorResponse } from "@/lib/serviceError"; import { isServiceError } from "@/lib/utils"; -import { withAuthV2, withMinimumOrgRole } from "@/withAuthV2"; +import { withAuth } from "@/middleware/withAuth"; +import { withMinimumOrgRole } from "@/middleware/withMinimumOrgRole"; import { OrgRole } from "@sourcebot/db"; import { createLogger, hasEntitlement } from "@sourcebot/shared"; import { StatusCodes } from "http-status-codes"; @@ -22,7 +23,7 @@ export const GET = apiHandler(async () => { }); } - const result = await withAuthV2(async ({ prisma, org, role, user }) => { + const result = await withAuth(async ({ prisma, org, role, user }) => { return withMinimumOrgRole(role, OrgRole.OWNER, async () => { try { const memberships = await prisma.userToOrg.findMany({ diff --git a/packages/web/src/app/api/(server)/mcp/route.ts b/packages/web/src/app/api/(server)/mcp/route.ts index bf820d1b7..a4225e20e 100644 --- a/packages/web/src/app/api/(server)/mcp/route.ts +++ b/packages/web/src/app/api/(server)/mcp/route.ts @@ -1,13 +1,13 @@ import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { createMcpServer } from '@/features/mcp/server'; -import { withOptionalAuthV2 } from '@/withAuthV2'; +import { withOptionalAuth } from '@/middleware/withAuth'; import { isServiceError } from '@/lib/utils'; import { notAuthenticated, serviceErrorResponse, ServiceError } from '@/lib/serviceError'; import { ErrorCode } from '@/lib/errorCodes'; import { StatusCodes } from 'http-status-codes'; import { NextRequest } from 'next/server'; -import { sew } from '@/actions'; +import { sew } from "@/middleware/sew"; import { apiHandler } from '@/lib/apiHandler'; import { env, hasEntitlement } from '@sourcebot/shared'; @@ -43,7 +43,7 @@ const sessions = new Map(); export const POST = apiHandler(async (request: NextRequest) => { const response = await sew(() => - withOptionalAuthV2(async ({ user }) => { + withOptionalAuth(async ({ user }) => { if (env.EXPERIMENT_ASK_GH_ENABLED === 'true' && !user) { return notAuthenticated(); } @@ -95,7 +95,7 @@ export const POST = apiHandler(async (request: NextRequest) => { export const DELETE = apiHandler(async (request: NextRequest) => { const result = await sew(() => - withOptionalAuthV2(async ({ user }) => { + withOptionalAuth(async ({ user }) => { if (env.EXPERIMENT_ASK_GH_ENABLED === 'true' && !user) { return notAuthenticated(); } diff --git a/packages/web/src/app/api/(server)/models/route.ts b/packages/web/src/app/api/(server)/models/route.ts index 1668ed846..4cba5d9d3 100644 --- a/packages/web/src/app/api/(server)/models/route.ts +++ b/packages/web/src/app/api/(server)/models/route.ts @@ -1,13 +1,13 @@ -import { sew } from "@/actions"; +import { sew } from "@/middleware/sew"; import { getConfiguredLanguageModelsInfo } from "@/features/chat/utils.server"; import { apiHandler } from "@/lib/apiHandler"; import { serviceErrorResponse } from "@/lib/serviceError"; import { isServiceError } from "@/lib/utils"; -import { withOptionalAuthV2 } from "@/withAuthV2"; +import { withOptionalAuth } from "@/middleware/withAuth"; export const GET = apiHandler(async () => { const response = await sew(() => - withOptionalAuthV2(async () => { + withOptionalAuth(async () => { const models = await getConfiguredLanguageModelsInfo(); return models; }) diff --git a/packages/web/src/app/api/(server)/repos/listReposApi.ts b/packages/web/src/app/api/(server)/repos/listReposApi.ts index cfb6e6c29..413feccec 100644 --- a/packages/web/src/app/api/(server)/repos/listReposApi.ts +++ b/packages/web/src/app/api/(server)/repos/listReposApi.ts @@ -1,13 +1,13 @@ -import { sew } from "@/actions"; +import { sew } from "@/middleware/sew"; import { getAuditService } from "@/ee/features/audit/factory"; import { ListReposQueryParams, RepositoryQuery } from "@/lib/types"; -import { withOptionalAuthV2 } from "@/withAuthV2"; +import { withOptionalAuth } from "@/middleware/withAuth"; import { getBrowsePath } from "@/app/[domain]/browse/hooks/utils"; import { env } from "@sourcebot/shared"; import { headers } from "next/headers"; export const listRepos = async ({ query, page, perPage, sort, direction, source }: ListReposQueryParams & { source?: string }) => sew(() => - withOptionalAuthV2(async ({ org, prisma, user }) => { + withOptionalAuth(async ({ org, prisma, user }) => { if (user) { const resolvedSource = source ?? (await headers()).get('X-Sourcebot-Client-Source') ?? undefined; getAuditService().createAudit({ diff --git a/packages/web/src/app/invite/actions.ts b/packages/web/src/app/invite/actions.ts index c25fd84f7..418a65530 100644 --- a/packages/web/src/app/invite/actions.ts +++ b/packages/web/src/app/invite/actions.ts @@ -2,14 +2,14 @@ import { isServiceError } from "@/lib/utils"; import { orgNotFound, ServiceError } from "@/lib/serviceError"; -import { sew } from "@/actions"; +import { sew } from "@/middleware/sew"; import { addUserToOrganization } from "@/lib/authUtils"; -import { withAuthV2_skipOrgMembershipCheck } from "@/withAuthV2"; +import { withAuth_skipOrgMembershipCheck } from "@/middleware/withAuth"; import { StatusCodes } from "http-status-codes"; import { ErrorCode } from "@/lib/errorCodes"; export const joinOrganization = async (orgId: number, inviteLinkId?: string) => sew(async () => - withAuthV2_skipOrgMembershipCheck(async ({ user, prisma }) => { + withAuth_skipOrgMembershipCheck(async ({ user, prisma }) => { const org = await prisma.org.findUnique({ where: { id: orgId, diff --git a/packages/web/src/ee/features/analytics/actions.ts b/packages/web/src/ee/features/analytics/actions.ts index 624bf4927..de49791da 100644 --- a/packages/web/src/ee/features/analytics/actions.ts +++ b/packages/web/src/ee/features/analytics/actions.ts @@ -1,7 +1,8 @@ 'use server'; -import { sew } from "@/actions"; -import { withAuthV2, withMinimumOrgRole } from "@/withAuthV2"; +import { sew } from "@/middleware/sew"; +import { withAuth } from "@/middleware/withAuth"; +import { withMinimumOrgRole } from "@/middleware/withMinimumOrgRole"; import { ServiceError } from "@/lib/serviceError"; import { AnalyticsResponse, AnalyticsRow } from "./types"; import { env, hasEntitlement } from "@sourcebot/shared"; @@ -10,7 +11,7 @@ import { StatusCodes } from "http-status-codes"; import { OrgRole } from "@sourcebot/db"; export const getAnalytics = async (): Promise => sew(() => - withAuthV2(async ({ org, role, prisma }) => + withAuth(async ({ org, role, prisma }) => withMinimumOrgRole(role, OrgRole.OWNER, async () => { if (!hasEntitlement("analytics")) { return { diff --git a/packages/web/src/ee/features/audit/actions.ts b/packages/web/src/ee/features/audit/actions.ts index 4e594d8f4..f2781ac3b 100644 --- a/packages/web/src/ee/features/audit/actions.ts +++ b/packages/web/src/ee/features/audit/actions.ts @@ -1,11 +1,12 @@ "use server"; -import { sew } from "@/actions"; +import { sew } from "@/middleware/sew"; import { getAuditService } from "@/ee/features/audit/factory"; import { ErrorCode } from "@/lib/errorCodes"; import { ServiceError } from "@/lib/serviceError"; import { prisma } from "@/prisma"; -import { withAuthV2, withMinimumOrgRole } from "@/withAuthV2"; +import { withAuth } from "@/middleware/withAuth"; +import { withMinimumOrgRole } from "@/middleware/withMinimumOrgRole"; import { createLogger } from "@sourcebot/shared"; import { StatusCodes } from "http-status-codes"; import { AuditEvent } from "./types"; @@ -15,7 +16,7 @@ const auditService = getAuditService(); const logger = createLogger('audit-utils'); export const createAuditAction = async (event: Omit) => sew(async () => - withAuthV2(async ({ user, org }) => { + withAuth(async ({ user, org }) => { await auditService.createAudit({ ...event, orgId: org.id, @@ -33,7 +34,7 @@ export interface FetchAuditRecordsParams { } export const fetchAuditRecords = async (params: FetchAuditRecordsParams) => sew(() => - withAuthV2(async ({ user, org, role }) => + withAuth(async ({ user, org, role }) => withMinimumOrgRole(role, OrgRole.OWNER, async () => { try { const where = { diff --git a/packages/web/src/ee/features/oauth/actions.ts b/packages/web/src/ee/features/oauth/actions.ts index e73c1e66a..351b4b84c 100644 --- a/packages/web/src/ee/features/oauth/actions.ts +++ b/packages/web/src/ee/features/oauth/actions.ts @@ -1,8 +1,8 @@ 'use server'; -import { sew } from '@/actions'; +import { sew } from "@/middleware/sew"; import { generateAndStoreAuthCode } from '@/ee/features/oauth/server'; -import { withAuthV2 } from '@/withAuthV2'; +import { withAuth } from '@/middleware/withAuth'; /** * Resolves the final URL to navigate to after an authorization decision. @@ -33,7 +33,7 @@ export const approveAuthorization = async ({ resource: string | null; state: string | undefined; }) => sew(() => - withAuthV2(async ({ user }) => { + withAuth(async ({ user }) => { const rawCode = await generateAndStoreAuthCode({ clientId, userId: user.id, @@ -59,7 +59,7 @@ export const denyAuthorization = async ({ redirectUri: string; state: string | undefined; }) => sew(() => - withAuthV2(async () => { + withAuth(async () => { const callbackUrl = new URL(redirectUri); callbackUrl.searchParams.set('error', 'access_denied'); callbackUrl.searchParams.set('error_description', 'The user denied the authorization request.'); diff --git a/packages/web/src/ee/features/sso/actions.ts b/packages/web/src/ee/features/sso/actions.ts index adb3d0fa8..4c71ade14 100644 --- a/packages/web/src/ee/features/sso/actions.ts +++ b/packages/web/src/ee/features/sso/actions.ts @@ -1,8 +1,9 @@ 'use server'; -import { sew } from "@/actions"; +import { sew } from "@/middleware/sew"; import { OPTIONAL_PROVIDERS_LINK_SKIPPED_COOKIE_NAME } from "@/lib/constants"; -import { withAuthV2, withMinimumOrgRole } from "@/withAuthV2"; +import { withAuth } from "@/middleware/withAuth"; +import { withMinimumOrgRole } from "@/middleware/withMinimumOrgRole"; import { OrgRole } from "@sourcebot/db"; import { createLogger, env, hasEntitlement, IdentityProviderType, loadConfig, PERMISSION_SYNC_SUPPORTED_IDENTITY_PROVIDERS } from "@sourcebot/shared"; import { cookies } from "next/headers"; @@ -24,7 +25,7 @@ export type LinkedAccount = { }; export const getLinkedAccounts = async () => sew(() => - withAuthV2(async ({ prisma, role, user }) => + withAuth(async ({ prisma, role, user }) => withMinimumOrgRole(role, OrgRole.MEMBER, async () => { const accounts = await prisma.account.findMany({ where: { userId: user.id }, @@ -83,7 +84,7 @@ export const getLinkedAccounts = async () => sew(() => export const unlinkLinkedAccountProvider = async (provider: string) => sew(() => - withAuthV2(async ({ prisma, role, user }) => + withAuth(async ({ prisma, role, user }) => withMinimumOrgRole(role, OrgRole.MEMBER, async () => { const result = await prisma.account.deleteMany({ where: { diff --git a/packages/web/src/ee/features/userManagement/actions.ts b/packages/web/src/ee/features/userManagement/actions.ts index c041cd958..b08dd1dde 100644 --- a/packages/web/src/ee/features/userManagement/actions.ts +++ b/packages/web/src/ee/features/userManagement/actions.ts @@ -1,11 +1,12 @@ 'use server'; -import { sew } from "@/actions"; +import { sew } from "@/middleware/sew"; import { getAuditService } from "@/ee/features/audit/factory"; import { ErrorCode } from "@/lib/errorCodes"; import { notFound, ServiceError } from "@/lib/serviceError"; import { prisma } from "@/prisma"; -import { withAuthV2, withMinimumOrgRole } from "@/withAuthV2"; +import { withAuth } from "@/middleware/withAuth"; +import { withMinimumOrgRole } from "@/middleware/withMinimumOrgRole"; import { OrgRole, Prisma } from "@sourcebot/db"; import { hasEntitlement } from "@sourcebot/shared"; import { StatusCodes } from "http-status-codes"; @@ -19,7 +20,7 @@ const orgManagementNotAvailable = (): ServiceError => ({ }); export const promoteToOwner = async (memberId: string): Promise<{ success: boolean } | ServiceError> => sew(() => - withAuthV2(async ({ user, org, role }) => + withAuth(async ({ user, org, role }) => withMinimumOrgRole(role, OrgRole.OWNER, async () => { if (!hasEntitlement('org-management')) { return orgManagementNotAvailable(); @@ -81,7 +82,7 @@ export const promoteToOwner = async (memberId: string): Promise<{ success: boole ); export const demoteToMember = async (memberId: string): Promise<{ success: boolean } | ServiceError> => sew(() => - withAuthV2(async ({ user, org, role }) => + withAuth(async ({ user, org, role }) => withMinimumOrgRole(role, OrgRole.OWNER, async () => { if (!hasEntitlement('org-management')) { return orgManagementNotAvailable(); diff --git a/packages/web/src/features/chat/actions.ts b/packages/web/src/features/chat/actions.ts index c1b5aec48..3762e88c3 100644 --- a/packages/web/src/features/chat/actions.ts +++ b/packages/web/src/features/chat/actions.ts @@ -1,12 +1,12 @@ 'use server'; -import { sew } from "@/actions"; +import { sew } from "@/middleware/sew"; import { getAuditService } from "@/ee/features/audit/factory"; import { getAnonymousId, getOrCreateAnonymousId } from "@/lib/anonymousId"; import { ErrorCode } from "@/lib/errorCodes"; import { captureEvent } from "@/lib/posthog"; import { notFound, ServiceError } from "@/lib/serviceError"; -import { withAuthV2, withOptionalAuthV2 } from "@/withAuthV2"; +import { withAuth, withOptionalAuth } from "@/middleware/withAuth"; import { ChatVisibility, Prisma } from "@sourcebot/db"; import { env } from "@sourcebot/shared"; import { StatusCodes } from "http-status-codes"; @@ -16,7 +16,7 @@ import { generateChatNameFromMessage, getConfiguredLanguageModels, isChatSharedW const auditService = getAuditService(); export const createChat = async ({ source }: { source?: string } = {}) => sew(() => - withOptionalAuthV2(async ({ org, user, prisma }) => { + withOptionalAuth(async ({ org, user, prisma }) => { const isGuestUser = user === undefined; // For anonymous users, get or create an anonymous ID to track ownership @@ -62,7 +62,7 @@ export const createChat = async ({ source }: { source?: string } = {}) => sew(() ); export const getChatInfo = async ({ chatId }: { chatId: string }) => sew(() => - withOptionalAuthV2(async ({ org, user, prisma }) => { + withOptionalAuth(async ({ org, user, prisma }) => { const chat = await prisma.chat.findUnique({ where: { id: chatId, @@ -93,7 +93,7 @@ export const getChatInfo = async ({ chatId }: { chatId: string }) => sew(() => ); export const getUserChatHistory = async () => sew(() => - withAuthV2(async ({ org, user, prisma }) => { + withAuth(async ({ org, user, prisma }) => { const chats = await prisma.chat.findMany({ where: { orgId: org.id, @@ -114,7 +114,7 @@ export const getUserChatHistory = async () => sew(() => ); export const updateChatName = async ({ chatId, name }: { chatId: string, name: string }) => sew(() => - withOptionalAuthV2(async ({ org, user, prisma }) => { + withOptionalAuth(async ({ org, user, prisma }) => { const chat = await prisma.chat.findUnique({ where: { id: chatId, @@ -150,7 +150,7 @@ export const updateChatName = async ({ chatId, name }: { chatId: string, name: s ); export const updateChatVisibility = async ({ chatId, visibility }: { chatId: string, visibility: ChatVisibility }) => sew(() => - withAuthV2(async ({ org, user, prisma }) => { + withAuth(async ({ org, user, prisma }) => { const chat = await prisma.chat.findUnique({ where: { id: chatId, @@ -192,7 +192,7 @@ export const updateChatVisibility = async ({ chatId, visibility }: { chatId: str ); export const generateAndUpdateChatNameFromMessage = async ({ chatId, languageModelId, message }: { chatId: string, languageModelId: string, message: string }) => sew(() => - withOptionalAuthV2(async ({ prisma, user, org }) => { + withOptionalAuth(async ({ prisma, user, org }) => { const chat = await prisma.chat.findUnique({ where: { id: chatId, @@ -240,7 +240,7 @@ export const generateAndUpdateChatNameFromMessage = async ({ chatId, languageMod ) export const deleteChat = async ({ chatId }: { chatId: string }) => sew(() => - withAuthV2(async ({ org, user, prisma }) => { + withAuth(async ({ org, user, prisma }) => { const chat = await prisma.chat.findUnique({ where: { id: chatId, @@ -283,7 +283,7 @@ export const deleteChat = async ({ chatId }: { chatId: string }) => sew(() => * Visibility is preserved so shared links continue to work. */ export const claimAnonymousChats = async () => sew(() => - withAuthV2(async ({ org, user, prisma }) => { + withAuth(async ({ org, user, prisma }) => { const anonymousId = await getAnonymousId(); if (!anonymousId) { @@ -317,7 +317,7 @@ export const claimAnonymousChats = async () => sew(() => * The new chat will be owned by the current user (authenticated or anonymous). */ export const duplicateChat = async ({ chatId, newName }: { chatId: string, newName: string }) => sew(() => - withOptionalAuthV2(async ({ org, user, prisma }) => { + withOptionalAuth(async ({ org, user, prisma }) => { const originalChat = await prisma.chat.findUnique({ where: { id: chatId, @@ -360,7 +360,7 @@ export const duplicateChat = async ({ chatId, newName }: { chatId: string, newNa * Returns the users that have been explicitly shared access to a chat. */ export const getSharedWithUsersForChat = async ({ chatId }: { chatId: string }) => sew(() => - withAuthV2(async ({ org, user, prisma }) => { + withAuth(async ({ org, user, prisma }) => { const chat = await prisma.chat.findUnique({ where: { id: chatId, @@ -399,7 +399,7 @@ export const getSharedWithUsersForChat = async ({ chatId }: { chatId: string }) * Shares the chat with a list of users. */ export const shareChatWithUsers = async ({ chatId, userIds }: { chatId: string, userIds: string[] }) => sew(() => - withAuthV2(async ({ org, user, prisma }) => { + withAuth(async ({ org, user, prisma }) => { const chat = await prisma.chat.findUnique({ where: { id: chatId, @@ -454,7 +454,7 @@ export const shareChatWithUsers = async ({ chatId, userIds }: { chatId: string, * Revokes access to a chat for a particular user. */ export const unshareChatWithUser = async ({ chatId, userId }: { chatId: string, userId: string }) => sew(() => - withAuthV2(async ({ org, user, prisma }) => { + withAuth(async ({ org, user, prisma }) => { const chat = await prisma.chat.findUnique({ where: { id: chatId, @@ -499,7 +499,7 @@ export const submitFeedback = async ({ messageId: string, feedbackType: 'like' | 'dislike' }) => sew(() => - withOptionalAuthV2(async ({ org, user, prisma }) => { + withOptionalAuth(async ({ org, user, prisma }) => { const chat = await prisma.chat.findUnique({ where: { id: chatId, diff --git a/packages/web/src/features/codeNav/api.ts b/packages/web/src/features/codeNav/api.ts index fe7a44e54..d45004795 100644 --- a/packages/web/src/features/codeNav/api.ts +++ b/packages/web/src/features/codeNav/api.ts @@ -1,10 +1,10 @@ import 'server-only'; -import { sew } from "@/actions"; +import { sew } from "@/middleware/sew"; import { search } from "@/features/search"; import { ServiceError } from "@/lib/serviceError"; import { isServiceError } from "@/lib/utils"; -import { withOptionalAuthV2 } from "@/withAuthV2"; +import { withOptionalAuth } from "@/middleware/withAuth"; import { SearchResponse } from "../search/types"; import { FindRelatedSymbolsRequest, FindRelatedSymbolsResponse } from "./types"; import { QueryIR } from '../search/ir'; @@ -14,7 +14,7 @@ import escapeStringRegexp from "escape-string-regexp"; const MAX_REFERENCE_COUNT = 1000; export const findSearchBasedSymbolReferences = async (props: FindRelatedSymbolsRequest): Promise => sew(() => - withOptionalAuthV2(async () => { + withOptionalAuth(async () => { const { symbolName, language, @@ -68,7 +68,7 @@ export const findSearchBasedSymbolReferences = async (props: FindRelatedSymbolsR export const findSearchBasedSymbolDefinitions = async (props: FindRelatedSymbolsRequest): Promise => sew(() => - withOptionalAuthV2(async () => { + withOptionalAuth(async () => { const { symbolName, language, diff --git a/packages/web/src/features/git/getDiffApi.ts b/packages/web/src/features/git/getDiffApi.ts index 6031724dd..9913b497a 100644 --- a/packages/web/src/features/git/getDiffApi.ts +++ b/packages/web/src/features/git/getDiffApi.ts @@ -1,6 +1,6 @@ -import { sew } from '@/actions'; +import { sew } from "@/middleware/sew"; import { invalidGitRef, notFound, ServiceError, unexpectedError } from '@/lib/serviceError'; -import { withOptionalAuthV2 } from '@/withAuthV2'; +import { withOptionalAuth } from '@/middleware/withAuth'; import { getRepoPath } from '@sourcebot/shared'; import parseDiff from 'parse-diff'; import { simpleGit } from 'simple-git'; @@ -39,7 +39,7 @@ export const getDiff = async ({ base, head, }: GetDiffRequest): Promise => sew(() => - withOptionalAuthV2(async ({ org, prisma }) => { + withOptionalAuth(async ({ org, prisma }) => { if (!isGitRefValid(base)) { return invalidGitRef(base); } diff --git a/packages/web/src/features/git/getFileSourceApi.ts b/packages/web/src/features/git/getFileSourceApi.ts index 2f4fcb8f0..0b0988eeb 100644 --- a/packages/web/src/features/git/getFileSourceApi.ts +++ b/packages/web/src/features/git/getFileSourceApi.ts @@ -1,4 +1,4 @@ -import { sew } from '@/actions'; +import { sew } from "@/middleware/sew"; import { getBrowsePath } from '@/app/[domain]/browse/hooks/utils'; import { getAuditService } from '@/ee/features/audit/factory'; import { SINGLE_TENANT_ORG_DOMAIN } from '@/lib/constants'; @@ -6,7 +6,7 @@ import { parseGitAttributes, resolveLanguageFromGitAttributes } from '@/lib/gita import { detectLanguageFromFilename } from '@/lib/languageDetection'; import { ServiceError, notFound, fileNotFound, invalidGitRef, unexpectedError } from '@/lib/serviceError'; import { getCodeHostBrowseFileAtBranchUrl } from '@/lib/utils'; -import { withOptionalAuthV2 } from '@/withAuthV2'; +import { withOptionalAuth } from '@/middleware/withAuth'; import { env, getRepoPath } from '@sourcebot/shared'; import { headers } from 'next/headers'; import simpleGit from 'simple-git'; @@ -18,7 +18,7 @@ export { fileSourceRequestSchema, fileSourceResponseSchema } from './schemas'; export type FileSourceRequest = z.infer; export type FileSourceResponse = z.infer; -export const getFileSource = async ({ path: filePath, repo: repoName, ref }: FileSourceRequest, { source }: { source?: string } = {}): Promise => sew(() => withOptionalAuthV2(async ({ org, prisma, user }) => { +export const getFileSource = async ({ path: filePath, repo: repoName, ref }: FileSourceRequest, { source }: { source?: string } = {}): Promise => sew(() => withOptionalAuth(async ({ org, prisma, user }) => { if (user) { const resolvedSource = source ?? (await headers()).get('X-Sourcebot-Client-Source') ?? undefined; getAuditService().createAudit({ diff --git a/packages/web/src/features/git/getFilesApi.ts b/packages/web/src/features/git/getFilesApi.ts index 7c6e4b4a9..2449a1fa3 100644 --- a/packages/web/src/features/git/getFilesApi.ts +++ b/packages/web/src/features/git/getFilesApi.ts @@ -1,7 +1,7 @@ -import { sew } from '@/actions'; +import { sew } from "@/middleware/sew"; import { FileTreeItem } from "./types"; import { notFound, ServiceError, unexpectedError } from '@/lib/serviceError'; -import { withOptionalAuthV2 } from "@/withAuthV2"; +import { withOptionalAuth } from "@/middleware/withAuth"; import { getRepoPath } from '@sourcebot/shared'; import simpleGit from 'simple-git'; import type z from 'zod'; @@ -13,7 +13,7 @@ export type GetFilesRequest = z.infer; export type GetFilesResponse = z.infer; export const getFiles = async ({ repoName, revisionName }: GetFilesRequest): Promise => sew(() => - withOptionalAuthV2(async ({ org, prisma }) => { + withOptionalAuth(async ({ org, prisma }) => { const repo = await prisma.repo.findFirst({ where: { name: repoName, diff --git a/packages/web/src/features/git/getFolderContentsApi.ts b/packages/web/src/features/git/getFolderContentsApi.ts index 335751b59..2dba88b25 100644 --- a/packages/web/src/features/git/getFolderContentsApi.ts +++ b/packages/web/src/features/git/getFolderContentsApi.ts @@ -1,7 +1,7 @@ -import { sew } from '@/actions'; +import { sew } from "@/middleware/sew"; import { FileTreeItem } from "./types"; import { notFound, unexpectedError } from '@/lib/serviceError'; -import { withOptionalAuthV2 } from "@/withAuthV2"; +import { withOptionalAuth } from "@/middleware/withAuth"; import { getRepoPath } from '@sourcebot/shared'; import simpleGit from 'simple-git'; import z from 'zod'; @@ -19,7 +19,7 @@ export type GetFolderContentsRequest = z.infer sew(() => - withOptionalAuthV2(async ({ org, prisma }) => { + withOptionalAuth(async ({ org, prisma }) => { const repo = await prisma.repo.findFirst({ where: { name: repoName, diff --git a/packages/web/src/features/git/getTreeApi.ts b/packages/web/src/features/git/getTreeApi.ts index 887379d19..cc66f958d 100644 --- a/packages/web/src/features/git/getTreeApi.ts +++ b/packages/web/src/features/git/getTreeApi.ts @@ -1,7 +1,7 @@ -import { sew } from '@/actions'; +import { sew } from "@/middleware/sew"; import { getAuditService } from '@/ee/features/audit/factory'; import { invalidGitRef, notFound, ServiceError, unexpectedError } from '@/lib/serviceError'; -import { withOptionalAuthV2 } from "@/withAuthV2"; +import { withOptionalAuth } from "@/middleware/withAuth"; import { getRepoPath } from '@sourcebot/shared'; import { headers } from 'next/headers'; import simpleGit from 'simple-git'; @@ -19,7 +19,7 @@ export type GetTreeResponse = z.infer; * into a single tree. */ export const getTree = async ({ repoName, revisionName, paths }: GetTreeRequest, { source }: { source?: string } = {}): Promise => sew(() => - withOptionalAuthV2(async ({ org, prisma, user }) => { + withOptionalAuth(async ({ org, prisma, user }) => { if (user) { const resolvedSource = source ?? (await headers()).get('X-Sourcebot-Client-Source') ?? undefined; getAuditService().createAudit({ diff --git a/packages/web/src/features/git/listCommitsApi.ts b/packages/web/src/features/git/listCommitsApi.ts index a57128058..0e8bf113c 100644 --- a/packages/web/src/features/git/listCommitsApi.ts +++ b/packages/web/src/features/git/listCommitsApi.ts @@ -1,6 +1,6 @@ -import { sew } from '@/actions'; +import { sew } from "@/middleware/sew"; import { invalidGitRef, notFound, ServiceError, unexpectedError } from '@/lib/serviceError'; -import { withOptionalAuthV2 } from '@/withAuthV2'; +import { withOptionalAuth } from '@/middleware/withAuth'; import { getRepoPath } from '@sourcebot/shared'; import { z } from 'zod'; import { simpleGit } from 'simple-git'; @@ -45,7 +45,7 @@ export const listCommits = async ({ maxCount = 50, skip = 0, }: ListCommitsRequest): Promise => sew(() => - withOptionalAuthV2(async ({ org, prisma }) => { + withOptionalAuth(async ({ org, prisma }) => { const repo = await prisma.repo.findFirst({ where: { name: repoName, diff --git a/packages/web/src/features/mcp/askCodebase.ts b/packages/web/src/features/mcp/askCodebase.ts index 6ee6110da..059eef3c7 100644 --- a/packages/web/src/features/mcp/askCodebase.ts +++ b/packages/web/src/features/mcp/askCodebase.ts @@ -1,10 +1,10 @@ -import { sew } from "@/actions"; +import { sew } from "@/middleware/sew"; import { getConfiguredLanguageModels, getAISDKLanguageModelAndOptions, generateChatNameFromMessage, updateChatMessages } from "@/features/chat/utils.server"; import { LanguageModelInfo, SBChatMessage, SearchScope } from "@/features/chat/types"; import { convertLLMOutputToPortableMarkdown, getAnswerPartFromAssistantMessage, getLanguageModelKey } from "@/features/chat/utils"; import { ErrorCode } from "@/lib/errorCodes"; import { ServiceError, ServiceErrorException } from "@/lib/serviceError"; -import { withOptionalAuthV2 } from "@/withAuthV2"; +import { withOptionalAuth } from "@/middleware/withAuth"; import { ChatVisibility, Prisma } from "@sourcebot/db"; import { createLogger, env } from "@sourcebot/shared"; import { randomUUID } from "crypto"; @@ -43,7 +43,7 @@ const blockStreamUntilFinish = async => sew(() => - withOptionalAuthV2(async ({ org, user, prisma }) => { + withOptionalAuth(async ({ org, user, prisma }) => { const { query, repos = [], languageModel: requestedLanguageModel, visibility: requestedVisibility, source } = params; const configuredModels = await getConfiguredLanguageModels(); diff --git a/packages/web/src/features/search/searchApi.ts b/packages/web/src/features/search/searchApi.ts index b21f05c07..424c546af 100644 --- a/packages/web/src/features/search/searchApi.ts +++ b/packages/web/src/features/search/searchApi.ts @@ -1,7 +1,7 @@ -import { sew } from "@/actions"; +import { sew } from "@/middleware/sew"; import { getAuditService } from "@/ee/features/audit/factory"; import { getRepoPermissionFilterForUser } from "@/prisma"; -import { withOptionalAuthV2 } from "@/withAuthV2"; +import { withOptionalAuth } from "@/middleware/withAuth"; import { PrismaClient, UserWithAccounts } from "@sourcebot/db"; import { env, hasEntitlement } from "@sourcebot/shared"; import { headers } from "next/headers"; @@ -29,7 +29,7 @@ type QueryIRSearchRequest = { type SearchRequest = QueryStringSearchRequest | QueryIRSearchRequest; export const search = (request: SearchRequest) => sew(() => - withOptionalAuthV2(async ({ prisma, user, org }) => { + withOptionalAuth(async ({ prisma, user, org }) => { if (user) { const source = request.source ?? (await headers()).get('X-Sourcebot-Client-Source') ?? undefined; getAuditService().createAudit({ @@ -60,7 +60,7 @@ export const search = (request: SearchRequest) => sew(() => })); export const streamSearch = (request: SearchRequest) => sew(() => - withOptionalAuthV2(async ({ prisma, user, org }) => { + withOptionalAuth(async ({ prisma, user, org }) => { if (user) { const source = request.source ?? (await headers()).get('X-Sourcebot-Client-Source') ?? undefined; getAuditService().createAudit({ diff --git a/packages/web/src/features/searchAssist/actions.ts b/packages/web/src/features/searchAssist/actions.ts index 6db31a290..b9ebb8266 100644 --- a/packages/web/src/features/searchAssist/actions.ts +++ b/packages/web/src/features/searchAssist/actions.ts @@ -1,10 +1,10 @@ 'use server'; -import { sew } from "@/actions"; +import { sew } from "@/middleware/sew"; import { getConfiguredLanguageModels, getAISDKLanguageModelAndOptions } from "../chat/utils.server"; import { ErrorCode } from "@/lib/errorCodes"; import { ServiceError } from "@/lib/serviceError"; -import { withOptionalAuthV2 } from "@/withAuthV2"; +import { withOptionalAuth } from "@/middleware/withAuth"; import { SEARCH_SYNTAX_DESCRIPTION } from "@sourcebot/query-language"; import { generateObject } from "ai"; import { z } from "zod"; @@ -25,7 +25,7 @@ ${SEARCH_SYNTAX_DESCRIPTION} `; export const translateSearchQuery = async ({ prompt }: { prompt: string }) => sew(() => - withOptionalAuthV2(async () => { + withOptionalAuth(async () => { const models = await getConfiguredLanguageModels(); if (models.length === 0) { diff --git a/packages/web/src/features/userManagement/actions.ts b/packages/web/src/features/userManagement/actions.ts index 298ebfa69..f19546ad9 100644 --- a/packages/web/src/features/userManagement/actions.ts +++ b/packages/web/src/features/userManagement/actions.ts @@ -1,15 +1,16 @@ 'use server'; -import { sew } from "@/actions"; +import { sew } from "@/middleware/sew"; import { ErrorCode } from "@/lib/errorCodes"; import { notFound, ServiceError } from "@/lib/serviceError"; import { prisma } from "@/prisma"; -import { withAuthV2, withMinimumOrgRole } from "@/withAuthV2"; +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(() => - withAuthV2(async ({ org, role }) => + withAuth(async ({ org, role }) => withMinimumOrgRole(role, OrgRole.OWNER, async () => { const guardError = await prisma.$transaction(async (tx) => { const targetMember = await tx.userToOrg.findUnique({ @@ -63,7 +64,7 @@ export const removeMemberFromOrg = async (memberId: string): Promise<{ success: ); export const leaveOrg = async (): Promise<{ success: boolean } | ServiceError> => sew(() => - withAuthV2(async ({ user, org, role }) => { + withAuth(async ({ user, org, role }) => { const guardError = await prisma.$transaction(async (tx) => { if (role === OrgRole.OWNER) { const ownerCount = await tx.userToOrg.count({ diff --git a/packages/web/src/features/workerApi/actions.ts b/packages/web/src/features/workerApi/actions.ts index aa23790e0..dae456197 100644 --- a/packages/web/src/features/workerApi/actions.ts +++ b/packages/web/src/features/workerApi/actions.ts @@ -1,15 +1,16 @@ 'use server'; -import { sew } from "@/actions"; +import { sew } from "@/middleware/sew"; import { unexpectedError } from "@/lib/serviceError"; -import { withAuthV2, withMinimumOrgRole, withOptionalAuthV2 } from "@/withAuthV2"; +import { withAuth, withOptionalAuth } from "@/middleware/withAuth"; +import { withMinimumOrgRole } from "@/middleware/withMinimumOrgRole"; import { OrgRole } from "@sourcebot/db"; import z from "zod"; const WORKER_API_URL = 'http://localhost:3060'; export const syncConnection = async (connectionId: number) => sew(() => - withAuthV2(({ role }) => + withAuth(({ role }) => withMinimumOrgRole(role, OrgRole.OWNER, async () => { const response = await fetch(`${WORKER_API_URL}/api/sync-connection`, { method: 'POST', @@ -35,7 +36,7 @@ export const syncConnection = async (connectionId: number) => sew(() => ); export const indexRepo = async (repoId: number) => sew(() => - withAuthV2(({ role }) => + withAuth(({ role }) => withMinimumOrgRole(role, OrgRole.OWNER, async () => { const response = await fetch(`${WORKER_API_URL}/api/index-repo`, { method: 'POST', @@ -59,7 +60,7 @@ export const indexRepo = async (repoId: number) => sew(() => ); export const triggerAccountPermissionSync = async (accountId: string) => sew(() => - withAuthV2(({ role }) => + withAuth(({ role }) => withMinimumOrgRole(role, OrgRole.MEMBER, async () => { const response = await fetch(`${WORKER_API_URL}/api/trigger-account-permission-sync`, { method: 'POST', @@ -83,7 +84,7 @@ export const triggerAccountPermissionSync = async (accountId: string) => sew(() ); export const addGithubRepo = async (owner: string, repo: string) => sew(() => - withOptionalAuthV2(async () => { + withOptionalAuth(async () => { const response = await fetch(`${WORKER_API_URL}/api/experimental/add-github-repo`, { method: 'POST', body: JSON.stringify({ owner, repo }), diff --git a/packages/web/src/lib/posthog.ts b/packages/web/src/lib/posthog.ts index a63db4e26..33d335fb8 100644 --- a/packages/web/src/lib/posthog.ts +++ b/packages/web/src/lib/posthog.ts @@ -5,7 +5,7 @@ import * as Sentry from "@sentry/nextjs"; import { PosthogEvent, PosthogEventMap } from './posthogEvents'; import { cookies, headers } from 'next/headers'; import { auth } from '@/auth'; -import { getVerifiedApiObject } from '@/withAuthV2'; +import { getVerifiedApiObject } from '@/middleware/withAuth'; /** * @note: This is a subset of the properties stored in the diff --git a/packages/web/src/middleware/sew.ts b/packages/web/src/middleware/sew.ts new file mode 100644 index 000000000..8f48859f9 --- /dev/null +++ b/packages/web/src/middleware/sew.ts @@ -0,0 +1,27 @@ +import { ServiceError, ServiceErrorException, unexpectedError } from "@/lib/serviceError"; +import { createLogger } from "@sourcebot/shared"; +import * as Sentry from "@sentry/nextjs"; + +const logger = createLogger('sew'); + + +/** + * "Service Error Wrapper". + * + * Captures any thrown exceptions, logs them to the console and Sentry, + * and returns a generic unexpected service error. + */ +export const sew = async (fn: () => Promise): Promise => { + try { + return await fn(); + } catch (e) { + Sentry.captureException(e); + logger.error(e); + + if (e instanceof ServiceErrorException) { + return e.serviceError; + } + + return unexpectedError(`An unexpected error occurred. Please try again later.`); + } +}; diff --git a/packages/web/src/withAuthV2.test.ts b/packages/web/src/middleware/withAuth.test.ts similarity index 96% rename from packages/web/src/withAuthV2.test.ts rename to packages/web/src/middleware/withAuth.test.ts index 2eb5522e5..651667fc8 100644 --- a/packages/web/src/withAuthV2.test.ts +++ b/packages/web/src/middleware/withAuth.test.ts @@ -1,10 +1,10 @@ import { expect, test, vi, beforeEach, describe } from 'vitest'; import { Session } from 'next-auth'; -import { notAuthenticated } from './lib/serviceError'; -import { getAuthContext, getAuthenticatedUser, withAuthV2, withOptionalAuthV2 } from './withAuthV2'; -import { MOCK_API_KEY, MOCK_OAUTH_TOKEN, MOCK_ORG, MOCK_USER_WITH_ACCOUNTS, prisma } from './__mocks__/prisma'; +import { notAuthenticated } from '../lib/serviceError'; +import { getAuthContext, getAuthenticatedUser, withAuth, withOptionalAuth } from './withAuth'; +import { MOCK_API_KEY, MOCK_OAUTH_TOKEN, MOCK_ORG, MOCK_USER_WITH_ACCOUNTS, prisma } from '../__mocks__/prisma'; import { OrgRole } from '@sourcebot/db'; -import { ErrorCode } from './lib/errorCodes'; +import { ErrorCode } from '../lib/errorCodes'; import { StatusCodes } from 'http-status-codes'; const mocks = vi.hoisted(() => { @@ -17,7 +17,7 @@ const mocks = vi.hoisted(() => { } }); -vi.mock('./auth', () => ({ +vi.mock('../auth', () => ({ auth: mocks.auth, })); @@ -480,7 +480,7 @@ describe('withAuthV2', () => { setMockSession(createMockSession({ user: { id: 'test-user-id' } })); const cb = vi.fn(); - const result = await withAuthV2(cb); + const result = await withAuth(cb); expect(cb).toHaveBeenCalledWith({ user: { ...MOCK_USER_WITH_ACCOUNTS, @@ -510,7 +510,7 @@ describe('withAuthV2', () => { setMockSession(createMockSession({ user: { id: 'test-user-id' } })); const cb = vi.fn(); - const result = await withAuthV2(cb); + const result = await withAuth(cb); expect(cb).toHaveBeenCalledWith({ user: { ...MOCK_USER_WITH_ACCOUNTS, @@ -545,7 +545,7 @@ describe('withAuthV2', () => { setMockHeaders(new Headers({ 'X-Sourcebot-Api-Key': 'sourcebot-apikey' })); const cb = vi.fn(); - const result = await withAuthV2(cb); + const result = await withAuth(cb); expect(cb).toHaveBeenCalledWith({ user: { ...MOCK_USER_WITH_ACCOUNTS, @@ -580,7 +580,7 @@ describe('withAuthV2', () => { setMockHeaders(new Headers({ 'X-Sourcebot-Api-Key': 'sourcebot-apikey' })); const cb = vi.fn(); - const result = await withAuthV2(cb); + const result = await withAuth(cb); expect(cb).toHaveBeenCalledWith({ user: { ...MOCK_USER_WITH_ACCOUNTS, @@ -615,7 +615,7 @@ describe('withAuthV2', () => { setMockHeaders(new Headers({ 'Authorization': 'Bearer sourcebot-apikey' })); const cb = vi.fn(); - const result = await withAuthV2(cb); + const result = await withAuth(cb); expect(cb).toHaveBeenCalledWith({ user: { ...MOCK_USER_WITH_ACCOUNTS, @@ -650,7 +650,7 @@ describe('withAuthV2', () => { setMockHeaders(new Headers({ 'Authorization': 'Bearer sourcebot-apikey' })); const cb = vi.fn(); - const result = await withAuthV2(cb); + const result = await withAuth(cb); expect(cb).toHaveBeenCalledWith({ user: { ...MOCK_USER_WITH_ACCOUNTS, @@ -680,7 +680,7 @@ describe('withAuthV2', () => { setMockSession(null); const cb = vi.fn(); - const result = await withAuthV2(cb); + const result = await withAuth(cb); expect(cb).not.toHaveBeenCalled(); expect(result).toStrictEqual(notAuthenticated()); }); @@ -703,7 +703,7 @@ describe('withAuthV2', () => { setMockSession(createMockSession({ user: { id: 'test-user-id' } })); const cb = vi.fn(); - const result = await withAuthV2(cb); + const result = await withAuth(cb); expect(cb).not.toHaveBeenCalled(); expect(result).toStrictEqual(notAuthenticated()); }); @@ -722,7 +722,7 @@ describe('withAuthV2', () => { setMockSession(createMockSession({ user: { id: 'test-user-id' } })); const cb = vi.fn(); - const result = await withAuthV2(cb); + const result = await withAuth(cb); expect(cb).not.toHaveBeenCalled(); expect(result).toStrictEqual(notAuthenticated()); }); @@ -747,7 +747,7 @@ describe('withOptionalAuthV2', () => { setMockSession(createMockSession({ user: { id: 'test-user-id' } })); const cb = vi.fn(); - const result = await withOptionalAuthV2(cb); + const result = await withOptionalAuth(cb); expect(cb).toHaveBeenCalledWith({ user: { ...MOCK_USER_WITH_ACCOUNTS, @@ -777,7 +777,7 @@ describe('withOptionalAuthV2', () => { setMockSession(createMockSession({ user: { id: 'test-user-id' } })); const cb = vi.fn(); - const result = await withOptionalAuthV2(cb); + const result = await withOptionalAuth(cb); expect(cb).toHaveBeenCalledWith({ user: { ...MOCK_USER_WITH_ACCOUNTS, @@ -812,7 +812,7 @@ describe('withOptionalAuthV2', () => { setMockHeaders(new Headers({ 'X-Sourcebot-Api-Key': 'sourcebot-apikey' })); const cb = vi.fn(); - const result = await withOptionalAuthV2(cb); + const result = await withOptionalAuth(cb); expect(cb).toHaveBeenCalledWith({ user: { ...MOCK_USER_WITH_ACCOUNTS, @@ -847,7 +847,7 @@ describe('withOptionalAuthV2', () => { setMockHeaders(new Headers({ 'X-Sourcebot-Api-Key': 'sourcebot-apikey' })); const cb = vi.fn(); - const result = await withOptionalAuthV2(cb); + const result = await withOptionalAuth(cb); expect(cb).toHaveBeenCalledWith({ user: { ...MOCK_USER_WITH_ACCOUNTS, @@ -882,7 +882,7 @@ describe('withOptionalAuthV2', () => { setMockHeaders(new Headers({ 'Authorization': 'Bearer sourcebot-apikey' })); const cb = vi.fn(); - const result = await withOptionalAuthV2(cb); + const result = await withOptionalAuth(cb); expect(cb).toHaveBeenCalledWith({ user: { ...MOCK_USER_WITH_ACCOUNTS, @@ -917,7 +917,7 @@ describe('withOptionalAuthV2', () => { setMockHeaders(new Headers({ 'Authorization': 'Bearer sourcebot-apikey' })); const cb = vi.fn(); - const result = await withOptionalAuthV2(cb); + const result = await withOptionalAuth(cb); expect(cb).toHaveBeenCalledWith({ user: { ...MOCK_USER_WITH_ACCOUNTS, @@ -947,7 +947,7 @@ describe('withOptionalAuthV2', () => { setMockSession(null); const cb = vi.fn(); - const result = await withOptionalAuthV2(cb); + const result = await withOptionalAuth(cb); expect(cb).not.toHaveBeenCalled(); expect(result).toStrictEqual(notAuthenticated()); }); @@ -970,7 +970,7 @@ describe('withOptionalAuthV2', () => { setMockSession(createMockSession({ user: { id: 'test-user-id' } })); const cb = vi.fn(); - const result = await withOptionalAuthV2(cb); + const result = await withOptionalAuth(cb); expect(cb).not.toHaveBeenCalled(); expect(result).toStrictEqual(notAuthenticated()); }); @@ -989,7 +989,7 @@ describe('withOptionalAuthV2', () => { setMockSession(createMockSession({ user: { id: 'test-user-id' } })); const cb = vi.fn(); - const result = await withOptionalAuthV2(cb); + const result = await withOptionalAuth(cb); expect(cb).not.toHaveBeenCalled(); expect(result).toStrictEqual(notAuthenticated()); }); @@ -1011,7 +1011,7 @@ describe('withOptionalAuthV2', () => { setMockSession(createMockSession({ user: { id: 'test-user-id' } })); const cb = vi.fn(); - const result = await withOptionalAuthV2(cb); + const result = await withOptionalAuth(cb); expect(cb).toHaveBeenCalledWith({ user: { ...MOCK_USER_WITH_ACCOUNTS, @@ -1045,7 +1045,7 @@ describe('withOptionalAuthV2', () => { setMockSession(createMockSession({ user: { id: 'test-user-id' } })); const cb = vi.fn(); - const result = await withOptionalAuthV2(cb); + const result = await withOptionalAuth(cb); expect(cb).not.toHaveBeenCalled(); expect(result).toStrictEqual(notAuthenticated()); }); @@ -1067,7 +1067,7 @@ describe('withOptionalAuthV2', () => { setMockSession(createMockSession({ user: { id: 'test-user-id' } })); const cb = vi.fn(); - const result = await withOptionalAuthV2(cb); + const result = await withOptionalAuth(cb); expect(cb).not.toHaveBeenCalled(); expect(result).toStrictEqual(notAuthenticated()); }); diff --git a/packages/web/src/withAuthV2.ts b/packages/web/src/middleware/withAuth.ts similarity index 83% rename from packages/web/src/withAuthV2.ts rename to packages/web/src/middleware/withAuth.ts index 137cd4580..33e5be772 100644 --- a/packages/web/src/withAuthV2.ts +++ b/packages/web/src/middleware/withAuth.ts @@ -2,12 +2,12 @@ import { prisma as __unsafePrisma, userScopedPrismaClientExtension } from "@/pri 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"; -import { auth } from "./auth"; -import { notAuthenticated, notFound, ServiceError } from "./lib/serviceError"; -import { SINGLE_TENANT_ORG_ID } from "./lib/constants"; +import { auth } from "../auth"; +import { notAuthenticated, notFound, ServiceError } from "../lib/serviceError"; +import { SINGLE_TENANT_ORG_ID } from "../lib/constants"; import { StatusCodes } from "http-status-codes"; -import { ErrorCode } from "./lib/errorCodes"; -import { getOrgMetadata, isServiceError } from "./lib/utils"; +import { ErrorCode } from "../lib/errorCodes"; +import { getOrgMetadata, isServiceError } from "../lib/utils"; import { hasEntitlement } from "@sourcebot/shared"; type OptionalAuthContext = { @@ -29,7 +29,7 @@ type RequiredAuthContext = { * 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 withAuthV2_skipOrgMembershipCheck = async (fn: (params: Omit & { role: OrgRole; }) => Promise) => { +export const withAuth_skipOrgMembershipCheck = async (fn: (params: Omit & { role: OrgRole; }) => Promise) => { const authContext = await getAuthContext(); if (isServiceError(authContext)) { @@ -45,7 +45,7 @@ export const withAuthV2_skipOrgMembershipCheck = async (fn: (params: Omit(fn: (params: RequiredAuthContext) => Promise) => { +export const withAuth = async (fn: (params: RequiredAuthContext) => Promise) => { const authContext = await getAuthContext(); if (isServiceError(authContext)) { @@ -61,7 +61,7 @@ export const withAuthV2 = async (fn: (params: RequiredAuthContext) => Promise return fn({ user, org, role, prisma }); }; -export const withOptionalAuthV2 = async (fn: (params: OptionalAuthContext) => Promise) => { +export const withOptionalAuth = async (fn: (params: OptionalAuthContext) => Promise) => { const authContext = await getAuthContext(); if (isServiceError(authContext)) { return authContext; @@ -270,32 +270,4 @@ export const getVerifiedApiObject = async (apiKeyString: string): Promise( - userRole: OrgRole, - minRequiredRole: OrgRole = OrgRole.MEMBER, - fn: () => Promise, -): Promise => { - - const getAuthorizationPrecedence = (role: OrgRole): number => { - switch (role) { - case OrgRole.GUEST: - return 0; - case OrgRole.MEMBER: - return 1; - case OrgRole.OWNER: - return 2; - } - }; - - if ( - getAuthorizationPrecedence(userRole) < getAuthorizationPrecedence(minRequiredRole) - ) { - return { - statusCode: StatusCodes.FORBIDDEN, - errorCode: ErrorCode.INSUFFICIENT_PERMISSIONS, - message: "You do not have sufficient permissions to perform this action.", - } satisfies ServiceError; - } - return fn(); -} diff --git a/packages/web/src/middleware/withMinimumOrgRole.ts b/packages/web/src/middleware/withMinimumOrgRole.ts new file mode 100644 index 000000000..d55809170 --- /dev/null +++ b/packages/web/src/middleware/withMinimumOrgRole.ts @@ -0,0 +1,32 @@ +import { ErrorCode } from "@/lib/errorCodes"; +import { ServiceError } from "@/lib/serviceError"; +import { OrgRole } from "@sourcebot/db"; +import { StatusCodes } from "http-status-codes"; + +export const withMinimumOrgRole = async ( + userRole: OrgRole, + minRequiredRole: OrgRole = OrgRole.MEMBER, + fn: () => Promise +): Promise => { + + const getAuthorizationPrecedence = (role: OrgRole): number => { + switch (role) { + case OrgRole.GUEST: + return 0; + case OrgRole.MEMBER: + return 1; + case OrgRole.OWNER: + return 2; + } + }; + + if (getAuthorizationPrecedence(userRole) < getAuthorizationPrecedence(minRequiredRole)) { + return { + statusCode: StatusCodes.FORBIDDEN, + errorCode: ErrorCode.INSUFFICIENT_PERMISSIONS, + message: "You do not have sufficient permissions to perform this action.", + } satisfies ServiceError; + } + + return fn(); +}; From 22d92694a58864e9b9e9371dd3173820eec6bd48 Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Wed, 1 Apr 2026 18:59:11 -0700 Subject: [PATCH 2/4] fix(web): update mock paths in listCommitsApi.test.ts Update vi.mock paths from @/withAuthV2 to @/middleware/withAuth and from @/actions to @/middleware/sew to match the renamed modules. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/web/src/features/git/listCommitsApi.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/web/src/features/git/listCommitsApi.test.ts b/packages/web/src/features/git/listCommitsApi.test.ts index e0e9ffa93..72e743157 100644 --- a/packages/web/src/features/git/listCommitsApi.test.ts +++ b/packages/web/src/features/git/listCommitsApi.test.ts @@ -31,7 +31,7 @@ vi.mock('@/lib/serviceError', () => ({ message: `Invalid git reference: "${ref}". Git refs cannot start with '-'.`, }), })); -vi.mock('@/actions', () => ({ +vi.mock('@/middleware/sew', () => ({ sew: async (fn: () => Promise | T): Promise => { try { return await fn(); @@ -47,8 +47,8 @@ vi.mock('@/actions', () => ({ // Create a mock findFirst function that we can configure per-test const mockFindFirst = vi.fn(); -vi.mock('@/withAuthV2', () => ({ - withOptionalAuthV2: async (fn: (args: { org: { id: number; name: string }; prisma: unknown }) => Promise): Promise => { +vi.mock('@/middleware/withAuth', () => ({ + withOptionalAuth: async (fn: (args: { org: { id: number; name: string }; prisma: unknown }) => Promise): Promise => { // Mock withOptionalAuthV2 to provide org and prisma context const mockOrg = { id: 1, name: 'test-org' }; const mockPrisma = { From 088001ca77bc0eb02ad8eb620e515ff4836af8b0 Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Wed, 1 Apr 2026 19:02:50 -0700 Subject: [PATCH 3/4] fix(web): use auth context prisma instead of global import in audit and userManagement actions Replace direct @/prisma imports with the prisma instance from the withAuth callback to ensure userScopedPrismaClientExtension is applied. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/web/src/ee/features/audit/actions.ts | 3 +-- packages/web/src/ee/features/userManagement/actions.ts | 5 ++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/web/src/ee/features/audit/actions.ts b/packages/web/src/ee/features/audit/actions.ts index f2781ac3b..a1650a035 100644 --- a/packages/web/src/ee/features/audit/actions.ts +++ b/packages/web/src/ee/features/audit/actions.ts @@ -4,7 +4,6 @@ import { sew } from "@/middleware/sew"; import { getAuditService } from "@/ee/features/audit/factory"; import { ErrorCode } from "@/lib/errorCodes"; import { ServiceError } from "@/lib/serviceError"; -import { prisma } from "@/prisma"; import { withAuth } from "@/middleware/withAuth"; import { withMinimumOrgRole } from "@/middleware/withMinimumOrgRole"; import { createLogger } from "@sourcebot/shared"; @@ -34,7 +33,7 @@ export interface FetchAuditRecordsParams { } export const fetchAuditRecords = async (params: FetchAuditRecordsParams) => sew(() => - withAuth(async ({ user, org, role }) => + withAuth(async ({ user, org, role, prisma }) => withMinimumOrgRole(role, OrgRole.OWNER, async () => { try { const where = { diff --git a/packages/web/src/ee/features/userManagement/actions.ts b/packages/web/src/ee/features/userManagement/actions.ts index b08dd1dde..9945dbf92 100644 --- a/packages/web/src/ee/features/userManagement/actions.ts +++ b/packages/web/src/ee/features/userManagement/actions.ts @@ -4,7 +4,6 @@ import { sew } from "@/middleware/sew"; import { getAuditService } from "@/ee/features/audit/factory"; 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"; @@ -20,7 +19,7 @@ const orgManagementNotAvailable = (): ServiceError => ({ }); export const promoteToOwner = async (memberId: string): Promise<{ success: boolean } | ServiceError> => sew(() => - withAuth(async ({ user, org, role }) => + withAuth(async ({ user, org, role, prisma }) => withMinimumOrgRole(role, OrgRole.OWNER, async () => { if (!hasEntitlement('org-management')) { return orgManagementNotAvailable(); @@ -82,7 +81,7 @@ export const promoteToOwner = async (memberId: string): Promise<{ success: boole ); export const demoteToMember = async (memberId: string): Promise<{ success: boolean } | ServiceError> => sew(() => - withAuth(async ({ user, org, role }) => + withAuth(async ({ user, org, role, prisma }) => withMinimumOrgRole(role, OrgRole.OWNER, async () => { if (!hasEntitlement('org-management')) { return orgManagementNotAvailable(); From f64293303092d477a5a02d08ecfb7ea0b1cde460 Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Wed, 1 Apr 2026 19:47:29 -0700 Subject: [PATCH 4/4] feedback --- CLAUDE.md | 20 +++++++++---------- .../src/features/git/listCommitsApi.test.ts | 2 +- packages/web/src/middleware/withAuth.test.ts | 4 ++-- packages/web/tools/globToRegexpPlayground.ts | 6 +++--- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 81f3d1548..943898eae 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -155,11 +155,11 @@ Server actions should be used for mutations (POST/PUT/DELETE operations), not fo ## Authentication -Use `withAuthV2` or `withOptionalAuthV2` from `@/withAuthV2` to protect server actions and API routes. +Use `withAuth` or `withOptionalAuth` from `@/middleware/withAuth` to protect server actions and API routes. -- **`withAuthV2`** - Requires authentication. Returns `notAuthenticated()` if user is not logged in. -- **`withOptionalAuthV2`** - Allows anonymous access if the org has anonymous access enabled. `user` may be `undefined`. -- **`withMinimumOrgRole`** - Wrap inside auth context to require a minimum role (e.g., `OrgRole.OWNER`). +- **`withAuth`** - Requires authentication. Returns `notAuthenticated()` if user is not logged in. +- **`withOptionalAuth`** - Allows anonymous access if the org has anonymous access enabled. `user` may be `undefined`. +- **`withMinimumOrgRole`** - Wrap inside auth context to require a minimum role (e.g., `OrgRole.OWNER`). Import from `@/middleware/withMinimumOrgRole`. **Important:** Always use the `prisma` instance provided by the auth context. This instance has `userScopedPrismaClientExtension` applied, which enforces repository visibility rules (e.g., filtering repos based on user permissions). Do NOT import `prisma` directly from `@/prisma` in actions or routes that return data to the client. @@ -168,11 +168,11 @@ Use `withAuthV2` or `withOptionalAuthV2` from `@/withAuthV2` to protect server a ```ts 'use server'; -import { sew } from "@/actions"; -import { withAuthV2 } from "@/withAuthV2"; +import { sew } from "@/middleware/sew"; +import { withAuth } from "@/middleware/withAuth"; export const myProtectedAction = async ({ id }: { id: string }) => sew(() => - withAuthV2(async ({ org, user, prisma }) => { + withAuth(async ({ org, user, prisma }) => { // user is guaranteed to be defined // prisma is scoped to the user return { success: true }; @@ -180,7 +180,7 @@ export const myProtectedAction = async ({ id }: { id: string }) => sew(() => ); export const myPublicAction = async ({ id }: { id: string }) => sew(() => - withOptionalAuthV2(async ({ org, user, prisma }) => { + withOptionalAuth(async ({ org, user, prisma }) => { // user may be undefined for anonymous access return { success: true }; }) @@ -192,10 +192,10 @@ export const myPublicAction = async ({ id }: { id: string }) => sew(() => ```ts import { serviceErrorResponse } from "@/lib/serviceError"; import { isServiceError } from "@/lib/utils"; -import { withAuthV2 } from "@/withAuthV2"; +import { withAuth } from "@/middleware/withAuth"; export const GET = apiHandler(async (request: NextRequest) => { - const result = await withAuthV2(async ({ org, user, prisma }) => { + const result = await withAuth(async ({ org, user, prisma }) => { // ... your logic return data; }); diff --git a/packages/web/src/features/git/listCommitsApi.test.ts b/packages/web/src/features/git/listCommitsApi.test.ts index 72e743157..af02bd2cc 100644 --- a/packages/web/src/features/git/listCommitsApi.test.ts +++ b/packages/web/src/features/git/listCommitsApi.test.ts @@ -49,7 +49,7 @@ const mockFindFirst = vi.fn(); vi.mock('@/middleware/withAuth', () => ({ withOptionalAuth: async (fn: (args: { org: { id: number; name: string }; prisma: unknown }) => Promise): Promise => { - // Mock withOptionalAuthV2 to provide org and prisma context + // Mock withOptionalAuth to provide org and prisma context const mockOrg = { id: 1, name: 'test-org' }; const mockPrisma = { repo: { diff --git a/packages/web/src/middleware/withAuth.test.ts b/packages/web/src/middleware/withAuth.test.ts index 651667fc8..4d9170fae 100644 --- a/packages/web/src/middleware/withAuth.test.ts +++ b/packages/web/src/middleware/withAuth.test.ts @@ -461,7 +461,7 @@ describe('getAuthContext', () => { }); }); -describe('withAuthV2', () => { +describe('withAuth', () => { test('should call the callback with the auth context object if a valid session is present and the user is a member of the organization', async () => { const userId = 'test-user-id'; prisma.user.findUnique.mockResolvedValue({ @@ -728,7 +728,7 @@ describe('withAuthV2', () => { }); }); -describe('withOptionalAuthV2', () => { +describe('withOptionalAuth', () => { test('should call the callback with the auth context object if a valid session is present and the user is a member of the organization', async () => { const userId = 'test-user-id'; prisma.user.findUnique.mockResolvedValue({ diff --git a/packages/web/tools/globToRegexpPlayground.ts b/packages/web/tools/globToRegexpPlayground.ts index fc915b55b..a3c2ce9f9 100644 --- a/packages/web/tools/globToRegexpPlayground.ts +++ b/packages/web/tools/globToRegexpPlayground.ts @@ -66,13 +66,13 @@ const examples: SearchInput[] = [ { pattern: 'expect\\(', include: '*.test.ts' }, // Specific subdirectory + extension - { pattern: 'withAuthV2', path: 'packages/web/src/app', include: '**/*.ts' }, + { pattern: 'withAuth', path: 'packages/web/src/app', include: '**/*.ts' }, // Next.js route group — parens in path are regex special chars - { pattern: 'withAuthV2', path: 'packages/web/src/app/api/(server)', include: '**/*.ts' }, + { pattern: 'withAuth', path: 'packages/web/src/app/api/(server)', include: '**/*.ts' }, // Next.js dynamic segment — brackets in path are regex special chars - { pattern: 'withOptionalAuthV2', path: 'packages/web/src/app/[domain]', include: '**/*.ts' }, + { pattern: 'withOptionalAuth', path: 'packages/web/src/app/[domain]', include: '**/*.ts' }, // Pattern with spaces — must be quoted in zoekt query { pattern: 'Starting scheduler', include: '**/*.ts' },