This commit is contained in:
bkellam 2025-09-23 21:30:00 -07:00
parent 6abe7a40a5
commit e990edbb10
15 changed files with 630 additions and 841 deletions

View file

@ -4,13 +4,12 @@ import { getAuditService } from "@/ee/features/audit/factory";
import { env } from "@/env.mjs";
import { addUserToOrganization, orgHasAvailability } from "@/lib/authUtils";
import { ErrorCode } from "@/lib/errorCodes";
import { notAuthenticated, notFound, orgNotFound, secretAlreadyExists, ServiceError, unexpectedError } from "@/lib/serviceError";
import { notAuthenticated, notFound, orgNotFound, secretAlreadyExists, ServiceError } from "@/lib/serviceError";
import { CodeHostType, getOrgMetadata, isHttpError, isServiceError } from "@/lib/utils";
import { prisma } from "@/prisma";
import { render } from "@react-email/components";
import * as Sentry from '@sentry/nextjs';
import { decrypt, encrypt, generateApiKey, getTokenFromConfig, hashSecret } from "@sourcebot/crypto";
import { ApiKey, ConnectionSyncStatus, Org, OrgRole, Prisma, RepoIndexingStatus, StripeSubscriptionStatus } from "@sourcebot/db";
import { decrypt, encrypt, generateApiKey, getTokenFromConfig } from "@sourcebot/crypto";
import { ConnectionSyncStatus, OrgRole, Prisma, RepoIndexingStatus, StripeSubscriptionStatus } from "@sourcebot/db";
import { createLogger } from "@sourcebot/logger";
import { azuredevopsSchema } from "@sourcebot/schemas/v3/azuredevops.schema";
import { bitbucketSchema } from "@sourcebot/schemas/v3/bitbucket.schema";
@ -29,7 +28,6 @@ import { StatusCodes } from "http-status-codes";
import { cookies, headers } from "next/headers";
import { createTransport } from "nodemailer";
import { Octokit } from "octokit";
import { auth } from "./auth";
import { getConnection } from "./data/connection";
import { getOrgFromDomain } from "./data/org";
import { decrementOrgSeatCount, getSubscriptionForOrg } from "./ee/features/billing/serverUtils";
@ -37,9 +35,9 @@ import { IS_BILLING_ENABLED } from "./ee/features/billing/stripe";
import InviteUserEmail from "./emails/inviteUserEmail";
import JoinRequestApprovedEmail from "./emails/joinRequestApprovedEmail";
import JoinRequestSubmittedEmail from "./emails/joinRequestSubmittedEmail";
import { AGENTIC_SEARCH_TUTORIAL_DISMISSED_COOKIE_NAME, MOBILE_UNSUPPORTED_SPLASH_SCREEN_DISMISSED_COOKIE_NAME, SEARCH_MODE_COOKIE_NAME, SINGLE_TENANT_ORG_DOMAIN, SOURCEBOT_GUEST_USER_ID, SOURCEBOT_SUPPORT_EMAIL } from "./lib/constants";
import { AGENTIC_SEARCH_TUTORIAL_DISMISSED_COOKIE_NAME, MOBILE_UNSUPPORTED_SPLASH_SCREEN_DISMISSED_COOKIE_NAME, SEARCH_MODE_COOKIE_NAME, SOURCEBOT_SUPPORT_EMAIL } from "./lib/constants";
import { orgNameSchema, repositoryQuerySchema } from "./lib/schemas";
import { ApiKeyPayload, TenancyMode } from "./lib/types";
import { sew } from "./sew";
import { withAuthV2, withOptionalAuthV2 } from "./withAuthV2";
import { withMinimumOrgRole } from "./withMinimumOrgRole";
@ -50,142 +48,6 @@ const ajv = new Ajv({
const logger = createLogger('web-actions');
const auditService = getAuditService();
/**
* "Service Error Wrapper".
*
* Captures any thrown exceptions and converts them to a unexpected
* service error. Also logs them with Sentry.
*/
export const sew = async <T>(fn: () => Promise<T>): Promise<T | ServiceError> => {
try {
return await fn();
} catch (e) {
Sentry.captureException(e);
logger.error(e);
if (e instanceof Error) {
return unexpectedError(e.message);
}
return unexpectedError(`An unexpected error occurred. Please try again later.`);
}
}
export const withAuth = async <T>(fn: (userId: string, apiKeyHash: string | undefined) => Promise<T>, allowAnonymousAccess: boolean = false, apiKey: ApiKeyPayload | undefined = undefined) => {
const session = await auth();
if (!session) {
// First we check if public access is enabled and supported. If not, then we check if an api key was provided. If not,
// then this is an invalid unauthed request and we return a 401.
const anonymousAccessEnabled = await getAnonymousAccessStatus(SINGLE_TENANT_ORG_DOMAIN);
if (apiKey) {
const apiKeyOrError = await verifyApiKey(apiKey);
if (isServiceError(apiKeyOrError)) {
logger.error(`Invalid API key: ${JSON.stringify(apiKey)}. Error: ${JSON.stringify(apiKeyOrError)}`);
return notAuthenticated();
}
const user = await prisma.user.findUnique({
where: {
id: apiKeyOrError.apiKey.createdById,
},
});
if (!user) {
logger.error(`No user found for API key: ${apiKey}`);
return notAuthenticated();
}
await prisma.apiKey.update({
where: {
hash: apiKeyOrError.apiKey.hash,
},
data: {
lastUsedAt: new Date(),
},
});
return fn(user.id, apiKeyOrError.apiKey.hash);
} else if (
allowAnonymousAccess &&
!isServiceError(anonymousAccessEnabled) &&
anonymousAccessEnabled
) {
if (!hasEntitlement("anonymous-access")) {
const plan = getPlan();
logger.error(`Anonymous access isn't supported in your current plan: ${plan}. For support, contact ${SOURCEBOT_SUPPORT_EMAIL}.`);
return notAuthenticated();
}
// To support anonymous access a guest user is created in initialize.ts, which we return here
return fn(SOURCEBOT_GUEST_USER_ID, undefined);
}
return notAuthenticated();
}
return fn(session.user.id, undefined);
}
export const withOrgMembership = async <T>(userId: string, domain: string, fn: (params: { userRole: OrgRole, org: Org }) => Promise<T>, minRequiredRole: OrgRole = OrgRole.MEMBER) => {
const org = await prisma.org.findUnique({
where: {
domain,
},
});
if (!org) {
return notFound("Organization not found");
}
const membership = await prisma.userToOrg.findUnique({
where: {
orgId_userId: {
userId,
orgId: org.id,
}
},
});
if (!membership) {
return notFound("User not a member of this organization");
}
const getAuthorizationPrecedence = (role: OrgRole): number => {
switch (role) {
case OrgRole.GUEST:
return 0;
case OrgRole.MEMBER:
return 1;
case OrgRole.OWNER:
return 2;
}
}
if (getAuthorizationPrecedence(membership.role) < 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({
org: org,
userRole: membership.role,
});
}
export const withTenancyModeEnforcement = async<T>(mode: TenancyMode, fn: () => Promise<T>) => {
if (env.SOURCEBOT_TENANCY_MODE !== mode) {
return {
statusCode: StatusCodes.FORBIDDEN,
errorCode: ErrorCode.ACTION_DISALLOWED_IN_TENANCY_MODE,
message: "This action is not allowed in the current tenancy mode.",
} satisfies ServiceError;
}
return fn();
}
////// Actions ///////
export const updateOrgName = async (name: string) => sew(() =>
@ -322,59 +184,6 @@ export const deleteSecret = async (key: string, domain: string): Promise<{ succe
}
}));
export const verifyApiKey = async (apiKeyPayload: ApiKeyPayload): Promise<{ apiKey: ApiKey } | ServiceError> => sew(async () => {
const parts = apiKeyPayload.apiKey.split("-");
if (parts.length !== 2 || parts[0] !== "sourcebot") {
return {
statusCode: StatusCodes.BAD_REQUEST,
errorCode: ErrorCode.INVALID_API_KEY,
message: "Invalid API key",
} satisfies ServiceError;
}
const hash = hashSecret(parts[1])
const apiKey = await prisma.apiKey.findUnique({
where: {
hash,
},
});
if (!apiKey) {
return {
statusCode: StatusCodes.UNAUTHORIZED,
errorCode: ErrorCode.INVALID_API_KEY,
message: "Invalid API key",
} satisfies ServiceError;
}
const apiKeyTargetOrg = await prisma.org.findUnique({
where: {
domain: apiKeyPayload.domain,
},
});
if (!apiKeyTargetOrg) {
return {
statusCode: StatusCodes.UNAUTHORIZED,
errorCode: ErrorCode.INVALID_API_KEY,
message: `Invalid API key payload. Provided domain ${apiKeyPayload.domain} does not exist.`,
} satisfies ServiceError;
}
if (apiKey.orgId !== apiKeyTargetOrg.id) {
return {
statusCode: StatusCodes.UNAUTHORIZED,
errorCode: ErrorCode.INVALID_API_KEY,
message: `Invalid API key payload. Provided domain ${apiKeyPayload.domain} does not match the API key's org.`,
} satisfies ServiceError;
}
return {
apiKey,
}
});
export const createApiKey = async (name: string, domain: string): Promise<{ key: string } | ServiceError> => sew(() =>
withAuthV2(async ({ user, org, prisma }) => {
const userId = user.id;
@ -1236,10 +1045,9 @@ export const getMe = async () => sew(() =>
}));
export const redeemInvite = async (inviteId: string): Promise<{ success: boolean } | ServiceError> => sew(() =>
withAuth(async () => {
const user = await getMe();
if (isServiceError(user)) {
return user;
withOptionalAuthV2(async ({ user, prisma }) => {
if (!user) {
return notAuthenticated();
}
const invite = await prisma.invite.findUnique({
@ -1315,10 +1123,9 @@ export const redeemInvite = async (inviteId: string): Promise<{ success: boolean
}));
export const getInviteInfo = async (inviteId: string) => sew(() =>
withAuth(async () => {
const user = await getMe();
if (isServiceError(user)) {
return user;
withOptionalAuthV2(async ({ user }) => {
if (!user) {
return notAuthenticated();
}
const invite = await prisma.invite.findUnique({
@ -1613,38 +1420,23 @@ export const getOrgAccountRequests = async (domain: string) => sew(() =>
}));
}));
export const createAccountRequest = async (userId: string, domain: string) => sew(async () => {
const user = await prisma.user.findUnique({
where: {
id: userId,
},
});
export const createAccountRequest = async () => sew(() =>
withOptionalAuthV2(async ({ user, org, prisma }) => {
if (!user) {
return notFound("User not found");
}
const org = await prisma.org.findUnique({
where: {
domain,
},
});
if (!org) {
return notFound("Organization not found");
return notAuthenticated();
}
const existingRequest = await prisma.accountRequest.findUnique({
where: {
requestedById_orgId: {
requestedById: userId,
requestedById: user.id,
orgId: org.id,
},
},
});
if (existingRequest) {
logger.warn(`User ${userId} already has an account request for org ${org.id}. Skipping account request creation.`);
logger.warn(`User ${user.id} already has an account request for org ${org.id}. Skipping account request creation.`);
return {
success: true,
existingRequest: true,
@ -1654,7 +1446,7 @@ export const createAccountRequest = async (userId: string, domain: string) => se
if (!existingRequest) {
await prisma.accountRequest.create({
data: {
requestedById: userId,
requestedById: user.id,
orgId: org.id,
},
});
@ -1676,7 +1468,7 @@ export const createAccountRequest = async (userId: string, domain: string) => se
});
if (!owner) {
logger.error(`Failed to find owner for org ${org.id} when drafting email for account request from ${userId}`);
logger.error(`Failed to find owner for org ${org.id} when drafting email for account request from ${user.id}`);
} else {
const html = await render(JoinRequestSubmittedEmail({
baseUrl: deploymentUrl,
@ -1713,7 +1505,7 @@ export const createAccountRequest = async (userId: string, domain: string) => se
success: true,
existingRequest: false,
}
});
}));
export const getMemberApprovalRequired = async (domain: string): Promise<boolean | ServiceError> => sew(async () => {
const org = await prisma.org.findUnique({

View file

@ -10,17 +10,16 @@ import { useRouter } from "next/navigation"
interface SubmitButtonProps {
domain: string
userId: string
}
export function SubmitAccountRequestButton({ domain, userId }: SubmitButtonProps) {
export function SubmitAccountRequestButton({ domain }: SubmitButtonProps) {
const { toast } = useToast()
const router = useRouter()
const [isSubmitting, setIsSubmitting] = useState(false)
const handleSubmit = async () => {
setIsSubmitting(true)
const result = await createAccountRequest(userId, domain)
const result = await createAccountRequest();
if (!isServiceError(result)) {
if (result.existingRequest) {
toast({

View file

@ -1,6 +1,5 @@
import { LogoutEscapeHatch } from "@/app/components/logoutEscapeHatch"
import { SourcebotLogo } from "@/app/components/sourcebotLogo"
import { auth } from "@/auth"
import { SubmitAccountRequestButton } from "./submitAccountRequestButton"
interface SubmitJoinRequestProps {
@ -8,13 +7,6 @@ interface SubmitJoinRequestProps {
}
export const SubmitJoinRequest = async ({ domain }: SubmitJoinRequestProps) => {
const session = await auth()
const userId = session?.user?.id
if (!userId) {
return null
}
return (
<div className="min-h-screen bg-[var(--background)] flex items-center justify-center p-6">
<LogoutEscapeHatch className="absolute top-0 right-0 p-6" />
@ -45,7 +37,7 @@ export const SubmitJoinRequest = async ({ domain }: SubmitJoinRequestProps) => {
<div className="space-y-4">
<div className="flex justify-center">
<SubmitAccountRequestButton domain={domain} userId={userId} />
<SubmitAccountRequestButton domain={domain} />
</div>
</div>
</div>

View file

@ -1,4 +1,5 @@
import { sew, withAuth, withOrgMembership } from "@/actions";
import { sew } from "@/sew";
import { withOptionalAuthV2 } from "@/withAuthV2";
import { _getConfiguredLanguageModelsFull, _getAISDKLanguageModelAndOptions, updateChatMessages } from "@/features/chat/actions";
import { createAgentStream } from "@/features/chat/agent";
import { additionalChatRequestParamsSchema, SBChatMessage, SearchScope } from "@/features/chat/types";
@ -9,7 +10,6 @@ import { isServiceError } from "@/lib/utils";
import { prisma } from "@/prisma";
import { LanguageModelV2 as AISDKLanguageModelV2 } from "@ai-sdk/provider";
import * as Sentry from "@sentry/nextjs";
import { OrgRole } from "@sourcebot/db";
import { createLogger } from "@sourcebot/logger";
import {
createUIMessageStream,
@ -52,8 +52,7 @@ export async function POST(req: Request) {
const { messages, id, selectedSearchScopes, languageModelId } = parsed.data;
const response = await sew(() =>
withAuth((userId) =>
withOrgMembership(userId, domain, async ({ org }) => {
withOptionalAuthV2(async ({ org, prisma }) => {
// Validate that the chat exists and is not readonly.
const chat = await prisma.chat.findUnique({
where: {
@ -100,8 +99,7 @@ export async function POST(req: Request) {
domain,
orgId: org.id,
});
}, /* minRequiredRole = */ OrgRole.GUEST), /* allowSingleTenantUnauthedAccess = */ true
)
})
)
if (isServiceError(response)) {

View file

@ -1,16 +1,20 @@
"use server";
import { withAuth } from "@/actions";
import { isServiceError } from "@/lib/utils";
import { orgNotFound, ServiceError } from "@/lib/serviceError";
import { sew } from "@/actions";
import { notAuthenticated, orgNotFound, ServiceError } from "@/lib/serviceError";
import { sew } from "@/sew";
import { addUserToOrganization } from "@/lib/authUtils";
import { prisma } from "@/prisma";
import { StatusCodes } from "http-status-codes";
import { ErrorCode } from "@/lib/errorCodes";
import { withOptionalAuthV2 } from "@/withAuthV2";
export const joinOrganization = async (orgId: number, inviteLinkId?: string) => sew(async () =>
withAuth(async (userId) => {
withOptionalAuthV2(async ({ user }) => {
if (!user) {
return notAuthenticated();
}
const org = await prisma.org.findUnique({
where: {
id: orgId,
@ -40,7 +44,7 @@ export const joinOrganization = async (orgId: number, inviteLinkId?: string) =>
}
}
const addUserToOrgRes = await addUserToOrganization(userId, org.id);
const addUserToOrgRes = await addUserToOrganization(user.id, org.id);
if (isServiceError(addUserToOrgRes)) {
return addUserToOrgRes;
}

View file

@ -1,17 +1,15 @@
'use server';
import { sew, withAuth, withOrgMembership } from "@/actions";
import { OrgRole } from "@sourcebot/db";
import { prisma } from "@/prisma";
import { sew } from "@/sew";
import { withAuthV2 } from "@/withAuthV2";
import { ServiceError } from "@/lib/serviceError";
import { AnalyticsResponse } from "./types";
import { hasEntitlement } from "@sourcebot/shared";
import { ErrorCode } from "@/lib/errorCodes";
import { StatusCodes } from "http-status-codes";
export const getAnalytics = async (domain: string, apiKey: string | undefined = undefined): Promise<AnalyticsResponse | ServiceError> => sew(() =>
withAuth((userId, _apiKeyHash) =>
withOrgMembership(userId, domain, async ({ org }) => {
export const getAnalytics = async (domain: string, _apiKey: string | undefined = undefined): Promise<AnalyticsResponse | ServiceError> => sew(() =>
withAuthV2(async ({ org, prisma }) => {
if (!hasEntitlement("analytics")) {
return {
statusCode: StatusCodes.FORBIDDEN,
@ -99,5 +97,5 @@ export const getAnalytics = async (domain: string, apiKey: string | undefined =
return rows;
}, /* minRequiredRole = */ OrgRole.MEMBER), /* allowAnonymousAccess = */ true, apiKey ? { apiKey, domain } : undefined)
})
);

View file

@ -1,9 +1,10 @@
"use server";
import { prisma } from "@/prisma";
import { ErrorCode } from "@/lib/errorCodes";
import { StatusCodes } from "http-status-codes";
import { sew, withAuth, withOrgMembership } from "@/actions";
import { sew } from "@/sew";
import { withAuthV2 } from "@/withAuthV2";
import { withMinimumOrgRole } from "@/withMinimumOrgRole";
import { OrgRole } from "@sourcebot/db";
import { createLogger } from "@sourcebot/logger";
import { ServiceError } from "@/lib/serviceError";
@ -13,16 +14,15 @@ import { AuditEvent } from "./types";
const auditService = getAuditService();
const logger = createLogger('audit-utils');
export const createAuditAction = async (event: Omit<AuditEvent, 'sourcebotVersion' | 'orgId' | 'actor' | 'target'>, domain: string) => sew(async () =>
withAuth((userId) =>
withOrgMembership(userId, domain, async ({ org }) => {
await auditService.createAudit({ ...event, orgId: org.id, actor: { id: userId, type: "user" }, target: { id: org.id.toString(), type: "org" } })
}, /* minRequiredRole = */ OrgRole.MEMBER), /* allowAnonymousAccess = */ true)
export const createAuditAction = async (event: Omit<AuditEvent, 'sourcebotVersion' | 'orgId' | 'actor' | 'target'>, _domain: string) => sew(async () =>
withAuthV2(async ({ user, org }) => {
await auditService.createAudit({ ...event, orgId: org.id, actor: { id: user.id, type: "user" }, target: { id: org.id.toString(), type: "org" } })
})
);
export const fetchAuditRecords = async (domain: string, apiKey: string | undefined = undefined) => sew(() =>
withAuth((userId) =>
withOrgMembership(userId, domain, async ({ org }) => {
export const fetchAuditRecords = async (domain: string, _apiKey: string | undefined = undefined) => sew(() =>
withAuthV2(async ({ user, org, prisma, role }) =>
withMinimumOrgRole(role, OrgRole.OWNER, async () => {
try {
const auditRecords = await prisma.audit.findMany({
where: {
@ -36,7 +36,7 @@ export const fetchAuditRecords = async (domain: string, apiKey: string | undefin
await auditService.createAudit({
action: "audit.fetch",
actor: {
id: userId,
id: user.id,
type: "user"
},
target: {
@ -55,5 +55,6 @@ export const fetchAuditRecords = async (domain: string, apiKey: string | undefin
message: "Failed to fetch audit logs",
} satisfies ServiceError;
}
}, /* minRequiredRole = */ OrgRole.OWNER), /* allowAnonymousAccess = */ true, apiKey ? { apiKey, domain } : undefined)
})
)
);

View file

@ -1,9 +1,9 @@
'use server';
import { getMe, sew, withAuth } from "@/actions";
import { sew } from "@/sew";
import { ServiceError, stripeClientNotInitialized, notFound } from "@/lib/serviceError";
import { withOrgMembership } from "@/actions";
import { prisma } from "@/prisma";
import { withAuthV2 } from "@/withAuthV2";
import { withMinimumOrgRole } from "@/withMinimumOrgRole";
import { OrgRole } from "@sourcebot/db";
import { stripeClient } from "./stripe";
import { isServiceError } from "@/lib/utils";
@ -17,13 +17,8 @@ import { createLogger } from "@sourcebot/logger";
const logger = createLogger('billing-actions');
export const createOnboardingSubscription = async (domain: string) => sew(() =>
withAuth(async (userId) =>
withOrgMembership(userId, domain, async ({ org }) => {
const user = await getMe();
if (isServiceError(user)) {
return user;
}
withAuthV2(async ({ user, org, prisma, role }) =>
withMinimumOrgRole(role, OrgRole.OWNER, async () => {
if (!stripeClient) {
return stripeClientNotInitialized();
}
@ -108,12 +103,10 @@ export const createOnboardingSubscription = async (domain: string) => sew(() =>
message: "Failed to create subscription",
} satisfies ServiceError;
}
}, /* minRequiredRole = */ OrgRole.OWNER)
));
})));
export const createStripeCheckoutSession = async (domain: string) => sew(() =>
withAuth((userId) =>
withOrgMembership(userId, domain, async ({ org }) => {
withAuthV2(async ({ org, prisma }) => {
if (!org.stripeCustomerId) {
return notFound();
}
@ -164,12 +157,11 @@ export const createStripeCheckoutSession = async (domain: string) => sew(() =>
return {
url: stripeSession.url,
}
})
));
}));
export const getCustomerPortalSessionLink = async (domain: string): Promise<string | ServiceError> => sew(() =>
withAuth((userId) =>
withOrgMembership(userId, domain, async ({ org }) => {
withAuthV2(async ({ org, role }) =>
withMinimumOrgRole(role, OrgRole.OWNER, async () => {
if (!org.stripeCustomerId) {
return notFound();
}
@ -185,12 +177,10 @@ export const getCustomerPortalSessionLink = async (domain: string): Promise<stri
});
return portalSession.url;
}, /* minRequiredRole = */ OrgRole.OWNER)
));
})));
export const getSubscriptionBillingEmail = async (domain: string): Promise<string | ServiceError> => sew(() =>
withAuth(async (userId) =>
withOrgMembership(userId, domain, async ({ org }) => {
export const getSubscriptionBillingEmail = async (_domain: string): Promise<string | ServiceError> => sew(() =>
withAuthV2(async ({ org }) => {
if (!org.stripeCustomerId) {
return notFound();
}
@ -204,12 +194,11 @@ export const getSubscriptionBillingEmail = async (domain: string): Promise<strin
return notFound();
}
return customer.email!;
})
));
}));
export const changeSubscriptionBillingEmail = async (domain: string, newEmail: string): Promise<{ success: boolean } | ServiceError> => sew(() =>
withAuth((userId) =>
withOrgMembership(userId, domain, async ({ org }) => {
withAuthV2(async ({ org, role }) =>
withMinimumOrgRole(role, OrgRole.OWNER, async () => {
if (!org.stripeCustomerId) {
return notFound();
}
@ -225,12 +214,10 @@ export const changeSubscriptionBillingEmail = async (domain: string, newEmail: s
return {
success: true,
}
}, /* minRequiredRole = */ OrgRole.OWNER)
));
})));
export const getSubscriptionInfo = async (domain: string) => sew(() =>
withAuth(async (userId) =>
withOrgMembership(userId, domain, async ({ org }) => {
export const getSubscriptionInfo = async (_domain: string) => sew(() =>
withAuthV2(async ({ org, prisma }) => {
const subscription = await getSubscriptionForOrg(org.id, prisma);
if (isServiceError(subscription)) {
@ -244,5 +231,4 @@ export const getSubscriptionInfo = async (domain: string) => sew(() =>
perSeatPrice: subscription.items.data[0].price.unit_amount! / 100,
nextBillingDate: subscription.current_period_end!,
}
})
));
}));

View file

@ -1,6 +1,7 @@
'use server';
import { sew, withAuth, withOrgMembership } from "@/actions";
import { sew } from "@/sew";
import { withAuthV2, withOptionalAuthV2 } from "@/withAuthV2";
import { env } from "@/env.mjs";
import { SOURCEBOT_GUEST_USER_ID } from "@/lib/constants";
import { ErrorCode } from "@/lib/errorCodes";
@ -32,9 +33,8 @@ import path from 'path';
import { LanguageModelInfo, SBChatMessage } from "./types";
export const createChat = async (domain: string) => sew(() =>
withAuth((userId) =>
withOrgMembership(userId, domain, async ({ org }) => {
withOptionalAuthV2(async ({ user, org, prisma }) => {
const userId = user?.id ?? SOURCEBOT_GUEST_USER_ID;
const isGuestUser = userId === SOURCEBOT_GUEST_USER_ID;
const chat = await prisma.chat.create({
@ -49,12 +49,12 @@ export const createChat = async (domain: string) => sew(() =>
return {
id: chat.id,
}
}, /* minRequiredRole = */ OrgRole.GUEST), /* allowSingleTenantUnauthedAccess = */ true)
})
);
export const getChatInfo = async ({ chatId }: { chatId: string }, domain: string) => sew(() =>
withAuth((userId) =>
withOrgMembership(userId, domain, async ({ org }) => {
withOptionalAuthV2(async ({ user, org, prisma }) => {
const userId = user?.id ?? SOURCEBOT_GUEST_USER_ID;
const chat = await prisma.chat.findUnique({
where: {
id: chatId,
@ -76,12 +76,12 @@ export const getChatInfo = async ({ chatId }: { chatId: string }, domain: string
name: chat.name,
isReadonly: chat.isReadonly,
};
}, /* minRequiredRole = */ OrgRole.GUEST), /* allowSingleTenantUnauthedAccess = */ true)
})
);
export const updateChatMessages = async ({ chatId, messages }: { chatId: string, messages: SBChatMessage[] }, domain: string) => sew(() =>
withAuth((userId) =>
withOrgMembership(userId, domain, async ({ org }) => {
withOptionalAuthV2(async ({ user, org, prisma }) => {
const userId = user?.id ?? SOURCEBOT_GUEST_USER_ID;
const chat = await prisma.chat.findUnique({
where: {
id: chatId,
@ -123,16 +123,15 @@ export const updateChatMessages = async ({ chatId, messages }: { chatId: string,
return {
success: true,
}
}, /* minRequiredRole = */ OrgRole.GUEST), /* allowSingleTenantUnauthedAccess = */ true)
})
);
export const getUserChatHistory = async (domain: string) => sew(() =>
withAuth((userId) =>
withOrgMembership(userId, domain, async ({ org }) => {
withAuthV2(async ({ user, org, prisma }) => {
const chats = await prisma.chat.findMany({
where: {
orgId: org.id,
createdById: userId,
createdById: user.id,
},
orderBy: {
updatedAt: 'desc',
@ -146,12 +145,11 @@ export const getUserChatHistory = async (domain: string) => sew(() =>
visibility: chat.visibility,
}))
})
)
);
export const updateChatName = async ({ chatId, name }: { chatId: string, name: string }, domain: string) => sew(() =>
withAuth((userId) =>
withOrgMembership(userId, domain, async ({ org }) => {
withOptionalAuthV2(async ({ user, org, prisma }) => {
const userId = user?.id ?? SOURCEBOT_GUEST_USER_ID;
const chat = await prisma.chat.findUnique({
where: {
id: chatId,
@ -184,12 +182,11 @@ export const updateChatName = async ({ chatId, name }: { chatId: string, name: s
return {
success: true,
}
}, /* minRequiredRole = */ OrgRole.GUEST), /* allowSingleTenantUnauthedAccess = */ true)
})
);
export const generateAndUpdateChatNameFromMessage = async ({ chatId, languageModelId, message }: { chatId: string, languageModelId: string, message: string }, domain: string) => sew(() =>
withAuth((userId) =>
withOrgMembership(userId, domain, async ({ org }) => {
withOptionalAuthV2(async ({ org }) => {
// From the language model ID, attempt to find the
// corresponding config in `config.json`.
const languageModelConfig =
@ -235,13 +232,12 @@ User question: ${message}`;
return {
success: true,
}
}, /* minRequiredRole = */ OrgRole.GUEST), /* allowSingleTenantUnauthedAccess = */ true
)
})
);
export const deleteChat = async ({ chatId }: { chatId: string }, domain: string) => sew(() =>
withAuth((userId) =>
withOrgMembership(userId, domain, async ({ org }) => {
withAuthV2(async ({ user, org, prisma }) => {
const userId = user.id;
const chat = await prisma.chat.findUnique({
where: {
id: chatId,
@ -278,7 +274,6 @@ export const deleteChat = async ({ chatId }: { chatId: string }, domain: string)
success: true,
}
})
)
);
export const submitFeedback = async ({
@ -290,8 +285,8 @@ export const submitFeedback = async ({
messageId: string,
feedbackType: 'like' | 'dislike'
}, domain: string) => sew(() =>
withAuth((userId) =>
withOrgMembership(userId, domain, async ({ org }) => {
withOptionalAuthV2(async ({ user, org, prisma }) => {
const userId = user?.id ?? SOURCEBOT_GUEST_USER_ID;
const chat = await prisma.chat.findUnique({
where: {
id: chatId,
@ -337,7 +332,7 @@ export const submitFeedback = async ({
});
return { success: true };
}, /* minRequiredRole = */ OrgRole.GUEST), /* allowSingleTenantUnauthedAccess = */ true)
})
);
/**

View file

@ -1,13 +1,13 @@
'use server';
import { sew, withAuth, withOrgMembership } from "@/actions";
import { sew } from "@/sew";
import { withOptionalAuthV2 } from "@/withAuthV2";
import { searchResponseSchema } from "@/features/search/schemas";
import { search } from "@/features/search/searchApi";
import { isServiceError } from "@/lib/utils";
import { FindRelatedSymbolsResponse } from "./types";
import { ServiceError } from "@/lib/serviceError";
import { SearchResponse } from "../search/types";
import { OrgRole } from "@sourcebot/db";
// The maximum number of matches to return from the search API.
const MAX_REFERENCE_COUNT = 1000;
@ -20,8 +20,7 @@ export const findSearchBasedSymbolReferences = async (
},
domain: string,
): Promise<FindRelatedSymbolsResponse | ServiceError> => sew(() =>
withAuth((session) =>
withOrgMembership(session, domain, async () => {
withOptionalAuthV2(async () => {
const {
symbolName,
language,
@ -41,7 +40,7 @@ export const findSearchBasedSymbolReferences = async (
}
return parseRelatedSymbolsSearchResponse(searchResult);
}, /* minRequiredRole = */ OrgRole.GUEST), /* allowAnonymousAccess = */ true)
})
);
@ -53,8 +52,7 @@ export const findSearchBasedSymbolDefinitions = async (
},
domain: string,
): Promise<FindRelatedSymbolsResponse | ServiceError> => sew(() =>
withAuth((session) =>
withOrgMembership(session, domain, async () => {
withOptionalAuthV2(async () => {
const {
symbolName,
language,
@ -74,7 +72,7 @@ export const findSearchBasedSymbolDefinitions = async (
}
return parseRelatedSymbolsSearchResponse(searchResult);
}, /* minRequiredRole = */ OrgRole.GUEST), /* allowAnonymousAccess = */ true)
})
);
const parseRelatedSymbolsSearchResponse = (searchResult: SearchResponse) => {

View file

@ -1,6 +1,6 @@
'use server';
import { sew } from '@/actions';
import { sew } from "@/sew";
import { env } from '@/env.mjs';
import { notFound, unexpectedError } from '@/lib/serviceError';
import { withOptionalAuthV2 } from '@/withAuthV2';

View file

@ -5,7 +5,7 @@ import { fileNotFound, ServiceError, unexpectedError } from "../../lib/serviceEr
import { FileSourceRequest, FileSourceResponse } from "./types";
import { isServiceError } from "../../lib/utils";
import { search } from "./searchApi";
import { sew } from "@/actions";
import { sew } from "@/sew";
import { withOptionalAuthV2 } from "@/withAuthV2";
// @todo (bkellam) : We should really be using `git show <hash>:<path>` to fetch file contents here.
// This will allow us to support permalinks to files at a specific revision that may not be indexed

View file

@ -9,7 +9,7 @@ import { StatusCodes } from "http-status-codes";
import { zoektSearchResponseSchema } from "./zoektSchema";
import { SearchRequest, SearchResponse, SourceRange } from "./types";
import { PrismaClient, Repo } from "@sourcebot/db";
import { sew } from "@/actions";
import { sew } from "@/sew";
import { base64Decode } from "@sourcebot/shared";
import { withOptionalAuthV2 } from "@/withAuthV2";

28
packages/web/src/sew.ts Normal file
View file

@ -0,0 +1,28 @@
'use server';
import * as Sentry from "@sentry/nextjs";
import { ServiceError, unexpectedError } from "./lib/serviceError";
import { createLogger } from "@sourcebot/logger";
const logger = createLogger('service-error-wrapper');
/**
* "Service Error Wrapper".
*
* Captures any thrown exceptions and converts them to a unexpected
* service error. Also logs them with Sentry.
*/
export const sew = async <T>(fn: () => Promise<T>): Promise<T | ServiceError> => {
try {
return await fn();
} catch (e) {
Sentry.captureException(e);
logger.error(e);
if (e instanceof Error) {
return unexpectedError(e.message);
}
return unexpectedError(`An unexpected error occurred. Please try again later.`);
}
};

View file

@ -5,8 +5,6 @@ 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 { StatusCodes } from "http-status-codes";
import { ErrorCode } from "./lib/errorCodes";
import { getOrgMetadata, isServiceError } from "./lib/utils";
import { hasEntitlement } from "@sourcebot/shared";