mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-11 20:05:25 +00:00
wip
This commit is contained in:
parent
6abe7a40a5
commit
e990edbb10
15 changed files with 630 additions and 841 deletions
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
);
|
||||
|
|
@ -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)
|
||||
})
|
||||
)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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!,
|
||||
}
|
||||
})
|
||||
));
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
28
packages/web/src/sew.ts
Normal 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.`);
|
||||
}
|
||||
};
|
||||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue