sourcebot/packages/web/src/withAuthV2.ts
Brendan Kellam f3a8fa3dab
Some checks failed
Publish to ghcr / build (linux/amd64, blacksmith-4vcpu-ubuntu-2404) (push) Has been cancelled
Publish to ghcr / build (linux/arm64, blacksmith-8vcpu-ubuntu-2204-arm) (push) Has been cancelled
Update Roadmap Released / update (push) Has been cancelled
Publish to ghcr / merge (push) Has been cancelled
feat(web): Streamed code search (#623)
* generate protobuf types

* stream poc over SSE

* wip: make stream search api follow existing schema. Modify UI to support streaming

* fix scrolling issue

* Dockerfile

* wip on lezer parser grammar for query language

* add lezer tree -> grpc transformer

* remove spammy log message

* fix syntax highlighting by adding a module resolution for @lezer/common

* further wip on query language

* Add case sensitivity and regexp toggles

* Improved type safety / cleanup for query lang

* support search contexts

* update Dockerfile with query langauge package

* fix filter

* Add skeletons to filter panel when search is streaming

* add client side caching

* improved cancelation handling

* add isSearchExausted flag for flagging when a search captured all results

* Add back posthog search_finished event

* remove zoekt tenant enforcement

* migrate blocking search over to grpc. Centralize everything in searchApi

* branch handling

* plumb file weburl

* add repo_sets filter for repositories a user has access to

* refactor a bunch of stuff + add support for passing in Query IR to search api

* refactor

* dev README

* wip on better error handling

* error handling for stream path

* update mcp

* changelog wip

* type fix

* style

* Support rev:* wildcard

* changelog

* changelog nit

* feedback

* fix build

* update docs and remove uneeded test file
2025-11-22 15:33:31 -08:00

207 lines
5.4 KiB
TypeScript

import { prisma as __unsafePrisma, userScopedPrismaClientExtension } from "@/prisma";
import { hashSecret } 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 { StatusCodes } from "http-status-codes";
import { ErrorCode } from "./lib/errorCodes";
import { getOrgMetadata, isServiceError } from "./lib/utils";
import { hasEntitlement } from "@sourcebot/shared";
interface OptionalAuthContext {
user?: UserWithAccounts;
org: Org;
role: OrgRole;
prisma: PrismaClient;
}
interface RequiredAuthContext {
user: UserWithAccounts;
org: Org;
role: Exclude<OrgRole, 'GUEST'>;
prisma: PrismaClient;
}
export const withAuthV2 = async <T>(fn: (params: RequiredAuthContext) => Promise<T>) => {
const authContext = await getAuthContext();
if (isServiceError(authContext)) {
return authContext;
}
const { user, org, role, prisma } = authContext;
if (!user || role === OrgRole.GUEST) {
return notAuthenticated();
}
return fn({ user, org, role, prisma });
};
export const withOptionalAuthV2 = async <T>(fn: (params: OptionalAuthContext) => Promise<T>) => {
const authContext = await getAuthContext();
if (isServiceError(authContext)) {
return authContext;
}
const { user, org, role, prisma } = authContext;
const hasAnonymousAccessEntitlement = hasEntitlement("anonymous-access");
const orgMetadata = getOrgMetadata(org);
if (
(
!user ||
role === OrgRole.GUEST
) && (
!hasAnonymousAccessEntitlement ||
!orgMetadata?.anonymousAccessEnabled
)
) {
return notAuthenticated();
}
return fn({ user, org, role, prisma });
};
export const getAuthContext = async (): Promise<OptionalAuthContext | ServiceError> => {
const user = await getAuthenticatedUser();
const org = await __unsafePrisma.org.findUnique({
where: {
id: SINGLE_TENANT_ORG_ID,
}
});
if (!org) {
return notFound("Organization not found");
}
const membership = user ? await __unsafePrisma.userToOrg.findUnique({
where: {
orgId_userId: {
orgId: org.id,
userId: user.id,
},
},
}) : null;
const prisma = __unsafePrisma.$extends(userScopedPrismaClientExtension(user)) as PrismaClient;
return {
user: user ?? undefined,
org,
role: membership?.role ?? OrgRole.GUEST,
prisma,
};
};
export const getAuthenticatedUser = async () => {
// First, check if we have a valid JWT session.
const session = await auth();
if (session) {
const userId = session.user.id;
const user = await __unsafePrisma.user.findUnique({
where: {
id: userId,
},
include: {
accounts: true,
}
});
return user ?? undefined;
}
// If not, check if we have a valid API key.
const apiKeyString = (await headers()).get("X-Sourcebot-Api-Key") ?? undefined;
if (apiKeyString) {
const apiKey = await getVerifiedApiObject(apiKeyString);
if (!apiKey) {
return undefined;
}
// Attempt to find the user associated with this api key.
const user = await __unsafePrisma.user.findUnique({
where: {
id: apiKey.createdById,
},
include: {
accounts: true,
}
});
if (!user) {
return undefined;
}
// Update the last used at timestamp for this api key.
await __unsafePrisma.apiKey.update({
where: {
hash: apiKey.hash,
},
data: {
lastUsedAt: new Date(),
},
});
return user;
}
return undefined;
}
/**
* Returns a API key object if the API key string is valid, otherwise returns undefined.
*/
const getVerifiedApiObject = async (apiKeyString: string): Promise<ApiKey | undefined> => {
const parts = apiKeyString.split("-");
if (parts.length !== 2 || parts[0] !== "sourcebot") {
return undefined;
}
const hash = hashSecret(parts[1]);
const apiKey = await __unsafePrisma.apiKey.findUnique({
where: {
hash,
},
});
if (!apiKey) {
return undefined;
}
return apiKey;
}
export const withMinimumOrgRole = async <T>(
userRole: OrgRole,
minRequiredRole: OrgRole = OrgRole.MEMBER,
fn: () => Promise<T>,
): Promise<T | ServiceError> => {
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();
}