diff --git a/.env.development b/.env.development index 84670f852..39b25125c 100644 --- a/.env.development +++ b/.env.development @@ -40,12 +40,6 @@ CONFIG_PATH=${PWD}/config.json # Path to the sourcebot config file (if one exist # Redis REDIS_URL="redis://localhost:6379" -# Stripe -# STRIPE_SECRET_KEY: z.string().optional(), -# STRIPE_PRODUCT_ID: z.string().optional(), -# STRIPE_WEBHOOK_SECRET: z.string().optional(), -# STRIPE_ENABLE_TEST_CLOCKS=false - # Agents # GITHUB_APP_ID= @@ -75,6 +69,5 @@ SOURCEBOT_TELEMETRY_DISABLED=true # Disables telemetry collection # CONFIG_MAX_REPOS_NO_TOKEN= NODE_ENV=development -# SOURCEBOT_TENANCY_MODE=single DEBUG_WRITE_CHAT_MESSAGES_TO_FILE=true diff --git a/CHANGELOG.md b/CHANGELOG.md index d2f2db5fa..424544c3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [4.16.5] - 2026-04-02 + +### Added +- Added `GET /api/commit` endpoint for retrieving details about a single commit, including parent commit SHAs [#1077](https://github.com/sourcebot-dev/sourcebot/pull/1077) + +### Changed +- Replaced placeholder avatars with deterministic minidenticon-based avatars generated from email addresses [#1072](https://github.com/sourcebot-dev/sourcebot/pull/1072) +- Changed `author_name` and `author_email` fields to `authorName` and `authorEmail` in `GET /api/commits` response [#1077](https://github.com/sourcebot-dev/sourcebot/pull/1077) +- Changed `oldPath` and `newPath` in `GET /api/diff` response from `"/dev/null"` to `null` for added/deleted files [#1077](https://github.com/sourcebot-dev/sourcebot/pull/1077) +- Bumped `simple-git` to `3.33.0`. [#1078](https://github.com/sourcebot-dev/sourcebot/pull/1078) + ## [4.16.4] - 2026-04-01 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index 3755abd4c..943898eae 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -71,6 +71,13 @@ className="border-[var(--border)] bg-[var(--card)] text-[var(--foreground)]" ## API Route Handlers +When implementing a new API route, ask the user whether it should be part of the public API. If yes: + +1. Add the request/response Zod schemas to `packages/web/src/openapi/publicApiSchemas.ts`, calling `.openapi('SchemaName')` on each schema to register it with a name. +2. Register the route in `packages/web/src/openapi/publicApiDocument.ts` using `registry.registerPath(...)`, assigning it to the appropriate tag. +3. Add the endpoint to the relevant group in the `API Reference` tab of `docs/docs.json`. +4. Regenerate the OpenAPI spec by running `yarn workspace @sourcebot/web openapi:generate`. + Route handlers should validate inputs using Zod schemas. **Query parameters** (GET requests): @@ -148,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. @@ -161,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 }; @@ -173,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 }; }) @@ -185,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/docs/api-reference/sourcebot-public.openapi.json b/docs/api-reference/sourcebot-public.openapi.json index ebfa93f1e..5dae5e884 100644 --- a/docs/api-reference/sourcebot-public.openapi.json +++ b/docs/api-reference/sourcebot-public.openapi.json @@ -2,7 +2,7 @@ "openapi": "3.0.3", "info": { "title": "Sourcebot Public API", - "version": "v4.16.4", + "version": "v4.16.5", "description": "OpenAPI description for the public Sourcebot REST endpoints used for search, repository listing, and file browsing. Authentication is instance-dependent: API keys are the standard integration mechanism, OAuth bearer tokens are EE-only, and some instances may allow anonymous access." }, "tags": [ @@ -23,12 +23,8 @@ "description": "System health and version endpoints." }, { - "name": "User Management (EE)", - "description": "User management endpoints. Requires the `org-management` entitlement and OWNER role." - }, - { - "name": "Audit (EE)", - "description": "Audit log endpoints. Requires the `audit` entitlement and OWNER role." + "name": "Enterprise (EE)", + "description": "Enterprise endpoints for user management and audit logging." } ], "security": [ @@ -623,11 +619,13 @@ "properties": { "oldPath": { "type": "string", - "description": "The file path before the change. `/dev/null` for added files." + "nullable": true, + "description": "The file path before the change. `null` for added files." }, "newPath": { "type": "string", - "description": "The file path after the change. `/dev/null` for deleted files." + "nullable": true, + "description": "The file path after the change. `null` for deleted files." }, "hunks": { "type": "array", @@ -896,10 +894,10 @@ "type": "string", "description": "The commit body (everything after the subject line)." }, - "author_name": { + "authorName": { "type": "string" }, - "author_email": { + "authorEmail": { "type": "string" } }, @@ -909,8 +907,8 @@ "message", "refs", "body", - "author_name", - "author_email" + "authorName", + "authorEmail" ] }, "PublicListCommitsResponse": { @@ -919,6 +917,54 @@ "$ref": "#/components/schemas/PublicCommit" } }, + "PublicCommitDetail": { + "type": "object", + "properties": { + "hash": { + "type": "string", + "description": "The full commit SHA." + }, + "date": { + "type": "string", + "description": "The commit date in ISO 8601 format." + }, + "message": { + "type": "string", + "description": "The commit subject line." + }, + "refs": { + "type": "string", + "description": "Refs pointing to this commit (e.g. branch or tag names)." + }, + "body": { + "type": "string", + "description": "The commit body (everything after the subject line)." + }, + "authorName": { + "type": "string" + }, + "authorEmail": { + "type": "string" + }, + "parents": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The parent commit SHAs." + } + }, + "required": [ + "hash", + "date", + "message", + "refs", + "body", + "authorName", + "authorEmail", + "parents" + ] + }, "PublicEeUser": { "type": "object", "properties": { @@ -1100,13 +1146,13 @@ "bearerToken": { "type": "http", "scheme": "bearer", - "description": "Send either a Sourcebot API key (`sbk_...`) or, on EE instances with OAuth enabled, an OAuth access token (`sboa_...`) in the Authorization header." + "description": "Bearer authentication header of the form `Bearer `, where `` is your API key." }, "apiKeyHeader": { "type": "apiKey", "in": "header", "name": "X-Sourcebot-Api-Key", - "description": "Send a Sourcebot API key (`sbk_...`) in the X-Sourcebot-Api-Key header." + "description": "Header of the form `X-Sourcebot-Api-Key: `, where `` is your API key." } } }, @@ -1824,14 +1870,91 @@ } } }, + "/api/commit": { + "get": { + "operationId": "getCommit", + "tags": [ + "Git" + ], + "summary": "Get commit details", + "description": "Returns details for a single commit, including parent commit SHAs.", + "parameters": [ + { + "schema": { + "type": "string", + "description": "The fully-qualified repository name." + }, + "required": true, + "description": "The fully-qualified repository name.", + "name": "repo", + "in": "query" + }, + { + "schema": { + "type": "string", + "description": "The git ref (commit SHA, branch, or tag)." + }, + "required": true, + "description": "The git ref (commit SHA, branch, or tag).", + "name": "ref", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Commit details.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicCommitDetail" + } + } + } + }, + "400": { + "description": "Invalid query parameters or git ref.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicApiServiceError" + } + } + } + }, + "404": { + "description": "Repository or revision not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicApiServiceError" + } + } + } + }, + "500": { + "description": "Unexpected failure.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicApiServiceError" + } + } + } + } + } + } + }, "/api/ee/user": { "get": { "operationId": "getUser", "tags": [ - "User Management (EE)" + "Enterprise (EE)" ], "summary": "Get a user", "description": "Fetches profile details for a single organization member by `userId`. Only organization owners can access this endpoint.", + "x-mint": { + "content": "\nThis API is only available with an active Enterprise license. Please add your [license key](/docs/license-key) to activate it.\n" + }, "parameters": [ { "schema": { @@ -1900,10 +2023,13 @@ "delete": { "operationId": "deleteUser", "tags": [ - "User Management (EE)" + "Enterprise (EE)" ], "summary": "Delete a user", "description": "Permanently deletes a user and all associated records. Only organization owners can delete other users.", + "x-mint": { + "content": "\nThis API is only available with an active Enterprise license. Please add your [license key](/docs/license-key) to activate it.\n" + }, "parameters": [ { "schema": { @@ -1974,10 +2100,13 @@ "get": { "operationId": "listUsers", "tags": [ - "User Management (EE)" + "Enterprise (EE)" ], "summary": "List users", "description": "Returns all members of the organization. Only organization owners can access this endpoint.", + "x-mint": { + "content": "\nThis API is only available with an active Enterprise license. Please add your [license key](/docs/license-key) to activate it.\n" + }, "responses": { "200": { "description": "List of organization members.", @@ -2016,10 +2145,13 @@ "get": { "operationId": "listAuditRecords", "tags": [ - "Audit (EE)" + "Enterprise (EE)" ], "summary": "List audit records", "description": "Returns a paginated list of audit log entries. Only organization owners can access this endpoint.", + "x-mint": { + "content": "\nThis API is only available with an active Enterprise license. Please add your [license key](/docs/license-key) to activate it.\n" + }, "parameters": [ { "schema": { diff --git a/docs/docs.json b/docs/docs.json index e526bd312..50e9e7990 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -145,6 +145,13 @@ "icon": "code", "openapi": "api-reference/sourcebot-public.openapi.json", "groups": [ + { + "group": "Overview", + "icon": "book-open", + "pages": [ + "docs/api-reference/authentication" + ] + }, { "group": "Search & Navigation", "icon": "magnifying-glass", @@ -165,6 +172,7 @@ "group": "Git", "icon": "code-branch", "pages": [ + "GET /api/commit", "GET /api/diff", "GET /api/commits", "GET /api/source", diff --git a/docs/docs/api-reference/authentication.mdx b/docs/docs/api-reference/authentication.mdx new file mode 100644 index 000000000..9c75fb425 --- /dev/null +++ b/docs/docs/api-reference/authentication.mdx @@ -0,0 +1,34 @@ +--- +title: "Authentication" +--- + +To securely access and interact with Sourcebot’s API, authentication is required. Users must generate an API Key, which will be used to authenticate requests. + + +If [anonymous access](/docs/configuration/auth/access-settings#anonymous-access) is enabled, some endpoints will be accessible without a API key. + + +## Creating an API key + +Navigate to **Settings → API Keys** and click **Create API Key**. Copy the value - it is only shown once. + + + API Keys page in Sourcebot Settings + + +## Using an API key + +Pass your API key as a Bearer token in the `Authorization` header on every request. + +```bash +Authorization: Bearer +``` + +For example, to call the `/api/search` endpoint: + +```bash +curl -X POST https://your-sourcebot-instance.com/api/search \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{"query": "hello world", "matches": 10}' +``` diff --git a/docs/docs/configuration/tenancy.mdx b/docs/docs/configuration/tenancy.mdx deleted file mode 100644 index b70412a9c..000000000 --- a/docs/docs/configuration/tenancy.mdx +++ /dev/null @@ -1,27 +0,0 @@ ---- -title: Multi Tenancy Mode -sidebarTitle: Multi tenancy ---- - -If you're switching from single-tenant mode, delete the Sourcebot cache (the `.sourcebot` folder) before starting. -[Authentication](/docs/configuration/auth/overview) must be enabled to enable multi tenancy mode -Multi tenancy allows your Sourcebot deployment to have **multiple organizations**, each with their own set of members and repos. To enable multi tenancy mode, define an environment variable -named `SOURCEBOT_TENANCY_MODE` and set its value to `multi`. When multi tenancy mode is enabled: - -- Any members or repos that are configured in an organization are isolated to that organization -- Members must be invited to an organization to gain access -- Members may be a part of multiple organizations and switch through them in the UI - - -### Organization creation form - -When you sign in for the first time (assuming you didn't go through an invite), you'll be presented with the organization creation form. The member who creates -the organization will be the Owner. - -![Org creation](/images/org_create.png) - -### Switching between organizations - -To switch between organizations, press the drop down on the top left of the navigation menu. This also provides an option to create a new organization: - -![Org switching](/images/org_switch.png) diff --git a/packages/backend/package.json b/packages/backend/package.json index ffa716c43..779c3fcad 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -53,7 +53,7 @@ "posthog-node": "^5.24.15", "prom-client": "^15.1.3", "redlock": "5.0.0-beta.2", - "simple-git": "^3.27.0", + "simple-git": "^3.33.0", "zod": "^3.25.74" } } diff --git a/packages/db/prisma/migrations/20260401200619_remove_stripe_billing/migration.sql b/packages/db/prisma/migrations/20260401200619_remove_stripe_billing/migration.sql new file mode 100644 index 000000000..436c17849 --- /dev/null +++ b/packages/db/prisma/migrations/20260401200619_remove_stripe_billing/migration.sql @@ -0,0 +1,15 @@ +/* + Warnings: + + - You are about to drop the column `stripeCustomerId` on the `Org` table. All the data in the column will be lost. + - You are about to drop the column `stripeLastUpdatedAt` on the `Org` table. All the data in the column will be lost. + - You are about to drop the column `stripeSubscriptionStatus` on the `Org` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "Org" DROP COLUMN "stripeCustomerId", +DROP COLUMN "stripeLastUpdatedAt", +DROP COLUMN "stripeSubscriptionStatus"; + +-- DropEnum +DROP TYPE "StripeSubscriptionStatus"; diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index 609c71b89..0c58714b6 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -19,11 +19,6 @@ enum ConnectionSyncStatus { FAILED } -enum StripeSubscriptionStatus { - ACTIVE - INACTIVE -} - enum ChatVisibility { PRIVATE PUBLIC @@ -283,10 +278,6 @@ model Org { memberApprovalRequired Boolean @default(true) - stripeCustomerId String? - stripeSubscriptionStatus StripeSubscriptionStatus? - stripeLastUpdatedAt DateTime? - /// List of pending invites to this organization invites Invite[] diff --git a/packages/shared/src/entitlements.ts b/packages/shared/src/entitlements.ts index 0effb0a58..de841a0dd 100644 --- a/packages/shared/src/entitlements.ts +++ b/packages/shared/src/entitlements.ts @@ -30,7 +30,6 @@ export type Plan = keyof typeof planLabels; // eslint-disable-next-line @typescript-eslint/no-unused-vars const entitlements = [ "search-contexts", - "billing", "anonymous-access", "multi-tenancy", "sso", diff --git a/packages/shared/src/env.server.ts b/packages/shared/src/env.server.ts index 95006d875..62b447638 100644 --- a/packages/shared/src/env.server.ts +++ b/packages/shared/src/env.server.ts @@ -6,7 +6,6 @@ import { readFile } from 'fs/promises'; import stripJsonComments from "strip-json-comments"; import { z } from "zod"; import { getTokenFromConfig } from "./crypto.js"; -import { tenancyModeSchema } from "./types.js"; // Booleans are specified as 'true' or 'false' strings. const booleanSchema = z.enum(["true", "false"]); @@ -163,12 +162,6 @@ const options = { SMTP_PASSWORD: z.string().optional(), EMAIL_FROM_ADDRESS: z.string().email().optional(), - // Stripe - STRIPE_SECRET_KEY: z.string().optional(), - STRIPE_PRODUCT_ID: z.string().optional(), - STRIPE_WEBHOOK_SECRET: z.string().optional(), - STRIPE_ENABLE_TEST_CLOCKS: booleanSchema.default('false'), - LOGTAIL_TOKEN: z.string().optional(), LOGTAIL_HOST: z.string().url().optional(), @@ -188,7 +181,6 @@ const options = { DATABASE_NAME: z.string().optional(), DATABASE_ARGS: z.string().optional(), - SOURCEBOT_TENANCY_MODE: tenancyModeSchema.default("single"), CONFIG_PATH: z.string(), // Misc UI flags diff --git a/packages/shared/src/index.server.ts b/packages/shared/src/index.server.ts index 9f8dc3c27..a1eb34204 100644 --- a/packages/shared/src/index.server.ts +++ b/packages/shared/src/index.server.ts @@ -17,7 +17,6 @@ export type { export { repoMetadataSchema, repoIndexingJobMetadataSchema, - tenancyModeSchema, } from "./types.js"; export { base64Decode, diff --git a/packages/shared/src/types.ts b/packages/shared/src/types.ts index d52a09923..b0291a57b 100644 --- a/packages/shared/src/types.ts +++ b/packages/shared/src/types.ts @@ -64,6 +64,4 @@ export const repoIndexingJobMetadataSchema = z.object({ export type RepoIndexingJobMetadata = z.infer; -export const tenancyModeSchema = z.enum(["multi", "single"]); - export type IdentityProviderType = IdentityProviderConfig['provider']; \ No newline at end of file diff --git a/packages/shared/src/version.ts b/packages/shared/src/version.ts index aec0bbf22..c19ab8bf4 100644 --- a/packages/shared/src/version.ts +++ b/packages/shared/src/version.ts @@ -1,2 +1,2 @@ // This file is auto-generated by .github/workflows/release-sourcebot.yml -export const SOURCEBOT_VERSION = "v4.16.4"; +export const SOURCEBOT_VERSION = "v4.16.5"; diff --git a/packages/web/package.json b/packages/web/package.json index 35eefe539..695d310d9 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -11,7 +11,6 @@ "openapi:generate": "tsx tools/generateOpenApi.ts", "generate:protos": "proto-loader-gen-types --includeComments --longs=Number --enums=String --defaults --oneofs --grpcLib=@grpc/grpc-js --keepCase --includeDirs=../../vendor/zoekt/grpc/protos --outDir=src/proto zoekt/webserver/v1/webserver.proto zoekt/webserver/v1/query.proto", "dev:emails": "email dev --dir ./src/emails", - "stripe:listen": "stripe listen --forward-to http://localhost:3000/api/stripe", "tool:decrypt-jwe": "tsx tools/decryptJWE.ts" }, "dependencies": { @@ -102,8 +101,6 @@ "@sourcebot/schemas": "workspace:*", "@sourcebot/shared": "workspace:*", "@ssddanbrown/codemirror-lang-twig": "^1.0.0", - "@stripe/react-stripe-js": "^3.1.1", - "@stripe/stripe-js": "^5.6.0", "@tailwindcss/typography": "^0.5.16", "@tanstack/react-query": "^5.53.3", "@tanstack/react-table": "^8.20.5", @@ -154,6 +151,7 @@ "linguist-languages": "^9.3.1", "lucide-react": "^0.517.0", "micromatch": "^4.0.8", + "minidenticons": "^4.2.1", "next": "16.1.6", "next-auth": "^5.0.0-beta.30", "next-navigation-guard": "^0.2.0", @@ -181,13 +179,12 @@ "scroll-into-view-if-needed": "^3.1.0", "server-only": "^0.0.1", "sharp": "^0.33.5", - "simple-git": "^3.27.0", + "simple-git": "^3.33.0", "slate": "^0.117.0", "slate-dom": "^0.116.0", "slate-history": "^0.113.1", "slate-react": "^0.117.1", "strip-json-comments": "^5.0.1", - "stripe": "^17.6.0", "tailwind-merge": "^2.5.2", "tailwindcss-animate": "^1.0.7", "use-stick-to-bottom": "^1.1.3", @@ -199,7 +196,7 @@ "devDependencies": { "@asteasolutions/zod-to-openapi": "7.3.4", "@eslint/eslintrc": "^3", - "@react-email/preview-server": "5.2.8", + "@react-email/preview-server": "5.2.10", "@react-grab/mcp": "^0.1.23", "@tanstack/eslint-plugin-query": "^5.74.7", "@testing-library/dom": "^10.4.1", @@ -222,7 +219,7 @@ "npm-run-all": "^4.1.5", "postcss": "^8", "raw-loader": "^4.0.2", - "react-email": "^5.1.0", + "react-email": "^5.2.10", "react-grab": "^0.1.23", "react-scan": "^0.5.3", "tailwindcss": "^3.4.1", diff --git a/packages/web/src/__mocks__/prisma.ts b/packages/web/src/__mocks__/prisma.ts index 8a0d9544c..acf687294 100644 --- a/packages/web/src/__mocks__/prisma.ts +++ b/packages/web/src/__mocks__/prisma.ts @@ -19,9 +19,6 @@ export const MOCK_ORG: Org = { imageUrl: null, metadata: null, memberApprovalRequired: false, - stripeCustomerId: null, - stripeSubscriptionStatus: null, - stripeLastUpdatedAt: null, inviteLinkEnabled: false, inviteLinkId: null } diff --git a/packages/web/src/actions.ts b/packages/web/src/actions.ts index 60ffe332e..ad5d5f24e 100644 --- a/packages/web/src/actions.ts +++ b/packages/web/src/actions.ts @@ -4,13 +4,12 @@ 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 { notAuthenticated, 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, Org, OrgRole, Prisma, RepoIndexingJobStatus, RepoIndexingJobType, StripeSubscriptionStatus } from "@sourcebot/db"; +import { ApiKey, ConnectionSyncJobStatus, OrgRole, Prisma, RepoIndexingJobStatus, RepoIndexingJobType } from "@sourcebot/db"; import { createLogger } from "@sourcebot/shared"; import { GiteaConnectionConfig } from "@sourcebot/schemas/v3/gitea.type"; import { GithubConnectionConfig } from "@sourcebot/schemas/v3/github.type"; @@ -20,205 +19,34 @@ import { StatusCodes } from "http-status-codes"; import { cookies } from "next/headers"; import { createTransport } from "nodemailer"; import { Octokit } from "octokit"; -import { auth } from "./auth"; import { getOrgFromDomain } from "./data/org"; -import { getSubscriptionForOrg } from "./ee/features/billing/serverUtils"; -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, SINGLE_TENANT_ORG_DOMAIN, SOURCEBOT_GUEST_USER_ID, SOURCEBOT_SUPPORT_EMAIL } from "./lib/constants"; -import { ApiKeyPayload, RepositoryQuery, TenancyMode } from "./lib/types"; -import { withAuthV2, withOptionalAuthV2 } from "./withAuthV2"; +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 { 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.`); - } -} - -export const withAuth = async (fn: (userId: string, apiKeyHash: string | undefined) => Promise, 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(); - } - - if (env.DISABLE_API_KEY_USAGE_FOR_NON_OWNER_USERS === 'true') { - const membership = await prisma.userToOrg.findFirst({ - where: { userId: user.id }, - }); - if (membership?.role !== OrgRole.OWNER) { - return { - statusCode: StatusCodes.FORBIDDEN, - errorCode: ErrorCode.API_KEY_USAGE_DISABLED, - message: "API key usage is disabled for non-admin users.", - } satisfies ServiceError; - } - } - - 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 (userId: string, domain: string, fn: (params: { userRole: OrgRole, org: Org }) => Promise, 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(mode: TenancyMode, fn: () => Promise) => { - 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 completeOnboarding = async (domain: string): Promise<{ success: boolean } | ServiceError> => sew(() => - withAuth((userId) => - withOrgMembership(userId, domain, async ({ org }) => { - // If billing is not enabled, we can just mark the org as onboarded. - if (!IS_BILLING_ENABLED) { - await prisma.org.update({ - where: { id: org.id }, - data: { - isOnboarded: true, - } - }); - - // Else, validate that the org has an active subscription. - } else { - const subscriptionOrError = await getSubscriptionForOrg(org.id, prisma); - if (isServiceError(subscriptionOrError)) { - return subscriptionOrError; - } - - await prisma.org.update({ - where: { id: org.id }, - data: { - isOnboarded: true, - stripeSubscriptionStatus: StripeSubscriptionStatus.ACTIVE, - stripeLastUpdatedAt: new Date(), - } - }); +export const completeOnboarding = async (): Promise<{ success: boolean } | ServiceError> => sew(() => + withAuth(async ({ org, prisma }) => { + await prisma.org.update({ + where: { id: org.id }, + data: { + isOnboarded: true, } + }); - return { - success: true, - } - }) - )); + return { + success: true, + } + })); export const verifyApiKey = async (apiKeyPayload: ApiKeyPayload): Promise<{ apiKey: ApiKey } | ServiceError> => sew(async () => { const parts = apiKeyPayload.apiKey.split("-"); @@ -273,157 +101,154 @@ export const verifyApiKey = async (apiKeyPayload: ApiKeyPayload): Promise<{ apiK }); -export const createApiKey = async (name: string, domain: string): Promise<{ key: string } | ServiceError> => sew(() => - withAuth((userId) => - withOrgMembership(userId, domain, async ({ org, userRole }) => { - if ((env.DISABLE_API_KEY_CREATION_FOR_NON_OWNER_USERS === 'true' || env.DISABLE_API_KEY_USAGE_FOR_NON_OWNER_USERS === 'true') && userRole !== OrgRole.OWNER) { - logger.error(`API key creation is disabled for non-admin users. User ${userId} is not an owner.`); - return { - statusCode: StatusCodes.FORBIDDEN, - errorCode: ErrorCode.INSUFFICIENT_PERMISSIONS, - message: "API key creation is disabled for non-admin users.", - } satisfies ServiceError; - } - - const existingApiKey = await prisma.apiKey.findFirst({ - where: { - createdById: userId, - name, - }, - }); - - if (existingApiKey) { - await auditService.createAudit({ - action: "api_key.creation_failed", - actor: { - id: userId, - type: "user" - }, - target: { - id: org.id.toString(), - type: "org" - }, - orgId: org.id, - metadata: { - message: `API key ${name} already exists`, - api_key: name - } - }); - return { - statusCode: StatusCodes.BAD_REQUEST, - errorCode: ErrorCode.API_KEY_ALREADY_EXISTS, - message: `API key ${name} already exists`, - } satisfies ServiceError; - } +export const createApiKey = async (name: string): Promise<{ key: string } | ServiceError> => sew(() => + 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 { + statusCode: StatusCodes.FORBIDDEN, + errorCode: ErrorCode.INSUFFICIENT_PERMISSIONS, + message: "API key creation is disabled for non-admin users.", + } satisfies ServiceError; + } - const { key, hash } = generateApiKey(); - const apiKey = await prisma.apiKey.create({ - data: { - name, - hash, - orgId: org.id, - createdById: userId, - } - }); + const existingApiKey = await prisma.apiKey.findFirst({ + where: { + createdById: user.id, + name, + }, + }); + if (existingApiKey) { await auditService.createAudit({ - action: "api_key.created", + action: "api_key.creation_failed", actor: { - id: userId, + id: user.id, type: "user" }, target: { - id: apiKey.hash, - type: "api_key" + id: org.id.toString(), + type: "org" }, - orgId: org.id + orgId: org.id, + metadata: { + message: `API key ${name} already exists`, + api_key: name + } }); - return { - key, + statusCode: StatusCodes.BAD_REQUEST, + errorCode: ErrorCode.API_KEY_ALREADY_EXISTS, + message: `API key ${name} already exists`, + } satisfies ServiceError; + } + + const { key, hash } = generateApiKey(); + const apiKey = await prisma.apiKey.create({ + data: { + name, + hash, + orgId: org.id, + createdById: user.id, } - }))); + }); -export const deleteApiKey = async (name: string, domain: string): Promise<{ success: boolean } | ServiceError> => sew(() => - withAuth((userId) => - withOrgMembership(userId, domain, async ({ org }) => { - const apiKey = await prisma.apiKey.findFirst({ - where: { - name, - createdById: userId, - }, - }); + await auditService.createAudit({ + action: "api_key.created", + actor: { + id: user.id, + type: "user" + }, + target: { + id: apiKey.hash, + type: "api_key" + }, + orgId: org.id + }); - if (!apiKey) { - await auditService.createAudit({ - action: "api_key.deletion_failed", - actor: { - id: userId, - type: "user" - }, - target: { - id: domain, - type: "org" - }, - orgId: org.id, - metadata: { - message: `API key ${name} not found for user ${userId}`, - api_key: name - } - }); - return { - statusCode: StatusCodes.NOT_FOUND, - errorCode: ErrorCode.API_KEY_NOT_FOUND, - message: `API key ${name} not found for user ${userId}`, - } satisfies ServiceError; - } + return { + key, + } + })); - await prisma.apiKey.delete({ - where: { - hash: apiKey.hash, - }, - }); +export const deleteApiKey = async (name: string): Promise<{ success: boolean } | ServiceError> => sew(() => + withAuth(async ({ org, user, prisma }) => { + const apiKey = await prisma.apiKey.findFirst({ + where: { + name, + createdById: user.id, + }, + }); + if (!apiKey) { await auditService.createAudit({ - action: "api_key.deleted", + action: "api_key.deletion_failed", actor: { - id: userId, + id: user.id, type: "user" }, target: { - id: apiKey.hash, - type: "api_key" + id: org.domain, + type: "org" }, orgId: org.id, metadata: { + message: `API key ${name} not found for user ${user.id}`, api_key: name } }); - return { - success: true, + statusCode: StatusCodes.NOT_FOUND, + errorCode: ErrorCode.API_KEY_NOT_FOUND, + message: `API key ${name} not found for user ${user.id}`, + } satisfies ServiceError; + } + + await prisma.apiKey.delete({ + where: { + hash: apiKey.hash, + }, + }); + + await auditService.createAudit({ + action: "api_key.deleted", + actor: { + id: user.id, + type: "user" + }, + target: { + id: apiKey.hash, + type: "api_key" + }, + orgId: org.id, + metadata: { + api_key: name } - }))); + }); -export const getUserApiKeys = async (domain: string): Promise<{ name: string; createdAt: Date; lastUsedAt: Date | null }[] | ServiceError> => sew(() => - withAuth((userId) => - withOrgMembership(userId, domain, async ({ org }) => { - const apiKeys = await prisma.apiKey.findMany({ - where: { - orgId: org.id, - createdById: userId, - }, - orderBy: { - createdAt: 'desc', - } - }); + return { + success: true, + } + })); - return apiKeys.map((apiKey) => ({ - name: apiKey.name, - createdAt: apiKey.createdAt, - lastUsedAt: apiKey.lastUsedAt, - })); - }))); +export const getUserApiKeys = async (): Promise<{ name: string; createdAt: Date; lastUsedAt: Date | null }[] | ServiceError> => sew(() => + withAuth(async ({ org, user, prisma }) => { + const apiKeys = await prisma.apiKey.findMany({ + where: { + orgId: org.id, + createdById: user.id, + }, + orderBy: { + createdAt: 'desc', + } + }); + + return apiKeys.map((apiKey) => ({ + name: apiKey.name, + createdAt: apiKey.createdAt, + lastUsedAt: apiKey.lastUsedAt, + })); + })); export const getRepos = async ({ where, @@ -432,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, @@ -452,7 +277,6 @@ export const getRepos = async ({ repoName: repo.name, path: '', pathType: 'tree', - domain: org.domain, })}`, externalWebUrl: repo.webUrl ?? undefined, imageUrl: repo.imageUrl ?? undefined, @@ -468,7 +292,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, @@ -519,7 +343,7 @@ export const getReposStats = async () => sew(() => ) export const getConnectionStats = async () => sew(() => - withAuthV2(async ({ org, prisma }) => { + withAuth(async ({ org, prisma }) => { const [ numberOfConnections, numberOfConnectionsWithFirstTimeSyncJobsInProgress, @@ -555,7 +379,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 @@ -614,7 +438,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, @@ -749,21 +573,19 @@ export const experimental_addGithubRepositoryByUrl = async (repositoryUrl: strin } })); -export const getCurrentUserRole = async (domain: string): Promise => sew(() => - withAuth((userId) => - withOrgMembership(userId, domain, async ({ userRole }) => { - return userRole; - }, /* minRequiredRole = */ OrgRole.GUEST), /* allowAnonymousAccess = */ true - )); +export const getCurrentUserRole = async (): Promise => sew(() => + withOptionalAuth(async ({ role }) => { + return role; + })); -export const createInvites = async (emails: string[], domain: string): Promise<{ success: boolean } | ServiceError> => sew(() => - withAuth((userId) => - withOrgMembership(userId, domain, async ({ org }) => { +export const createInvites = async (emails: string[]): Promise<{ success: boolean } | ServiceError> => sew(() => + withAuth(async ({ org, user, role, prisma }) => + withMinimumOrgRole(role, OrgRole.OWNER, async () => { const failAuditCallback = async (error: string) => { await auditService.createAudit({ action: "user.invite_failed", actor: { - id: userId, + id: user.id, type: "user" }, target: { @@ -777,17 +599,13 @@ export const createInvites = async (emails: string[], domain: string): Promise<{ } }); } - const user = await getMe(); - if (isServiceError(user)) { - throw new ServiceErrorException(user); - } - const hasAvailability = await orgHasAvailability(domain); + const hasAvailability = await orgHasAvailability(org.domain); if (!hasAvailability) { await auditService.createAudit({ action: "user.invite_failed", actor: { - id: userId, + id: user.id, type: "user" }, target: { @@ -850,7 +668,7 @@ export const createInvites = async (emails: string[], domain: string): Promise<{ await prisma.invite.createMany({ data: emails.map((email) => ({ recipientEmail: email, - hostUserId: userId, + hostUserId: user.id, orgId: org.id, })), skipDuplicates: true, @@ -884,6 +702,7 @@ export const createInvites = async (emails: string[], domain: string): Promise<{ const inviteLink = `${env.AUTH_URL}/redeem?invite_id=${invite.id}`; const transport = createTransport(smtpConnectionUrl); const html = await render(InviteUserEmail({ + baseUrl: env.AUTH_URL, host: { name: user.name ?? undefined, email: user.email!, @@ -917,7 +736,7 @@ export const createInvites = async (emails: string[], domain: string): Promise<{ await auditService.createAudit({ action: "user.invites_created", actor: { - id: userId, + id: user.id, type: "user" }, target: { @@ -932,12 +751,12 @@ export const createInvites = async (emails: string[], domain: string): Promise<{ return { success: true, } - }, /* minRequiredRole = */ OrgRole.OWNER) + }) )); -export const cancelInvite = async (inviteId: string, domain: string): Promise<{ success: boolean } | ServiceError> => sew(() => - withAuth((userId) => - withOrgMembership(userId, domain, async ({ org }) => { +export const cancelInvite = async (inviteId: string): Promise<{ success: boolean } | ServiceError> => sew(() => + withAuth(async ({ org, role, prisma }) => + withMinimumOrgRole(role, OrgRole.OWNER, async () => { const invite = await prisma.invite.findUnique({ where: { id: inviteId, @@ -958,14 +777,14 @@ export const cancelInvite = async (inviteId: string, domain: string): Promise<{ return { success: true, } - }, /* minRequiredRole = */ OrgRole.OWNER) + }) )); export const getMe = async () => sew(() => - withAuth(async (userId) => { - const user = await prisma.user.findUnique({ + withAuth(async ({ user, prisma }) => { + const userWithOrgs = await prisma.user.findUnique({ where: { - id: userId, + id: user.id, }, include: { orgs: { @@ -976,16 +795,16 @@ export const getMe = async () => sew(() => } }); - if (!user) { + if (!userWithOrgs) { return notFound(); } return { - id: user.id, - email: user.email, - name: user.name, - image: user.image, - memberships: user.orgs.map((org) => ({ + id: userWithOrgs.id, + email: userWithOrgs.email, + name: userWithOrgs.name, + image: userWithOrgs.image, + memberships: userWithOrgs.orgs.map((org) => ({ id: org.orgId, role: org.role, domain: org.org.domain, @@ -995,12 +814,7 @@ 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; - } - + withAuth_skipOrgMembershipCheck(async ({ user, prisma }) => { const invite = await prisma.invite.findUnique({ where: { id: inviteId, @@ -1074,12 +888,7 @@ 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; - } - + withAuth_skipOrgMembershipCheck(async ({ user, prisma }) => { const invite = await prisma.invite.findUnique({ where: { id: inviteId, @@ -1115,69 +924,64 @@ export const getInviteInfo = async (inviteId: string) => sew(() => } })); -export const getOrgMembers = async (domain: string) => sew(() => - withAuth(async (userId) => - withOrgMembership(userId, domain, async ({ org }) => { - const members = await prisma.userToOrg.findMany({ - where: { - orgId: org.id, - role: { - not: OrgRole.GUEST, - } - }, - include: { - user: true, - }, - }); +export const getOrgMembers = async () => sew(() => + withAuth(async ({ org, prisma }) => { + const members = await prisma.userToOrg.findMany({ + where: { + orgId: org.id, + role: { + not: OrgRole.GUEST, + } + }, + include: { + user: true, + }, + }); - return members.map((member) => ({ - id: member.userId, - email: member.user.email!, - name: member.user.name ?? undefined, - avatarUrl: member.user.image ?? undefined, - role: member.role, - joinedAt: member.joinedAt, - })); - }) - )); + return members.map((member) => ({ + id: member.userId, + email: member.user.email!, + name: member.user.name ?? undefined, + avatarUrl: member.user.image ?? undefined, + role: member.role, + joinedAt: member.joinedAt, + })); + })); -export const getOrgInvites = async (domain: string) => sew(() => - withAuth(async (userId) => - withOrgMembership(userId, domain, async ({ org }) => { - const invites = await prisma.invite.findMany({ - where: { - orgId: org.id, - }, - }); +export const getOrgInvites = async () => sew(() => + withAuth(async ({ org, prisma }) => { + const invites = await prisma.invite.findMany({ + where: { + orgId: org.id, + }, + }); - return invites.map((invite) => ({ - id: invite.id, - email: invite.recipientEmail, - createdAt: invite.createdAt, - })); - }) - )); + return invites.map((invite) => ({ + id: invite.id, + email: invite.recipientEmail, + createdAt: invite.createdAt, + })); + })); -export const getOrgAccountRequests = async (domain: string) => sew(() => - withAuth(async (userId) => - withOrgMembership(userId, domain, async ({ org }) => { - const requests = await prisma.accountRequest.findMany({ - where: { - orgId: org.id, - }, - include: { - requestedBy: true, - }, - }); +export const getOrgAccountRequests = async () => sew(() => + withAuth(async ({ org, prisma }) => { + const requests = await prisma.accountRequest.findMany({ + where: { + orgId: org.id, + }, + include: { + requestedBy: true, + }, + }); - return requests.map((request) => ({ - id: request.id, - email: request.requestedBy.email!, - createdAt: request.createdAt, - name: request.requestedBy.name ?? undefined, - })); - }) - )); + return requests.map((request) => ({ + id: request.id, + email: request.requestedBy.email!, + createdAt: request.createdAt, + name: request.requestedBy.name ?? undefined, + image: request.requestedBy.image ?? undefined, + })); + })); export const createAccountRequest = async (userId: string, domain: string) => sew(async () => { const user = await prisma.user.findUnique({ @@ -1296,9 +1100,9 @@ export const getMemberApprovalRequired = async (domain: string): Promise => sew(async () => - withAuth(async (userId) => - withOrgMembership(userId, domain, async ({ org }) => { +export const setMemberApprovalRequired = async (required: boolean): Promise<{ success: boolean } | ServiceError> => sew(async () => + withAuth(async ({ org, role, prisma }) => + withMinimumOrgRole(role, OrgRole.OWNER, async () => { await prisma.org.update({ where: { id: org.id }, data: { memberApprovalRequired: required }, @@ -1307,13 +1111,13 @@ export const setMemberApprovalRequired = async (domain: string, required: boolea return { success: true, }; - }, /* minRequiredRole = */ OrgRole.OWNER) + }) ) ); -export const setInviteLinkEnabled = async (domain: string, enabled: boolean): Promise<{ success: boolean } | ServiceError> => sew(async () => - withAuth(async (userId) => - withOrgMembership(userId, domain, async ({ org }) => { +export const setInviteLinkEnabled = async (enabled: boolean): Promise<{ success: boolean } | ServiceError> => sew(async () => + withAuth(async ({ org, role, prisma }) => + withMinimumOrgRole(role, OrgRole.OWNER, async () => { await prisma.org.update({ where: { id: org.id }, data: { inviteLinkEnabled: enabled }, @@ -1322,18 +1126,18 @@ export const setInviteLinkEnabled = async (domain: string, enabled: boolean): Pr return { success: true, }; - }, /* minRequiredRole = */ OrgRole.OWNER) + }) ) ); -export const approveAccountRequest = async (requestId: string, domain: string) => sew(async () => - withAuth(async (userId) => - withOrgMembership(userId, domain, async ({ org }) => { +export const approveAccountRequest = async (requestId: string) => sew(async () => + withAuth(async ({ org, user, role, prisma }) => + withMinimumOrgRole(role, OrgRole.OWNER, async () => { const failAuditCallback = async (error: string) => { await auditService.createAudit({ action: "user.join_request_approve_failed", actor: { - id: userId, + id: user.id, type: "user" }, target: { @@ -1387,7 +1191,7 @@ export const approveAccountRequest = async (requestId: string, domain: string) = from: env.EMAIL_FROM_ADDRESS, subject: `Your request to join ${org.name} has been approved`, html, - text: `Your request to join ${org.name} on Sourcebot has been approved. You can now access the organization at ${origin}/${org.domain}`, + text: `Your request to join ${org.name} on Sourcebot has been approved. You can now access the organization at ${env.AUTH_URL}/${org.domain}`, }); const failed = result.rejected.concat(result.pending).filter(Boolean); @@ -1401,7 +1205,7 @@ export const approveAccountRequest = async (requestId: string, domain: string) = await auditService.createAudit({ action: "user.join_request_approved", actor: { - id: userId, + id: user.id, type: "user" }, orgId: org.id, @@ -1413,12 +1217,12 @@ export const approveAccountRequest = async (requestId: string, domain: string) = return { success: true, } - }, /* minRequiredRole = */ OrgRole.OWNER) + }) )); -export const rejectAccountRequest = async (requestId: string, domain: string) => sew(() => - withAuth(async (userId) => - withOrgMembership(userId, domain, async ({ org }) => { +export const rejectAccountRequest = async (requestId: string) => sew(() => + withAuth(async ({ org, role, prisma }) => + withMinimumOrgRole(role, OrgRole.OWNER, async () => { const request = await prisma.accountRequest.findUnique({ where: { id: requestId, @@ -1438,33 +1242,31 @@ export const rejectAccountRequest = async (requestId: string, domain: string) => return { success: true, } - }, /* minRequiredRole = */ OrgRole.OWNER) + }) )); -export const getSearchContexts = async (domain: string) => sew(() => - withAuth((userId) => - withOrgMembership(userId, domain, async ({ org }) => { - const searchContexts = await prisma.searchContext.findMany({ - where: { - orgId: org.id, - }, - include: { - repos: true, - }, - }); +export const getSearchContexts = async () => sew(() => + withOptionalAuth(async ({ org, prisma }) => { + const searchContexts = await prisma.searchContext.findMany({ + where: { + orgId: org.id, + }, + include: { + repos: true, + }, + }); - return searchContexts.map((context) => ({ - id: context.id, - name: context.name, - description: context.description ?? undefined, - repoNames: context.repos.map((repo) => repo.name), - })); - }, /* minRequiredRole = */ OrgRole.GUEST), /* allowAnonymousAccess = */ true - )); + return searchContexts.map((context) => ({ + id: context.id, + name: context.name, + description: context.description ?? undefined, + repoNames: context.repos.map((repo) => repo.name), + })); + })); 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, @@ -1559,9 +1361,9 @@ export const getAnonymousAccessStatus = async (domain: string): Promise => sew(async () => { - return await withAuth(async (userId) => { - return await withOrgMembership(userId, domain, async ({ org }) => { +export const setAnonymousAccessStatus = async (enabled: boolean): Promise => sew(async () => { + return await withAuth(async ({ org, role, prisma }) => { + return await withMinimumOrgRole(role, OrgRole.OWNER, async () => { const hasAnonymousAccessEntitlement = hasEntitlement("anonymous-access"); if (!hasAnonymousAccessEntitlement) { const plan = getPlan(); @@ -1589,7 +1391,7 @@ export const setAnonymousAccessStatus = async (domain: string, enabled: boolean) }); return true; - }, /* minRequiredRole = */ OrgRole.OWNER); + }); }); }); 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]/browse/[...path]/components/pureTreePreviewPanel.tsx b/packages/web/src/app/[domain]/browse/[...path]/components/pureTreePreviewPanel.tsx index 1c4f6ae43..6b714fb99 100644 --- a/packages/web/src/app/[domain]/browse/[...path]/components/pureTreePreviewPanel.tsx +++ b/packages/web/src/app/[domain]/browse/[...path]/components/pureTreePreviewPanel.tsx @@ -5,7 +5,6 @@ import { FileTreeItemComponent } from "@/app/[domain]/browse/components/fileTree import { getBrowsePath } from "../../hooks/utils"; import { ScrollArea } from "@/components/ui/scroll-area"; import { useBrowseParams } from "../../hooks/useBrowseParams"; -import { useDomain } from "@/hooks/useDomain"; import { FileTreeItem } from "@/features/git"; interface PureTreePreviewPanelProps { @@ -15,7 +14,6 @@ interface PureTreePreviewPanelProps { export const PureTreePreviewPanel = ({ items }: PureTreePreviewPanelProps) => { const { repoName, revisionName } = useBrowseParams(); const scrollAreaRef = useRef(null); - const domain = useDomain(); return ( { revisionName, path: item.path, pathType: item.type === 'tree' ? 'tree' : 'blob', - domain, })} /> ))} diff --git a/packages/web/src/app/[domain]/browse/components/pureFileTreePanel.tsx b/packages/web/src/app/[domain]/browse/components/pureFileTreePanel.tsx index 34a64685d..a6c2dc3cf 100644 --- a/packages/web/src/app/[domain]/browse/components/pureFileTreePanel.tsx +++ b/packages/web/src/app/[domain]/browse/components/pureFileTreePanel.tsx @@ -6,7 +6,6 @@ import React, { useCallback, useMemo, useRef } from "react"; import { FileTreeItemComponent } from "./fileTreeItemComponent"; import { getBrowsePath } from "@/app/[domain]/browse/hooks/utils"; import { useBrowseParams } from "@/app/[domain]/browse/hooks/useBrowseParams"; -import { useDomain } from "@/hooks/useDomain"; const renderLoadingSkeleton = (depth: number) => { return ( @@ -28,7 +27,6 @@ interface PureFileTreePanelProps { export const PureFileTreePanel = ({ tree, openPaths, path, onTreeNodeClicked }: PureFileTreePanelProps) => { const scrollAreaRef = useRef(null); const { repoName, revisionName } = useBrowseParams(); - const domain = useDomain(); const renderTree = useCallback((nodes: FileTreeNode, depth = 0): React.ReactNode => { return ( @@ -42,7 +40,6 @@ export const PureFileTreePanel = ({ tree, openPaths, path, onTreeNodeClicked }: revisionName, path: node.path, pathType: node.type === 'tree' ? 'tree' : 'blob', - domain, })} key={node.path} node={node} @@ -80,7 +77,7 @@ export const PureFileTreePanel = ({ tree, openPaths, path, onTreeNodeClicked }: })} ); - }, [domain, onTreeNodeClicked, path, repoName, revisionName, openPaths]); + }, [onTreeNodeClicked, path, repoName, revisionName, openPaths]); const renderedTree = useMemo(() => renderTree(tree), [tree, renderTree]); diff --git a/packages/web/src/app/[domain]/browse/hooks/useBrowseNavigation.ts b/packages/web/src/app/[domain]/browse/hooks/useBrowseNavigation.ts index 0ab8e71c4..e3168bd53 100644 --- a/packages/web/src/app/[domain]/browse/hooks/useBrowseNavigation.ts +++ b/packages/web/src/app/[domain]/browse/hooks/useBrowseNavigation.ts @@ -1,13 +1,11 @@ 'use client'; import { useRouter } from "next/navigation"; -import { useDomain } from "@/hooks/useDomain"; import { useCallback } from "react"; import { getBrowsePath, GetBrowsePathProps } from "./utils"; export const useBrowseNavigation = () => { const router = useRouter(); - const domain = useDomain(); const navigateToPath = useCallback(({ repoName, @@ -16,7 +14,7 @@ export const useBrowseNavigation = () => { pathType, highlightRange, setBrowseState, - }: Omit) => { + }: GetBrowsePathProps) => { const browsePath = getBrowsePath({ repoName, revisionName, @@ -24,13 +22,12 @@ export const useBrowseNavigation = () => { pathType, highlightRange, setBrowseState, - domain, }); router.push(browsePath); - }, [domain, router]); + }, [router]); return { navigateToPath, }; -}; \ No newline at end of file +}; \ No newline at end of file diff --git a/packages/web/src/app/[domain]/browse/hooks/useBrowsePath.ts b/packages/web/src/app/[domain]/browse/hooks/useBrowsePath.ts deleted file mode 100644 index 2165025d2..000000000 --- a/packages/web/src/app/[domain]/browse/hooks/useBrowsePath.ts +++ /dev/null @@ -1,32 +0,0 @@ -'use client'; - -import { useMemo } from "react"; -import { getBrowsePath, GetBrowsePathProps } from "./utils"; -import { useDomain } from "@/hooks/useDomain"; - -export const useBrowsePath = ({ - repoName, - revisionName, - path, - pathType, - highlightRange, - setBrowseState, -}: Omit) => { - const domain = useDomain(); - - const browsePath = useMemo(() => { - return getBrowsePath({ - repoName, - revisionName, - path, - pathType, - highlightRange, - setBrowseState, - domain, - }); - }, [repoName, revisionName, path, pathType, highlightRange, setBrowseState, domain]); - - return { - path: browsePath, - } -} \ No newline at end of file diff --git a/packages/web/src/app/[domain]/browse/hooks/utils.ts b/packages/web/src/app/[domain]/browse/hooks/utils.ts index b0aa3100d..77cbb9ab2 100644 --- a/packages/web/src/app/[domain]/browse/hooks/utils.ts +++ b/packages/web/src/app/[domain]/browse/hooks/utils.ts @@ -1,4 +1,5 @@ import { BrowseState, SET_BROWSE_STATE_QUERY_PARAM } from "../browseStateProvider"; +import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants"; export const HIGHLIGHT_RANGE_QUERY_PARAM = 'highlightRange'; @@ -17,7 +18,6 @@ export interface GetBrowsePathProps { pathType: 'blob' | 'tree'; highlightRange?: BrowseHighlightRange; setBrowseState?: Partial; - domain: string; } export const getBrowseParamsFromPathParam = (pathParam: string) => { @@ -64,8 +64,9 @@ export const getBrowseParamsFromPathParam = (pathParam: string) => { }; export const getBrowsePath = ({ - repoName, revisionName, path, pathType, highlightRange, setBrowseState, domain, + repoName, revisionName, path, pathType, highlightRange, setBrowseState, }: GetBrowsePathProps) => { + const domain = SINGLE_TENANT_ORG_DOMAIN; const params = new URLSearchParams(); if (highlightRange) { diff --git a/packages/web/src/app/[domain]/chat/[id]/opengraph-image.tsx b/packages/web/src/app/[domain]/chat/[id]/opengraph-image.tsx index 4d3176416..cfde021db 100644 --- a/packages/web/src/app/[domain]/chat/[id]/opengraph-image.tsx +++ b/packages/web/src/app/[domain]/chat/[id]/opengraph-image.tsx @@ -4,6 +4,7 @@ import { prisma } from '@/prisma'; import { getOrgFromDomain } from '@/data/org'; import { ChatVisibility } from '@sourcebot/db'; import { env } from "@sourcebot/shared"; +import { minidenticon } from 'minidenticons'; export const runtime = 'nodejs'; export const alt = 'Sourcebot Chat'; @@ -37,6 +38,7 @@ export default async function Image({ params }: ImageProps) { createdBy: { select: { name: true, + email: true, image: true, }, }, @@ -53,7 +55,9 @@ export default async function Image({ params }: ImageProps) { const chatName = rawChatName.length > MAX_CHAT_NAME_LENGTH ? rawChatName.substring(0, MAX_CHAT_NAME_LENGTH).trim() + '...' : rawChatName; - const creatorImage = chat.createdBy?.image; + const creatorEmail = chat.createdBy?.email; + const creatorImage = chat.createdBy?.image + ?? (creatorEmail ? 'data:image/svg+xml;utf8,' + encodeURIComponent(minidenticon(creatorEmail, 50, 50)) : undefined); return new ImageResponse( ( diff --git a/packages/web/src/app/[domain]/chat/[id]/page.tsx b/packages/web/src/app/[domain]/chat/[id]/page.tsx index d1c57fe5a..2fd31d7ad 100644 --- a/packages/web/src/app/[domain]/chat/[id]/page.tsx +++ b/packages/web/src/app/[domain]/chat/[id]/page.tsx @@ -104,7 +104,7 @@ export default async function Page(props: PageProps) { const languageModels = await getConfiguredLanguageModelsInfo(); const repos = await getRepos(); - const searchContexts = await getSearchContexts(params.domain); + const searchContexts = await getSearchContexts(); const chatInfo = await getChatInfo({ chatId: params.id }); const chatHistory = session ? await getUserChatHistory() : []; diff --git a/packages/web/src/app/[domain]/chat/components/shareChatPopover/ee/invitePanel.tsx b/packages/web/src/app/[domain]/chat/components/shareChatPopover/ee/invitePanel.tsx index 8e9db0888..146a209b0 100644 --- a/packages/web/src/app/[domain]/chat/components/shareChatPopover/ee/invitePanel.tsx +++ b/packages/web/src/app/[domain]/chat/components/shareChatPopover/ee/invitePanel.tsx @@ -3,13 +3,12 @@ import { searchChatShareableMembers } from "@/app/api/(client)/client"; import { SearchChatShareableMembersResponse } from "@/app/api/(server)/ee/chat/[chatId]/searchMembers/route"; import { SessionUser } from "@/auth"; -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { LoadingButton } from "@/components/ui/loading-button"; import { Separator } from "@/components/ui/separator"; import { unwrapServiceError } from "@/lib/utils"; -import placeholderAvatar from "@/public/placeholder_avatar.png"; +import { UserAvatar } from "@/components/userAvatar"; import { useQuery } from "@tanstack/react-query"; import { useDebounce } from "@uidotdev/usehooks"; import { ChevronLeft, Circle, CircleCheck, Loader2, X } from "lucide-react"; @@ -33,17 +32,6 @@ export const InvitePanel = ({ const resultsRef = useRef(null); const inputRef = useRef(null); - const getInitials = (name?: string, email?: string) => { - if (name) { - return name.split(' ').map((n) => n[0]).join('').toUpperCase().slice(0, 2); - } - if (email) { - return email[0].toUpperCase(); - } - return '?'; - }; - - const debouncedSearchQuery = useDebounce(searchQuery, 100); const { data: searchResults, isPending, isError } = useQuery({ @@ -157,10 +145,11 @@ export const InvitePanel = ({ ) : ( )} - - - {getInitials(user.name ?? undefined, user.email ?? undefined)} - +
{user.name || user.email} {user.name && ( diff --git a/packages/web/src/app/[domain]/chat/components/shareChatPopover/shareSettings.tsx b/packages/web/src/app/[domain]/chat/components/shareChatPopover/shareSettings.tsx index bf1335f82..9dd0a6668 100644 --- a/packages/web/src/app/[domain]/chat/components/shareChatPopover/shareSettings.tsx +++ b/packages/web/src/app/[domain]/chat/components/shareChatPopover/shareSettings.tsx @@ -2,7 +2,6 @@ import { SessionUser } from "@/auth"; import { useToast } from "@/components/hooks/use-toast"; -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Button } from "@/components/ui/button"; import { Select, @@ -13,7 +12,7 @@ import { } from "@/components/ui/select"; import { Separator } from "@/components/ui/separator"; import { cn } from "@/lib/utils"; -import placeholderAvatar from "@/public/placeholder_avatar.png"; +import { UserAvatar } from "@/components/userAvatar"; import { ChatVisibility } from "@sourcebot/db"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { Info, Link2Icon, Loader2, Lock, X } from "lucide-react"; @@ -69,16 +68,6 @@ export const ShareSettings = ({ } }, [chatId, visibility, toast]); - const getInitials = (name?: string | null, email?: string | null) => { - if (name) { - return name.split(' ').map((n) => n[0]).join('').toUpperCase().slice(0, 2); - } - if (email) { - return email[0].toUpperCase(); - } - return '?'; - }; - return (

Share

@@ -113,10 +102,11 @@ export const ShareSettings = ({ {currentUser && (
- - - {getInitials(currentUser.name, currentUser.email)} - +
{currentUser.name || currentUser.email} @@ -134,10 +124,11 @@ export const ShareSettings = ({ {sharedWithUsers.map((user) => (
- - - {getInitials(user.name, user.email)} - +
{user.name || user.email} {user.name && ( diff --git a/packages/web/src/app/[domain]/chat/page.tsx b/packages/web/src/app/[domain]/chat/page.tsx index f7c98adbb..61b33b33f 100644 --- a/packages/web/src/app/[domain]/chat/page.tsx +++ b/packages/web/src/app/[domain]/chat/page.tsx @@ -27,7 +27,7 @@ interface PageProps { export default async function Page(props: PageProps) { const params = await props.params; const languageModels = await getConfiguredLanguageModelsInfo(); - const searchContexts = await getSearchContexts(params.domain); + const searchContexts = await getSearchContexts(); const allRepos = await getRepos(); const session = await auth(); const chatHistory = session ? await getUserChatHistory() : []; diff --git a/packages/web/src/app/[domain]/components/codeHostIconButton.tsx b/packages/web/src/app/[domain]/components/codeHostIconButton.tsx deleted file mode 100644 index 796adf4e9..000000000 --- a/packages/web/src/app/[domain]/components/codeHostIconButton.tsx +++ /dev/null @@ -1,35 +0,0 @@ -'use client'; - -import { Button } from "@/components/ui/button"; -import useCaptureEvent from "@/hooks/useCaptureEvent"; -import { cn } from "@/lib/utils"; -import Image from "next/image"; - -interface CodeHostIconButton { - name: string; - logo: { src: string, className?: string }; - onClick: () => void; -} - -export const CodeHostIconButton = ({ - name, - logo, - onClick, -}: CodeHostIconButton) => { - const captureEvent = useCaptureEvent(); - return ( - - ) -} \ No newline at end of file diff --git a/packages/web/src/app/[domain]/components/configEditor.tsx b/packages/web/src/app/[domain]/components/configEditor.tsx deleted file mode 100644 index 63ba1acde..000000000 --- a/packages/web/src/app/[domain]/components/configEditor.tsx +++ /dev/null @@ -1,285 +0,0 @@ -'use client'; - -import { ScrollArea } from "@/components/ui/scroll-area"; -import { useKeymapExtension } from "@/hooks/useKeymapExtension"; -import { json, jsonLanguage, jsonParseLinter } from "@codemirror/lang-json"; -import { linter } from "@codemirror/lint"; -import { EditorView, hoverTooltip } from "@codemirror/view"; -import CodeMirror, { Extension, ReactCodeMirrorRef } from "@uiw/react-codemirror"; -import { useRef, forwardRef, useImperativeHandle, Ref, ReactNode, useState, useEffect } from "react"; -import { Button } from "@/components/ui/button"; -import { Separator } from "@/components/ui/separator"; -import { Schema } from "ajv"; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; -import useCaptureEvent from "@/hooks/useCaptureEvent"; -import { useCodeMirrorTheme } from "@/hooks/useCodeMirrorTheme"; -import { CodeHostType } from "@sourcebot/db"; - -export type QuickActionFn = (previous: T) => T; -export type QuickAction = { - name: string; - fn: QuickActionFn; - description?: string | ReactNode; - selectionText?: string; -}; - -interface ConfigEditorProps { - value: string; - type: CodeHostType; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - onChange: (...event: any[]) => void; - actions: QuickAction[], - schema: Schema; -} - -const customAutocompleteStyle = EditorView.baseTheme({ - ".cm-tooltip.cm-completionInfo": { - padding: "8px", - fontSize: "12px", - fontFamily: "monospace", - }, - ".cm-tooltip-hover.cm-tooltip": { - padding: "8px", - fontSize: "12px", - fontFamily: "monospace", - } -}); - -export function onQuickAction( - action: QuickActionFn, - config: string, - view: EditorView, - options?: { - focusEditor?: boolean; - moveCursor?: boolean; - selectionText?: string; - } -) { - const { - focusEditor = false, - moveCursor = true, - selectionText = `""`, - } = options ?? {}; - - let previousConfig: T; - try { - previousConfig = JSON.parse(config) as T; - } catch { - return; - } - - const nextConfig = action(previousConfig); - const next = JSON.stringify(nextConfig, null, 2); - - if (focusEditor) { - view.focus(); - } - - view.dispatch({ - changes: { - from: 0, - to: config.length, - insert: next, - } - }); - - if (moveCursor && selectionText) { - const cursorPos = next.lastIndexOf(selectionText); - if (cursorPos >= 0) { - view.dispatch({ - selection: { - anchor: cursorPos, - head: cursorPos + selectionText.length - } - }); - } - } -} - -export const isConfigValidJson = (config: string) => { - try { - JSON.parse(config); - return true; - } catch (_e) { - return false; - } -} - -const DEFAULT_ACTIONS_VISIBLE = 4; - -const ConfigEditor = (props: ConfigEditorProps, forwardedRef: Ref) => { - const { value, type, onChange, actions, schema } = props; - const captureEvent = useCaptureEvent(); - const editorRef = useRef(null); - const [isViewMoreActionsEnabled, setIsViewMoreActionsEnabled] = useState(false); - const [height, setHeight] = useState(224); - useImperativeHandle( - forwardedRef, - () => editorRef.current as ReactCodeMirrorRef - ); - - const keymapExtension = useKeymapExtension(editorRef.current?.view); - const theme = useCodeMirrorTheme(); - - // ⚠️ DISGUSTING HACK AHEAD ⚠️ - // Background: When navigating to the /connections/:id?tab=settings page, we were hitting a 500 error with the following - // message server side: - // - // > Internal error: Error: Element type is invalid: expected a string (for built-in components) or a class/function - // > (for composite components) but got: undefined. You likely forgot to export your component from the file it's - // > defined in, or you might have mixed up default and named imports. - // - // Why was this happening? We have no idea, but we isolated it to the extensions exported by the `codemirror-json-schema` - // package. The solution that worked was to dynamically import the package inside of the useEffect and load the extensions - // async. - // - // So, yeah. - Brendan - const [jsonSchemaExtensions, setJsonSchemaExtensions] = useState([]); - useEffect(() => { - const loadExtensions = async () => { - const { - handleRefresh, - jsonCompletion, - jsonSchemaHover, - jsonSchemaLinter, - stateExtensions - } = await import('codemirror-json-schema'); - return [ - linter(jsonParseLinter(), { - delay: 300, - }), - linter(jsonSchemaLinter(), { - needsRefresh: handleRefresh, - }), - jsonLanguage.data.of({ - autocomplete: jsonCompletion(), - }), - hoverTooltip(jsonSchemaHover()), - // eslint-disable-next-line @typescript-eslint/no-explicit-any - stateExtensions(schema as any), - ] - } - - loadExtensions().then((extensions) => { - console.debug('Loaded json schema extensions'); - setJsonSchemaExtensions(extensions); - }); - }, [schema]); - - return ( -
-
- - {actions - .slice(0, isViewMoreActionsEnabled ? actions.length : DEFAULT_ACTIONS_VISIBLE) - .map(({ name, fn, description, selectionText }, index, truncatedActions) => ( -
- - - - - - - {index !== truncatedActions.length - 1 && ( - - )} - {index === truncatedActions.length - 1 && truncatedActions.length < actions.length && ( - <> - - - - )} -
- ))} - -
-
- - - - - -
{ - e.preventDefault(); - const startY = e.clientY; - const startHeight = height; - - function onMouseMove(e: MouseEvent) { - const delta = e.clientY - startY; - setHeight(Math.max(112, startHeight + delta)); - } - - function onMouseUp() { - document.removeEventListener('mousemove', onMouseMove); - document.removeEventListener('mouseup', onMouseUp); - } - - document.addEventListener('mousemove', onMouseMove); - document.addEventListener('mouseup', onMouseUp); - }} - /> -
- ) -}; - -// @see: https://stackoverflow.com/a/78692562 -export default forwardRef(ConfigEditor) as ( - props: ConfigEditorProps & { ref?: Ref }, -) => ReturnType; diff --git a/packages/web/src/app/[domain]/components/meControlDropdownMenu.tsx b/packages/web/src/app/[domain]/components/meControlDropdownMenu.tsx index ae3263cb5..d9e4ee782 100644 --- a/packages/web/src/app/[domain]/components/meControlDropdownMenu.tsx +++ b/packages/web/src/app/[domain]/components/meControlDropdownMenu.tsx @@ -13,13 +13,12 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { cn } from "@/lib/utils" -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { signOut } from "next-auth/react" import posthog from "posthog-js"; import { useDomain } from "@/hooks/useDomain"; import { Session } from "next-auth"; import { AppearanceDropdownMenuGroup } from "./appearanceDropdownMenuGroup"; -import placeholderAvatar from "@/public/placeholder_avatar.png"; +import { UserAvatar } from "@/components/userAvatar"; interface MeControlDropdownMenuProps { menuButtonClassName?: string; @@ -35,24 +34,20 @@ export const MeControlDropdownMenu = ({ return ( - - - - {session.user.name && session.user.name.length > 0 ? session.user.name[0].toUpperCase() : 'U'} - - +
- - - - {session.user.name && session.user.name.length > 0 ? session.user.name[0].toUpperCase() : 'U'} - - +

{session.user.name ?? "User"}

{session.user.email && ( diff --git a/packages/web/src/app/[domain]/components/navigationMenu/index.tsx b/packages/web/src/app/[domain]/components/navigationMenu/index.tsx index 8c70f5699..b47afb67f 100644 --- a/packages/web/src/app/[domain]/components/navigationMenu/index.tsx +++ b/packages/web/src/app/[domain]/components/navigationMenu/index.tsx @@ -4,19 +4,14 @@ import { auth } from "@/auth"; import { Button } from "@/components/ui/button"; import { NavigationMenu as NavigationMenuBase } from "@/components/ui/navigation-menu"; import { Separator } from "@/components/ui/separator"; -import { getSubscriptionInfo } from "@/ee/features/billing/actions"; -import { IS_BILLING_ENABLED } from "@/ee/features/billing/stripe"; -import { env } from "@sourcebot/shared"; import { ServiceErrorException } from "@/lib/serviceError"; import { isServiceError } from "@/lib/utils"; import { OrgRole, RepoIndexingJobStatus, RepoIndexingJobType } from "@sourcebot/db"; import Link from "next/link"; -import { OrgSelector } from "../orgSelector"; import { MeControlDropdownMenu } from "../meControlDropdownMenu"; import WhatsNewIndicator from "../whatsNewIndicator"; import { NavigationItems } from "./navigationItems"; import { ProgressIndicator } from "./progressIndicator"; -import { TrialIndicator } from "./trialIndicator"; import { redirect } from "next/navigation"; import { AppearanceDropdownMenu } from "../appearanceDropdownMenu"; @@ -28,7 +23,6 @@ interface NavigationMenuProps { export const NavigationMenu = async ({ domain, }: NavigationMenuProps) => { - const subscription = IS_BILLING_ENABLED ? await getSubscriptionInfo(domain) : null; const session = await auth(); const isAuthenticated = session?.user !== undefined; @@ -37,7 +31,7 @@ export const NavigationMenu = async ({ throw new ServiceErrorException(repoStats); } - const role = isAuthenticated ? await getCurrentUserRole(domain) : null; + const role = isAuthenticated ? await getCurrentUserRole() : null; if (isServiceError(role)) { throw new ServiceErrorException(role); } @@ -47,7 +41,7 @@ export const NavigationMenu = async ({ return null; } - const joinRequests = await getOrgAccountRequests(domain); + const joinRequests = await getOrgAccountRequests(); if (isServiceError(joinRequests)) { throw new ServiceErrorException(joinRequests); } @@ -104,15 +98,6 @@ export const NavigationMenu = async ({ /> - {env.SOURCEBOT_TENANCY_MODE === 'multi' && ( - <> - - - - )} - - {session ? ( { - const domain = useDomain(); - const captureEvent = useCaptureEvent(); - - if (isServiceError(subscription)) { - captureEvent('wa_trial_nav_subscription_fetch_fail', { - errorCode: subscription.errorCode, - }); - return null; - } - - if (!subscription || subscription.status !== "trialing") { - return null; - } - - return ( - captureEvent('wa_trial_nav_pressed', {})}> -
- - - {/* eslint-disable-next-line react-hooks/purity -- Date.now() during render is intentional for displaying remaining trial days */} - {Math.ceil((subscription.nextBillingDate * 1000 - Date.now()) / (1000 * 60 * 60 * 24))} days left in trial - -
- - ); -}; diff --git a/packages/web/src/app/[domain]/components/orgSelector/index.tsx b/packages/web/src/app/[domain]/components/orgSelector/index.tsx deleted file mode 100644 index 769a072ec..000000000 --- a/packages/web/src/app/[domain]/components/orgSelector/index.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { OrgSelectorDropdown } from "./orgSelectorDropdown"; -import { prisma } from "@/prisma"; -import { getMe } from "@/actions"; -import { isServiceError } from "@/lib/utils"; - -interface OrgSelectorProps { - domain: string; -} - -export const OrgSelector = async ({ - domain, -}: OrgSelectorProps) => { - const user = await getMe(); - if (isServiceError(user)) { - return null; - } - - const activeOrg = await prisma.org.findUnique({ - where: { - domain, - } - }); - - if (!activeOrg) { - return null; - } - - return ( - ({ - name, - domain, - id, - }))} - activeOrgId={activeOrg.id} - /> - ) -} \ No newline at end of file diff --git a/packages/web/src/app/[domain]/components/orgSelector/orgIcon.tsx b/packages/web/src/app/[domain]/components/orgSelector/orgIcon.tsx deleted file mode 100644 index 2be5bedc8..000000000 --- a/packages/web/src/app/[domain]/components/orgSelector/orgIcon.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { cn } from "@/lib/utils"; -import placeholderAvatar from "@/public/placeholder_avatar.png"; -import { cva } from "class-variance-authority"; -import Image from "next/image"; - -interface OrgIconProps { - className?: string; - size?: "default"; -} - -const iconVariants = cva( - "rounded-full", - { - variants: { - size: { - default: "w-5 h-5" - } - }, - defaultVariants: { - size: "default" - } - }, -) - -export const OrgIcon = ({ - className, - size, -}: OrgIconProps) => { - return ( - Organization avatar - ) -} \ No newline at end of file diff --git a/packages/web/src/app/[domain]/components/orgSelector/orgSelectorDropdown.tsx b/packages/web/src/app/[domain]/components/orgSelector/orgSelectorDropdown.tsx deleted file mode 100644 index 42c1ff150..000000000 --- a/packages/web/src/app/[domain]/components/orgSelector/orgSelectorDropdown.tsx +++ /dev/null @@ -1,129 +0,0 @@ -'use client'; - -import { useToast } from "@/components/hooks/use-toast"; -import { Button } from "@/components/ui/button"; -import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; -import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; -import { CaretSortIcon, CheckIcon, PlusCircledIcon } from "@radix-ui/react-icons"; -import { useRouter } from "next/navigation"; -import { useCallback, useMemo, useState } from "react"; -import { OrgIcon } from "./orgIcon"; - - -interface OrgSelectorDropdownProps { - orgs: { - name: string, - domain: string, - id: number, - }[], - activeOrgId: number, -} - -export const OrgSelectorDropdown = ({ - orgs: _orgs, - activeOrgId -}: OrgSelectorDropdownProps) => { - const [searchFilter, setSearchFilter] = useState(""); - const [isDropdownOpen, setIsDropdownOpen] = useState(false); - const { toast } = useToast(); - const router = useRouter(); - - const activeOrg = _orgs.find((org) => org.id === activeOrgId)!; - const orgs = useMemo(() => { - // always place the active org at the top - return [ - activeOrg, - ..._orgs.filter(org => org.id !== activeOrgId), - ]; - }, [_orgs, activeOrg, activeOrgId]); - - const onSwitchOrg = useCallback((domain: string, orgName: string) => { - router.push(`/${domain}`); - toast({ - description: `✅ Switched to ${orgName}`, - }); - }, [router, toast]); - - return ( - /* - We need to set `modal=false` to fix a issue with having a dialog menu inside of - a dropdown menu. - @see : https://github.com/radix-ui/primitives/issues/1836#issuecomment-1547607143 - */ - - - - - - - - setSearchFilter(value)} - autoFocus={true} - /> - - -

No organization found

-

{`Your search term "${searchFilter}" did not match any organizations.`}

- -
- - {orgs.map((org, index) => ( - onSwitchOrg(org.domain, org.name)} - > -
- - {org.name} -
- {org.id === activeOrgId && ( - - )} -
- ))} -
-
-
-
- {searchFilter.length === 0 && ( - - - - - )} -
-
- ); -} diff --git a/packages/web/src/app/[domain]/components/pathHeader.tsx b/packages/web/src/app/[domain]/components/pathHeader.tsx index 2b4298e22..048e1d641 100644 --- a/packages/web/src/app/[domain]/components/pathHeader.tsx +++ b/packages/web/src/app/[domain]/components/pathHeader.tsx @@ -15,7 +15,6 @@ import { import { VscodeFileIcon } from "@/app/components/vscodeFileIcon"; import { CopyIconButton } from "./copyIconButton"; import Link from "next/link"; -import { useDomain } from "@/hooks/useDomain"; import { CodeHostType } from "@sourcebot/db"; interface FileHeaderProps { @@ -74,8 +73,6 @@ export const PathHeader = ({ const containerRef = useRef(null); const breadcrumbsRef = useRef(null); const [visibleSegmentCount, setVisibleSegmentCount] = useState(null); - const domain = useDomain(); - // Create breadcrumb segments from file path const breadcrumbSegments = useMemo(() => { const pathParts = path.split('/').filter(Boolean); @@ -223,7 +220,6 @@ export const PathHeader = ({ path: '/', pathType: 'tree', revisionName, - domain, })} > {info?.displayName} @@ -262,7 +258,6 @@ export const PathHeader = ({ path: segment.fullPath, pathType: segment.isLastSegment ? pathType : 'tree', revisionName, - domain, })} className="font-mono text-sm hover:cursor cursor-pointer" key={segment.fullPath} @@ -291,7 +286,6 @@ export const PathHeader = ({ path: segment.fullPath, pathType: segment.isLastSegment ? pathType : 'tree', revisionName, - domain, })} > {renderSegmentWithHighlight(segment)} diff --git a/packages/web/src/app/[domain]/components/searchBar/useSuggestionsData.ts b/packages/web/src/app/[domain]/components/searchBar/useSuggestionsData.ts index 92c9bb401..61a05b458 100644 --- a/packages/web/src/app/[domain]/components/searchBar/useSuggestionsData.ts +++ b/packages/web/src/app/[domain]/components/searchBar/useSuggestionsData.ts @@ -107,7 +107,7 @@ export const useSuggestionsData = ({ const { data: searchContextSuggestions, isLoading: _isLoadingSearchContexts } = useQuery({ queryKey: ["searchContexts", domain], - queryFn: () => getSearchContexts(domain), + queryFn: () => getSearchContexts(), select: (data): Suggestion[] => { if (isServiceError(data)) { return []; diff --git a/packages/web/src/app/[domain]/components/upgradeGuard.tsx b/packages/web/src/app/[domain]/components/upgradeGuard.tsx deleted file mode 100644 index f948eafa7..000000000 --- a/packages/web/src/app/[domain]/components/upgradeGuard.tsx +++ /dev/null @@ -1,31 +0,0 @@ -'use client'; - -import { Redirect } from "@/app/components/redirect"; -import { useDomain } from "@/hooks/useDomain"; -import { usePathname } from "next/navigation"; -import { useMemo } from "react"; - -interface UpgradeGuardProps { - children: React.ReactNode; -} - -export const UpgradeGuard = ({ children }: UpgradeGuardProps) => { - const domain = useDomain(); - const pathname = usePathname(); - - const content = useMemo(() => { - if (!pathname.endsWith('/upgrade')) { - return ( - - ) - } else { - return children; - } - }, [domain, children, pathname]); - - return content; -} - - diff --git a/packages/web/src/app/[domain]/layout.tsx b/packages/web/src/app/[domain]/layout.tsx index 3a1f48b05..3a4371cae 100644 --- a/packages/web/src/app/[domain]/layout.tsx +++ b/packages/web/src/app/[domain]/layout.tsx @@ -3,16 +3,13 @@ import { auth } from "@/auth"; import { getOrgFromDomain } from "@/data/org"; import { isServiceError } from "@/lib/utils"; import { OnboardGuard } from "./components/onboardGuard"; -import { UpgradeGuard } from "./components/upgradeGuard"; import { cookies, headers } from "next/headers"; import { getSelectorsByUserAgent } from "react-device-detect"; import { MobileUnsupportedSplashScreen } from "./components/mobileUnsupportedSplashScreen"; import { MOBILE_UNSUPPORTED_SPLASH_SCREEN_DISMISSED_COOKIE_NAME, OPTIONAL_PROVIDERS_LINK_SKIPPED_COOKIE_NAME } from "@/lib/constants"; import { SyntaxReferenceGuide } from "./components/syntaxReferenceGuide"; import { SyntaxGuideProvider } from "./components/syntaxGuideProvider"; -import { IS_BILLING_ENABLED } from "@/ee/features/billing/stripe"; import { notFound, redirect } from "next/navigation"; -import { getSubscriptionInfo } from "@/ee/features/billing/actions"; import { PendingApprovalCard } from "./components/pendingApproval"; import { SubmitJoinRequest } from "./components/submitJoinRequest"; import { hasEntitlement } from "@sourcebot/shared"; @@ -152,23 +149,6 @@ export default async function Layout(props: LayoutProps) { } } - if (IS_BILLING_ENABLED) { - const subscription = await getSubscriptionInfo(domain); - if ( - subscription && - ( - isServiceError(subscription) || - (subscription.status !== "active" && subscription.status !== "trialing") - ) - ) { - return ( - - {children} - - ) - } - } - const headersList = await headers(); const cookieStore = await cookies() const userAgent = headersList.get('user-agent'); diff --git a/packages/web/src/app/[domain]/repos/[id]/page.tsx b/packages/web/src/app/[domain]/repos/[id]/page.tsx index ca3ab28aa..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" @@ -52,7 +53,7 @@ export default async function RepoDetailPage({ params }: { params: Promise<{ id: const repoMetadata = repoMetadataSchema.parse(repo.metadata); - const userRole = await getCurrentUserRole(SINGLE_TENANT_ORG_DOMAIN); + const userRole = await getCurrentUserRole(); if (isServiceError(userRole)) { throw new ServiceErrorException(userRole); } @@ -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/components/addRepositoryDialog.tsx b/packages/web/src/app/[domain]/repos/components/addRepositoryDialog.tsx deleted file mode 100644 index 45a8b6c60..000000000 --- a/packages/web/src/app/[domain]/repos/components/addRepositoryDialog.tsx +++ /dev/null @@ -1,126 +0,0 @@ -'use client'; - -import { Button } from "@/components/ui/button"; -import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; -import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { useForm } from "react-hook-form"; -import { z } from "zod"; -import { experimental_addGithubRepositoryByUrl } from "@/actions"; -import { isServiceError } from "@/lib/utils"; -import { useToast } from "@/components/hooks/use-toast"; -import { useRouter } from "next/navigation"; - -interface AddRepositoryDialogProps { - isOpen: boolean; - onOpenChange: (open: boolean) => void; -} - -// Validation schema for repository URLs -const formSchema = z.object({ - repositoryUrl: z.string() - .min(1, "Repository URL is required") - .refine((url) => { - // Allow various GitHub URL formats: - // - https://github.com/owner/repo - // - github.com/owner/repo - // - owner/repo - const patterns = [ - /^https?:\/\/github\.com\/[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+\/?$/, - /^github\.com\/[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+\/?$/, - /^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/ - ]; - return patterns.some(pattern => pattern.test(url.trim())); - }, "Please enter a valid GitHub repository URL (e.g., owner/repo or https://github.com/owner/repo)"), -}); - -export const AddRepositoryDialog = ({ isOpen, onOpenChange }: AddRepositoryDialogProps) => { - const { toast } = useToast(); - const router = useRouter(); - - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: { - repositoryUrl: "", - }, - }); - - const { isSubmitting } = form.formState; - - const onSubmit = async (data: z.infer) => { - - const result = await experimental_addGithubRepositoryByUrl(data.repositoryUrl.trim()); - if (isServiceError(result)) { - toast({ - title: "Error adding repository", - description: result.message, - variant: "destructive", - }); - } else { - toast({ - title: "Repository added successfully!", - description: "It will be indexed shortly.", - }); - form.reset(); - onOpenChange(false); - router.refresh(); - } - }; - - const handleCancel = () => { - form.reset(); - onOpenChange(false); - }; - - return ( - - - - Add a public repository from GitHub - - Paste the repo URL - the code will be indexed and available in search. - - - -
- - ( - - Repository URL - - - - - - )} - /> - - - - - - - -
-
- ); -}; diff --git a/packages/web/src/app/[domain]/repos/components/reposTable.tsx b/packages/web/src/app/[domain]/repos/components/reposTable.tsx index 0578b95e2..1228f690e 100644 --- a/packages/web/src/app/[domain]/repos/components/reposTable.tsx +++ b/packages/web/src/app/[domain]/repos/components/reposTable.tsx @@ -138,7 +138,6 @@ export const getColumns = (context: ColumnsContext): ColumnDef[] => [ repoName: repo.name, path: '/', pathType: 'tree', - domain: SINGLE_TENANT_ORG_DOMAIN, })} className="font-medium hover:underline" > diff --git a/packages/web/src/app/[domain]/repos/layout.tsx b/packages/web/src/app/[domain]/repos/layout.tsx index 675fdf949..4da2c4f08 100644 --- a/packages/web/src/app/[domain]/repos/layout.tsx +++ b/packages/web/src/app/[domain]/repos/layout.tsx @@ -24,7 +24,7 @@ export default async function Layout( throw new ServiceErrorException(repoStats); } - const userRoleInOrg = await getCurrentUserRole(domain); + const userRoleInOrg = await getCurrentUserRole(); return (
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]/search/components/searchResultsPanel/fileMatch.tsx b/packages/web/src/app/[domain]/search/components/searchResultsPanel/fileMatch.tsx index 334333927..153f85e3c 100644 --- a/packages/web/src/app/[domain]/search/components/searchResultsPanel/fileMatch.tsx +++ b/packages/web/src/app/[domain]/search/components/searchResultsPanel/fileMatch.tsx @@ -4,7 +4,6 @@ import { SearchResultFile, SearchResultChunk } from "@/features/search"; import { LightweightCodeHighlighter } from "@/app/[domain]/components/lightweightCodeHighlighter"; import Link from "next/link"; import { getBrowsePath } from "@/app/[domain]/browse/hooks/utils"; -import { useDomain } from "@/hooks/useDomain"; interface FileMatchProps { @@ -16,8 +15,6 @@ export const FileMatch = ({ match, file, }: FileMatchProps) => { - const domain = useDomain(); - // If it's just the title, don't show a code preview if (match.matchRanges.length === 0) { return null; @@ -32,7 +29,6 @@ export const FileMatch = ({ revisionName: file.branches?.[0] ?? 'HEAD', path: file.fileName.text, pathType: 'blob', - domain, highlightRange: { start: { lineNumber: match.contentStart.lineNumber, diff --git a/packages/web/src/app/[domain]/settings/apiKeys/apiKeysPage.tsx b/packages/web/src/app/[domain]/settings/apiKeys/apiKeysPage.tsx index 0d86a2b8e..628d1d50b 100644 --- a/packages/web/src/app/[domain]/settings/apiKeys/apiKeysPage.tsx +++ b/packages/web/src/app/[domain]/settings/apiKeys/apiKeysPage.tsx @@ -7,7 +7,6 @@ import { Input } from "@/components/ui/input"; import { isServiceError } from "@/lib/utils"; import { Copy, Check, AlertTriangle, Loader2, Plus } from "lucide-react"; import { useCallback, useEffect, useMemo, useState } from "react"; -import { useDomain } from "@/hooks/useDomain"; import { useToast } from "@/components/hooks/use-toast"; import useCaptureEvent from "@/hooks/useCaptureEvent"; import { DataTable } from "@/components/ui/data-table"; @@ -16,7 +15,6 @@ import { Skeleton } from "@/components/ui/skeleton"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; export function ApiKeysPage({ canCreateApiKey }: { canCreateApiKey: boolean }) { - const domain = useDomain(); const { toast } = useToast(); const captureEvent = useCaptureEvent(); @@ -33,7 +31,7 @@ export function ApiKeysPage({ canCreateApiKey }: { canCreateApiKey: boolean }) { setIsLoading(true); setError(null); try { - const keys = await getUserApiKeys(domain); + const keys = await getUserApiKeys(); if (isServiceError(keys)) { setError("Failed to load API keys"); toast({ @@ -55,7 +53,7 @@ export function ApiKeysPage({ canCreateApiKey }: { canCreateApiKey: boolean }) { } finally { setIsLoading(false); } - }, [domain, toast]); + }, [toast]); useEffect(() => { loadApiKeys(); @@ -73,7 +71,7 @@ export function ApiKeysPage({ canCreateApiKey }: { canCreateApiKey: boolean }) { setIsCreatingKey(true); try { - const result = await createApiKey(newKeyName.trim(), domain); + const result = await createApiKey(newKeyName.trim()); if (isServiceError(result)) { toast({ title: "Error", diff --git a/packages/web/src/app/[domain]/settings/apiKeys/columns.tsx b/packages/web/src/app/[domain]/settings/apiKeys/columns.tsx index a32bfabba..c219db553 100644 --- a/packages/web/src/app/[domain]/settings/apiKeys/columns.tsx +++ b/packages/web/src/app/[domain]/settings/apiKeys/columns.tsx @@ -4,7 +4,6 @@ import { ColumnDef } from "@tanstack/react-table" import { ArrowUpDown, Key, Trash2 } from "lucide-react" import { Button } from "@/components/ui/button" import { deleteApiKey } from "@/actions" -import { useParams } from "next/navigation" import { AlertDialog, AlertDialogAction, @@ -27,14 +26,13 @@ export type ApiKeyColumnInfo = { // Component for the actions cell to properly use React hooks function ApiKeyActions({ apiKey }: { apiKey: ApiKeyColumnInfo }) { - const params = useParams<{ domain: string }>() const [isPending, setIsPending] = useState(false) const { toast } = useToast() const handleDelete = async () => { setIsPending(true) try { - await deleteApiKey(apiKey.name, params.domain) + await deleteApiKey(apiKey.name) window.location.reload() } catch (error) { console.error("Failed to delete API key", error) diff --git a/packages/web/src/app/[domain]/settings/billing/page.tsx b/packages/web/src/app/[domain]/settings/billing/page.tsx deleted file mode 100644 index 6c5ce8d0a..000000000 --- a/packages/web/src/app/[domain]/settings/billing/page.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import { getCurrentUserRole } from "@/actions" -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card" -import { getSubscriptionBillingEmail, getSubscriptionInfo } from "@/ee/features/billing/actions" -import { ChangeBillingEmailCard } from "@/ee/features/billing/components/changeBillingEmailCard" -import { ManageSubscriptionButton } from "@/ee/features/billing/components/manageSubscriptionButton" -import { IS_BILLING_ENABLED } from "@/ee/features/billing/stripe" -import { ServiceErrorException } from "@/lib/serviceError" -import { isServiceError } from "@/lib/utils" -import { CalendarIcon, DollarSign, Users } from "lucide-react" -import type { Metadata } from "next" -import { notFound } from "next/navigation" - -export const metadata: Metadata = { - title: "Billing | Settings", - description: "Manage your subscription and billing information", -} - -interface BillingPageProps { - params: Promise<{ - domain: string - }> -} - -export default async function BillingPage(props: BillingPageProps) { - const params = await props.params; - - const { - domain - } = params; - - if (!IS_BILLING_ENABLED) { - notFound(); - } - - const subscription = await getSubscriptionInfo(domain) - - if (isServiceError(subscription)) { - throw new ServiceErrorException(subscription); - } - - if (!subscription) { - throw new Error("Subscription not found"); - } - - const currentUserRole = await getCurrentUserRole(domain) - if (isServiceError(currentUserRole)) { - throw new ServiceErrorException(currentUserRole); - } - - const billingEmail = await getSubscriptionBillingEmail(domain); - if (isServiceError(billingEmail)) { - throw new ServiceErrorException(billingEmail); - } - - return ( -
-
-

Billing

-

Manage your subscription and billing information

-
-
- {/* Billing Email Card */} - - - - - Subscription Plan - - - {subscription.status === "trialing" - ? "You are currently on a free trial" - : `You are currently on the ${subscription.plan} plan.`} - - - -
-
- -
-

Seats

-

{subscription.seats} active users

-
-
-
-
-
- -
-

{subscription.status === "trialing" ? "Trial End Date" : "Next Billing Date"}

-

{new Date(subscription.nextBillingDate * 1000).toLocaleDateString()}

-
-
-
-
-
- -
-

Billing Amount

-

${(subscription.perSeatPrice * subscription.seats).toFixed(2)} per month

-
-
-
-
- - - -
- -
-
- ) -} 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 0eb17b83e..4dcd24224 100644 --- a/packages/web/src/app/[domain]/settings/layout.tsx +++ b/packages/web/src/app/[domain]/settings/layout.tsx @@ -2,7 +2,6 @@ import React from "react" import { Metadata } from "next" import { SidebarNav } from "./components/sidebar-nav" import { NavigationMenu } from "../components/navigationMenu" -import { IS_BILLING_ENABLED } from "@/ee/features/billing/stripe"; import { redirect } from "next/navigation"; import { auth } from "@/auth"; import { isServiceError } from "@/lib/utils"; @@ -11,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; @@ -68,10 +67,10 @@ 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(SINGLE_TENANT_ORG_DOMAIN); + const requests = await getOrgAccountRequests(); if (isServiceError(requests)) { throw new ServiceErrorException(requests); } @@ -84,12 +83,6 @@ export const getSidebarNavItems = async () => } return [ - ...(IS_BILLING_ENABLED ? [ - { - title: "Billing", - href: `/${SINGLE_TENANT_ORG_DOMAIN}/settings/billing`, - } - ] : []), ...(role === OrgRole.OWNER ? [ { title: "Access", diff --git a/packages/web/src/app/[domain]/settings/license/page.tsx b/packages/web/src/app/[domain]/settings/license/page.tsx index ba955260c..7413db9b8 100644 --- a/packages/web/src/app/[domain]/settings/license/page.tsx +++ b/packages/web/src/app/[domain]/settings/license/page.tsx @@ -75,7 +75,7 @@ export default async function LicensePage(props: LicensePageProps) { ) } - const members = await getOrgMembers(domain); + const members = await getOrgMembers(); if (isServiceError(members)) { throw new ServiceErrorException(members); } diff --git a/packages/web/src/app/[domain]/settings/members/components/inviteMemberCard.tsx b/packages/web/src/app/[domain]/settings/members/components/inviteMemberCard.tsx index d7a8ab736..878d50892 100644 --- a/packages/web/src/app/[domain]/settings/members/components/inviteMemberCard.tsx +++ b/packages/web/src/app/[domain]/settings/members/components/inviteMemberCard.tsx @@ -12,7 +12,6 @@ import { PlusCircleIcon, Loader2, AlertCircle } from "lucide-react"; import { OrgRole } from "@prisma/client"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog"; import { createInvites } from "@/actions"; -import { useDomain } from "@/hooks/useDomain"; import { isServiceError } from "@/lib/utils"; import { useToast } from "@/components/hooks/use-toast"; import { useRouter } from "next/navigation"; @@ -29,15 +28,13 @@ export const inviteMemberFormSchema = z.object({ interface InviteMemberCardProps { currentUserRole: OrgRole; - isBillingEnabled: boolean; seatsAvailable?: boolean; } -export const InviteMemberCard = ({ currentUserRole, isBillingEnabled, seatsAvailable = true }: InviteMemberCardProps) => { +export const InviteMemberCard = ({ currentUserRole, seatsAvailable = true }: InviteMemberCardProps) => { const [isInviteDialogOpen, setIsInviteDialogOpen] = useState(false); const [isLoading, setIsLoading] = useState(false); - const domain = useDomain(); - const { toast } = useToast(); + const { toast } = useToast(); const router = useRouter(); const captureEvent = useCaptureEvent(); @@ -55,7 +52,7 @@ export const InviteMemberCard = ({ currentUserRole, isBillingEnabled, seatsAvail const onSubmit = useCallback((data: z.infer) => { setIsLoading(true); - createInvites(data.emails.map(e => e.email), domain) + createInvites(data.emails.map(e => e.email)) .then((res) => { if (isServiceError(res)) { toast({ @@ -79,7 +76,7 @@ export const InviteMemberCard = ({ currentUserRole, isBillingEnabled, seatsAvail .finally(() => { setIsLoading(false); }); - }, [domain, form, toast, router, captureEvent]); + }, [form, toast, router, captureEvent]); const isDisabled = !seatsAvailable || currentUserRole !== OrgRole.OWNER || isLoading; @@ -164,7 +161,7 @@ export const InviteMemberCard = ({ currentUserRole, isBillingEnabled, seatsAvail Invite Team Members - {`Your team is growing! By confirming, you will be inviting ${form.getValues().emails.length} new members to your organization. ${isBillingEnabled ? "Your subscription's seat count will be adjusted when a member accepts their invitation." : ""}`} + {`Your team is growing! By confirming, you will be inviting ${form.getValues().emails.length} new members to your organization.`}
diff --git a/packages/web/src/app/[domain]/settings/members/components/invitesList.tsx b/packages/web/src/app/[domain]/settings/members/components/invitesList.tsx index 66d096652..42ab41135 100644 --- a/packages/web/src/app/[domain]/settings/members/components/invitesList.tsx +++ b/packages/web/src/app/[domain]/settings/members/components/invitesList.tsx @@ -3,18 +3,16 @@ import { OrgRole } from "@sourcebot/db"; import { useToast } from "@/components/hooks/use-toast"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog"; -import { Avatar, AvatarImage } from "@/components/ui/avatar"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { createPathWithQueryParams, isServiceError } from "@/lib/utils"; -import placeholderAvatar from "@/public/placeholder_avatar.png"; +import { UserAvatar } from "@/components/userAvatar"; import { Copy, MoreVertical, Search } from "lucide-react"; import { useCallback, useMemo, useState } from "react"; import { cancelInvite } from "@/actions"; import { useRouter } from "next/navigation"; -import { useDomain } from "@/hooks/useDomain"; import useCaptureEvent from "@/hooks/useCaptureEvent"; interface Invite { id: string; @@ -34,7 +32,6 @@ export const InvitesList = ({ invites, currentUserRole }: InviteListProps) => { const [inviteToCancel, setInviteToCancel] = useState(null) const { toast } = useToast(); const router = useRouter(); - const domain = useDomain(); const captureEvent = useCaptureEvent(); const filteredInvites = useMemo(() => { @@ -53,7 +50,7 @@ export const InvitesList = ({ invites, currentUserRole }: InviteListProps) => { }, [invites, searchQuery, dateSort]); const onCancelInvite = useCallback((inviteId: string) => { - cancelInvite(inviteId, domain) + cancelInvite(inviteId) .then((response) => { if (isServiceError(response)) { toast({ @@ -70,7 +67,7 @@ export const InvitesList = ({ invites, currentUserRole }: InviteListProps) => { router.refresh(); } }); - }, [domain, toast, router, captureEvent]); + }, [toast, router, captureEvent]); return (
@@ -109,9 +106,7 @@ export const InvitesList = ({ invites, currentUserRole }: InviteListProps) => { filteredInvites.map((invite) => (
- - - +
{invite.email}
diff --git a/packages/web/src/app/[domain]/settings/members/components/membersList.tsx b/packages/web/src/app/[domain]/settings/members/components/membersList.tsx index 8b197bfc9..63810ce73 100644 --- a/packages/web/src/app/[domain]/settings/members/components/membersList.tsx +++ b/packages/web/src/app/[domain]/settings/members/components/membersList.tsx @@ -3,12 +3,11 @@ import { Input } from "@/components/ui/input"; import { Search, MoreVertical } from "lucide-react"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" -import { Avatar, AvatarImage } from "@/components/ui/avatar"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; import { Button } from "@/components/ui/button"; import { useCallback, useMemo, useState } from "react"; import { OrgRole } from "@prisma/client"; -import placeholderAvatar from "@/public/placeholder_avatar.png"; +import { UserAvatar } from "@/components/userAvatar"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog"; import { promoteToOwner, demoteToMember } from "@/ee/features/userManagement/actions"; import { leaveOrg, removeMemberFromOrg } from "@/features/userManagement/actions"; @@ -200,9 +199,10 @@ export const MembersList = ({ members, currentUserId, currentUserRole, orgName, filteredMembers.map((member) => (
- - - +
{member.name}
{member.email}
diff --git a/packages/web/src/app/[domain]/settings/members/components/requestsList.tsx b/packages/web/src/app/[domain]/settings/members/components/requestsList.tsx index f3ae55f31..5856a0cc6 100644 --- a/packages/web/src/app/[domain]/settings/members/components/requestsList.tsx +++ b/packages/web/src/app/[domain]/settings/members/components/requestsList.tsx @@ -3,17 +3,15 @@ import { OrgRole } from "@sourcebot/db"; import { useToast } from "@/components/hooks/use-toast"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog"; -import { Avatar, AvatarImage } from "@/components/ui/avatar"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { isServiceError } from "@/lib/utils"; -import placeholderAvatar from "@/public/placeholder_avatar.png"; +import { UserAvatar } from "@/components/userAvatar"; import { CheckCircle, Search, XCircle } from "lucide-react"; import { useCallback, useMemo, useState } from "react"; import { approveAccountRequest, rejectAccountRequest } from "@/actions"; import { useRouter } from "next/navigation"; -import { useDomain } from "@/hooks/useDomain"; import useCaptureEvent from "@/hooks/useCaptureEvent"; interface Request { @@ -21,6 +19,7 @@ interface Request { email: string; createdAt: Date; name?: string; + image?: string; } interface RequestsListProps { @@ -36,7 +35,6 @@ export const RequestsList = ({ requests, currentUserRole }: RequestsListProps) = const [requestToAction, setRequestToAction] = useState(null) const { toast } = useToast(); const router = useRouter(); - const domain = useDomain(); const captureEvent = useCaptureEvent(); const filteredRequests = useMemo(() => { @@ -56,7 +54,7 @@ export const RequestsList = ({ requests, currentUserRole }: RequestsListProps) = }, [requests, searchQuery, dateSort]); const onApproveRequest = useCallback((requestId: string) => { - approveAccountRequest(requestId, domain) + approveAccountRequest(requestId) .then((response) => { if (isServiceError(response)) { toast({ @@ -73,10 +71,10 @@ export const RequestsList = ({ requests, currentUserRole }: RequestsListProps) = router.refresh(); } }); - }, [domain, toast, router, captureEvent]); + }, [toast, router, captureEvent]); const onRejectRequest = useCallback((requestId: string) => { - rejectAccountRequest(requestId, domain) + rejectAccountRequest(requestId) .then((response) => { if (isServiceError(response)) { toast({ @@ -93,7 +91,7 @@ export const RequestsList = ({ requests, currentUserRole }: RequestsListProps) = router.refresh(); } }); - }, [domain, toast, router, captureEvent]); + }, [toast, router, captureEvent]); return (
@@ -132,9 +130,7 @@ export const RequestsList = ({ requests, currentUserRole }: RequestsListProps) = filteredRequests.map((request) => (
- - - +
{request.name || request.email}
{request.email}
diff --git a/packages/web/src/app/[domain]/settings/members/page.tsx b/packages/web/src/app/[domain]/settings/members/page.tsx index be2c1e8f0..4b384d149 100644 --- a/packages/web/src/app/[domain]/settings/members/page.tsx +++ b/packages/web/src/app/[domain]/settings/members/page.tsx @@ -7,7 +7,6 @@ import { Tabs, TabsContent } from "@/components/ui/tabs"; import { TabSwitcher } from "@/components/ui/tab-switcher"; import { InvitesList } from "./components/invitesList"; import { getOrgInvites, getMe, getOrgAccountRequests } from "@/actions"; -import { IS_BILLING_ENABLED } from "@/ee/features/billing/stripe"; import { ServiceErrorException } from "@/lib/serviceError"; import { getSeats, hasEntitlement, SOURCEBOT_UNLIMITED_SEATS } from "@sourcebot/shared"; import { RequestsList } from "./components/requestsList"; @@ -57,17 +56,17 @@ export default async function MembersSettingsPage(props: MembersSettingsPageProp redirect(`/${domain}/settings`); } - const members = await getOrgMembers(domain); + const members = await getOrgMembers(); if (isServiceError(members)) { throw new ServiceErrorException(members); } - const invites = await getOrgInvites(domain); + const invites = await getOrgInvites(); if (isServiceError(invites)) { throw new ServiceErrorException(invites); } - const requests = await getOrgAccountRequests(domain); + const requests = await getOrgAccountRequests(); if (isServiceError(requests)) { throw new ServiceErrorException(requests); } @@ -99,7 +98,6 @@ export default async function MembersSettingsPage(props: MembersSettingsPageProp diff --git a/packages/web/src/app/[domain]/upgrade/page.tsx b/packages/web/src/app/[domain]/upgrade/page.tsx deleted file mode 100644 index 3d4e0e306..000000000 --- a/packages/web/src/app/[domain]/upgrade/page.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { SourcebotLogo } from "@/app/components/sourcebotLogo"; -import { Footer } from "@/app/components/footer"; -import { OrgSelector } from "../components/orgSelector"; -import { EnterpriseUpgradeCard } from "@/ee/features/billing/components/enterpriseUpgradeCard"; -import { TeamUpgradeCard } from "@/ee/features/billing/components/teamUpgradeCard"; -import { redirect } from "next/navigation"; -import { isServiceError } from "@/lib/utils"; -import Link from "next/link"; -import { ArrowLeftIcon } from "@radix-ui/react-icons"; -import { LogoutEscapeHatch } from "@/app/components/logoutEscapeHatch"; -import { env } from "@sourcebot/shared"; -import { IS_BILLING_ENABLED } from "@/ee/features/billing/stripe"; -import { getSubscriptionInfo } from "@/ee/features/billing/actions"; - -export default async function Upgrade(props: { params: Promise<{ domain: string }> }) { - const params = await props.params; - - const { - domain - } = params; - - if (!IS_BILLING_ENABLED) { - redirect(`/${domain}`); - } - - const subscription = await getSubscriptionInfo(domain); - if (!subscription) { - redirect(`/${domain}`); - } - - if (!isServiceError(subscription) && subscription.status === "active") { - redirect(`/${domain}`); - } - - const isTrialing = !isServiceError(subscription) ? subscription.status === "trialing" : false; - - return ( -
- {isTrialing && ( - -
- Return to dashboard -
- - )} - -
- -

- {isTrialing ? - "Upgrade your trial." : - "Your subscription has expired." - } -

-

- {isTrialing ? - "Upgrade now to get the most out of Sourcebot." : - "Please upgrade to continue using Sourcebot." - } -

-
- - {env.SOURCEBOT_TENANCY_MODE === 'multi' && ( - - )} - -
- - -
- -
-
- ) -} \ No newline at end of file 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)/commit/route.ts b/packages/web/src/app/api/(server)/commit/route.ts new file mode 100644 index 000000000..c5e409ce8 --- /dev/null +++ b/packages/web/src/app/api/(server)/commit/route.ts @@ -0,0 +1,30 @@ +import { getCommit } from "@/features/git"; +import { getCommitQueryParamsSchema } from "@/features/git/schemas"; +import { apiHandler } from "@/lib/apiHandler"; +import { queryParamsSchemaValidationError, serviceErrorResponse } from "@/lib/serviceError"; +import { isServiceError } from "@/lib/utils"; +import { NextRequest } from "next/server"; + +export const GET = apiHandler(async (request: NextRequest): Promise => { + const rawParams = Object.fromEntries( + Object.keys(getCommitQueryParamsSchema.shape).map(key => [ + key, + request.nextUrl.searchParams.get(key) ?? undefined + ]) + ); + const parsed = getCommitQueryParamsSchema.safeParse(rawParams); + + if (!parsed.success) { + return serviceErrorResponse( + queryParamsSchemaValidationError(parsed.error) + ); + } + + const result = await getCommit(parsed.data); + + if (isServiceError(result)) { + return serviceErrorResponse(result); + } + + return Response.json(result); +}); 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..c85098ae5 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({ @@ -54,7 +54,6 @@ export const listRepos = async ({ query, page, perPage, sort, direction, source repoName: repo.name, path: '', pathType: 'tree', - domain: org.domain, })}`, repoDisplayName: repo.displayName ?? undefined, externalWebUrl: repo.webUrl ?? undefined, diff --git a/packages/web/src/app/api/(server)/stripe/route.ts b/packages/web/src/app/api/(server)/stripe/route.ts deleted file mode 100644 index 212860437..000000000 --- a/packages/web/src/app/api/(server)/stripe/route.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { headers } from 'next/headers'; -import { NextRequest } from 'next/server'; -import Stripe from 'stripe'; -import { prisma } from '@/prisma'; -import { StripeSubscriptionStatus } from '@sourcebot/db'; -import { stripeClient } from '@/ee/features/billing/stripe'; -import { env } from '@sourcebot/shared'; -import { createLogger } from "@sourcebot/shared"; - -const logger = createLogger('stripe-webhook'); - -export async function POST(req: NextRequest) { - const body = await req.text(); - const signature = (await headers()).get('stripe-signature'); - - if (!signature) { - return new Response('No signature', { status: 400 }); - } - - if (!stripeClient) { - return new Response('Stripe client not initialized', { status: 500 }); - } - - if (!env.STRIPE_WEBHOOK_SECRET) { - return new Response('Stripe webhook secret not set', { status: 500 }); - } - - try { - const event = stripeClient.webhooks.constructEvent( - body, - signature, - env.STRIPE_WEBHOOK_SECRET - ); - - if (event.type === 'customer.subscription.deleted') { - const subscription = event.data.object as Stripe.Subscription; - const customerId = subscription.customer as string; - - const org = await prisma.org.findFirst({ - where: { - stripeCustomerId: customerId - } - }); - - if (!org) { - return new Response('Org not found', { status: 404 }); - } - - await prisma.org.update({ - where: { - id: org.id - }, - data: { - stripeSubscriptionStatus: StripeSubscriptionStatus.INACTIVE, - stripeLastUpdatedAt: new Date() - } - }); - logger.info(`Org ${org.id} subscription status updated to INACTIVE`); - - return new Response(JSON.stringify({ received: true }), { - status: 200 - }); - } else if (event.type === 'customer.subscription.created') { - const subscription = event.data.object as Stripe.Subscription; - const customerId = subscription.customer as string; - - const org = await prisma.org.findFirst({ - where: { - stripeCustomerId: customerId - } - }); - - if (!org) { - return new Response('Org not found', { status: 404 }); - } - - await prisma.org.update({ - where: { - id: org.id - }, - data: { - stripeSubscriptionStatus: StripeSubscriptionStatus.ACTIVE, - stripeLastUpdatedAt: new Date() - } - }); - logger.info(`Org ${org.id} subscription status updated to ACTIVE`); - - return new Response(JSON.stringify({ received: true }), { - status: 200 - }); - } else { - logger.info(`Received unknown event type: ${event.type}`); - return new Response(JSON.stringify({ received: true }), { - status: 202 - }); - } - - } catch (err) { - logger.error('Error processing webhook:', err); - return new Response( - 'Webhook error: ' + (err as Error).message, - { status: 400 } - ); - } -} diff --git a/packages/web/src/app/api/minidenticon/route.ts b/packages/web/src/app/api/minidenticon/route.ts new file mode 100644 index 000000000..139683b19 --- /dev/null +++ b/packages/web/src/app/api/minidenticon/route.ts @@ -0,0 +1,29 @@ +'use server'; + +import { minidenticon } from 'minidenticons'; +import sharp from 'sharp'; +import { NextRequest } from 'next/server'; +import { apiHandler } from '@/lib/apiHandler'; + +// Generates a minidenticon avatar PNG from an email address. +// Used as a fallback avatar in emails where data URIs aren't supported. +export const GET = apiHandler(async (request: NextRequest) => { + const email = request.nextUrl.searchParams.get('email'); + if (!email) { + return new Response('Missing email parameter', { status: 400 }); + } + + const svg = minidenticon(email, 50, 50); + const png = await sharp(Buffer.from(svg)) + .flatten({ background: { r: 241, g: 245, b: 249 } }) + .resize(128, 128) + .png() + .toBuffer(); + + return new Response(new Uint8Array(png), { + headers: { + 'Content-Type': 'image/png', + 'Cache-Control': 'public, max-age=31536000, immutable', + }, + }); +}, { track: false }); diff --git a/packages/web/src/app/components/anonymousAccessToggle.tsx b/packages/web/src/app/components/anonymousAccessToggle.tsx index 4d079288e..1079f2ed5 100644 --- a/packages/web/src/app/components/anonymousAccessToggle.tsx +++ b/packages/web/src/app/components/anonymousAccessToggle.tsx @@ -3,7 +3,6 @@ import { useState } from "react" import { Switch } from "@/components/ui/switch" import { setAnonymousAccessStatus } from "@/actions" -import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants" import { isServiceError } from "@/lib/utils" import { useToast } from "@/components/hooks/use-toast" @@ -22,7 +21,7 @@ export function AnonymousAccessToggle({ hasAnonymousAccessEntitlement, anonymous const handleToggle = async (checked: boolean) => { setIsLoading(true) try { - const result = await setAnonymousAccessStatus(SINGLE_TENANT_ORG_DOMAIN, checked) + const result = await setAnonymousAccessStatus(checked) if (isServiceError(result)) { toast({ diff --git a/packages/web/src/app/components/inviteLinkToggle.tsx b/packages/web/src/app/components/inviteLinkToggle.tsx index feaef814d..bd610a2e9 100644 --- a/packages/web/src/app/components/inviteLinkToggle.tsx +++ b/packages/web/src/app/components/inviteLinkToggle.tsx @@ -6,7 +6,6 @@ import { Input } from "@/components/ui/input" import { Switch } from "@/components/ui/switch" import { Copy, Check } from "lucide-react" import { useToast } from "@/components/hooks/use-toast" -import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants" import { setInviteLinkEnabled } from "@/actions" import { isServiceError } from "@/lib/utils" @@ -25,7 +24,7 @@ export function InviteLinkToggle({ inviteLinkEnabled, inviteLink }: InviteLinkTo const handleToggle = async (checked: boolean) => { setIsLoading(true) try { - const result = await setInviteLinkEnabled(SINGLE_TENANT_ORG_DOMAIN, checked) + const result = await setInviteLinkEnabled(checked) if (isServiceError(result)) { toast({ diff --git a/packages/web/src/app/components/memberApprovalRequiredToggle.tsx b/packages/web/src/app/components/memberApprovalRequiredToggle.tsx index 671992328..2cb33db43 100644 --- a/packages/web/src/app/components/memberApprovalRequiredToggle.tsx +++ b/packages/web/src/app/components/memberApprovalRequiredToggle.tsx @@ -3,7 +3,6 @@ import { useState } from "react" import { Switch } from "@/components/ui/switch" import { setMemberApprovalRequired } from "@/actions" -import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants" import { isServiceError } from "@/lib/utils" import { useToast } from "@/components/hooks/use-toast" @@ -21,7 +20,7 @@ export function MemberApprovalRequiredToggle({ memberApprovalRequired, onToggleC const handleToggle = async (checked: boolean) => { setIsLoading(true) try { - const result = await setMemberApprovalRequired(SINGLE_TENANT_ORG_DOMAIN, checked) + const result = await setMemberApprovalRequired(checked) if (isServiceError(result)) { toast({ diff --git a/packages/web/src/app/invite/actions.ts b/packages/web/src/app/invite/actions.ts index d7e8b1abd..418a65530 100644 --- a/packages/web/src/app/invite/actions.ts +++ b/packages/web/src/app/invite/actions.ts @@ -1,22 +1,21 @@ "use server"; -import { withAuth } from "@/actions"; 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 { prisma } from "@/prisma"; +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 () => - withAuth(async (userId) => { + withAuth_skipOrgMembershipCheck(async ({ user, prisma }) => { const org = await prisma.org.findUnique({ where: { id: orgId, }, }); - + if (!org) { return orgNotFound(); } @@ -40,7 +39,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; } diff --git a/packages/web/src/app/onboard/components/completeOnboardingButton.tsx b/packages/web/src/app/onboard/components/completeOnboardingButton.tsx index cd5455a4d..2e871b3df 100644 --- a/packages/web/src/app/onboard/components/completeOnboardingButton.tsx +++ b/packages/web/src/app/onboard/components/completeOnboardingButton.tsx @@ -4,7 +4,6 @@ import { useState } from "react" import { useRouter } from "next/navigation" import { Button } from "@/components/ui/button" import { completeOnboarding } from "@/actions" -import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants" import { isServiceError } from "@/lib/utils" import { useToast } from "@/components/hooks/use-toast" @@ -17,7 +16,7 @@ export function CompleteOnboardingButton() { setIsLoading(true) try { - const result = await completeOnboarding(SINGLE_TENANT_ORG_DOMAIN) + const result = await completeOnboarding() if (isServiceError(result)) { toast({ diff --git a/packages/web/src/app/redeem/components/acceptInviteCard.tsx b/packages/web/src/app/redeem/components/acceptInviteCard.tsx index d8f32e997..4d9b3ce23 100644 --- a/packages/web/src/app/redeem/components/acceptInviteCard.tsx +++ b/packages/web/src/app/redeem/components/acceptInviteCard.tsx @@ -4,6 +4,7 @@ import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; import { SourcebotLogo } from "@/app/components/sourcebotLogo"; import Link from "next/link"; import { Avatar, AvatarImage } from "@/components/ui/avatar"; +import { UserAvatar } from "@/components/userAvatar"; import placeholderAvatar from "@/public/placeholder_avatar.png"; import { ArrowRight, Loader2 } from "lucide-react"; import { Button } from "@/components/ui/button"; @@ -74,9 +75,11 @@ export const AcceptInviteCard = ({ inviteId, orgName, orgDomain, orgImageUrl, ho invited you to join the {orgName} organization.

- - - + diff --git a/packages/web/src/app/redeem/components/inviteNotFoundCard.tsx b/packages/web/src/app/redeem/components/inviteNotFoundCard.tsx index 52c9b80d2..496b8583e 100644 --- a/packages/web/src/app/redeem/components/inviteNotFoundCard.tsx +++ b/packages/web/src/app/redeem/components/inviteNotFoundCard.tsx @@ -1,6 +1,5 @@ import { SourcebotLogo } from "@/app/components/sourcebotLogo"; -import { Avatar, AvatarImage } from "@/components/ui/avatar"; -import placeholderAvatar from "@/public/placeholder_avatar.png"; +import { UserAvatar } from "@/components/userAvatar"; import { auth } from "@/auth"; import { Card } from "@/components/ui/card"; @@ -19,9 +18,11 @@ export const InviteNotFoundCard = async () => { The invite you are trying to redeem has already been used, expired, or does not exist.

- - - +

Logged in as {session?.user?.email}

diff --git a/packages/web/src/components/userAvatar.tsx b/packages/web/src/components/userAvatar.tsx new file mode 100644 index 000000000..77ecda8b3 --- /dev/null +++ b/packages/web/src/components/userAvatar.tsx @@ -0,0 +1,30 @@ +'use client'; + +import { minidenticon } from 'minidenticons'; +import { ComponentPropsWithoutRef, forwardRef, useMemo } from 'react'; +import { Avatar, AvatarImage } from '@/components/ui/avatar'; +import { cn } from '@/lib/utils'; + +interface UserAvatarProps extends ComponentPropsWithoutRef { + email?: string | null; + imageUrl?: string | null; +} + +export const UserAvatar = forwardRef( + ({ email, imageUrl, className, ...rest }, ref) => { + const identiconUri = useMemo(() => { + if (!email) { + return undefined; + } + return 'data:image/svg+xml;utf8,' + encodeURIComponent(minidenticon(email, 50, 50)); + }, [email]); + + return ( + + + + ); + } +); + +UserAvatar.displayName = 'UserAvatar'; diff --git a/packages/web/src/data/connection.ts b/packages/web/src/data/connection.ts deleted file mode 100644 index 7664dcd98..000000000 --- a/packages/web/src/data/connection.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { prisma } from '@/prisma'; -import 'server-only'; - -export const getConnection = async (connectionId: number, orgId: number) => { - const connection = await prisma.connection.findUnique({ - where: { - id: connectionId, - orgId: orgId, - }, - }); - - return connection; -} - -export const getConnectionByDomain = async (connectionId: number, domain: string) => { - const connection = await prisma.connection.findUnique({ - where: { - id: connectionId, - org: { - domain: domain, - } - }, - }); - - return connection; -} diff --git a/packages/web/src/ee/features/analytics/actions.ts b/packages/web/src/ee/features/analytics/actions.ts index 571207405..de49791da 100644 --- a/packages/web/src/ee/features/analytics/actions.ts +++ b/packages/web/src/ee/features/analytics/actions.ts @@ -1,26 +1,27 @@ 'use server'; -import { sew, withAuth, withOrgMembership } from "@/actions"; -import { OrgRole } from "@sourcebot/db"; -import { prisma } from "@/prisma"; +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"; import { ErrorCode } from "@/lib/errorCodes"; import { StatusCodes } from "http-status-codes"; +import { OrgRole } from "@sourcebot/db"; -export const getAnalytics = async (domain: string, apiKey: string | undefined = undefined): Promise => sew(() => - withAuth((userId, _apiKeyHash) => - withOrgMembership(userId, domain, async ({ org }) => { - if (!hasEntitlement("analytics")) { - return { - statusCode: StatusCodes.FORBIDDEN, - errorCode: ErrorCode.INSUFFICIENT_PERMISSIONS, - message: "Analytics is not available in your current plan", - } satisfies ServiceError; - } +export const getAnalytics = async (): Promise => sew(() => + withAuth(async ({ org, role, prisma }) => + withMinimumOrgRole(role, OrgRole.OWNER, async () => { + if (!hasEntitlement("analytics")) { + return { + statusCode: StatusCodes.FORBIDDEN, + errorCode: ErrorCode.INSUFFICIENT_PERMISSIONS, + message: "Analytics is not available in your current plan", + } satisfies ServiceError; + } - const rows = await prisma.$queryRaw` + const rows = await prisma.$queryRaw` WITH core AS ( SELECT date_trunc('day', "timestamp") AS day, @@ -171,10 +172,10 @@ export const getAnalytics = async (domain: string, apiKey: string | undefined = select: { timestamp: true }, }); - return { - rows, - retentionDays: env.SOURCEBOT_EE_AUDIT_RETENTION_DAYS, - oldestRecordDate: oldestRecord?.timestamp ?? null, - }; - }, /* minRequiredRole = */ OrgRole.MEMBER), /* allowAnonymousAccess = */ true, apiKey ? { apiKey, domain } : undefined) -); \ No newline at end of file + return { + rows, + retentionDays: env.SOURCEBOT_EE_AUDIT_RETENTION_DAYS, + oldestRecordDate: oldestRecord?.timestamp ?? null, + }; + })) +); \ No newline at end of file diff --git a/packages/web/src/ee/features/analytics/analyticsContent.tsx b/packages/web/src/ee/features/analytics/analyticsContent.tsx index c40dd2f10..fb9d5c15b 100644 --- a/packages/web/src/ee/features/analytics/analyticsContent.tsx +++ b/packages/web/src/ee/features/analytics/analyticsContent.tsx @@ -396,7 +396,7 @@ export function AnalyticsContent() { error } = useQuery({ queryKey: ["analytics", domain], - queryFn: () => unwrapServiceError(getAnalytics(domain)), + queryFn: () => unwrapServiceError(getAnalytics()), }) const chartColors = useMemo(() => ({ diff --git a/packages/web/src/ee/features/audit/actions.ts b/packages/web/src/ee/features/audit/actions.ts index 4e594d8f4..a1650a035 100644 --- a/packages/web/src/ee/features/audit/actions.ts +++ b/packages/web/src/ee/features/audit/actions.ts @@ -1,11 +1,11 @@ "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 +15,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 +33,7 @@ export interface FetchAuditRecordsParams { } export const fetchAuditRecords = async (params: FetchAuditRecordsParams) => sew(() => - withAuthV2(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/billing/actions.ts b/packages/web/src/ee/features/billing/actions.ts deleted file mode 100644 index d66e94eec..000000000 --- a/packages/web/src/ee/features/billing/actions.ts +++ /dev/null @@ -1,248 +0,0 @@ -'use server'; - -import { getMe, sew, withAuth } from "@/actions"; -import { ServiceError, stripeClientNotInitialized, notFound } from "@/lib/serviceError"; -import { withOrgMembership } from "@/actions"; -import { prisma } from "@/prisma"; -import { OrgRole } from "@sourcebot/db"; -import { stripeClient } from "./stripe"; -import { isServiceError } from "@/lib/utils"; -import { env } from "@sourcebot/shared"; -import { StatusCodes } from "http-status-codes"; -import { ErrorCode } from "@/lib/errorCodes"; -import { headers } from "next/headers"; -import { getSubscriptionForOrg } from "./serverUtils"; -import { createLogger } from "@sourcebot/shared"; - -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; - } - - if (!stripeClient) { - return stripeClientNotInitialized(); - } - - const test_clock = env.STRIPE_ENABLE_TEST_CLOCKS === 'true' ? await stripeClient.testHelpers.testClocks.create({ - frozen_time: Math.floor(Date.now() / 1000) - }) : null; - - // Use the existing customer if it exists, otherwise create a new one. - const customerId = await (async () => { - if (org.stripeCustomerId) { - return org.stripeCustomerId; - } - - const customer = await stripeClient.customers.create({ - name: org.name, - email: user.email ?? undefined, - test_clock: test_clock?.id, - description: `Created by ${user.email} on ${domain} (id: ${org.id})`, - }); - - await prisma.org.update({ - where: { - id: org.id, - }, - data: { - stripeCustomerId: customer.id, - } - }); - - return customer.id; - })(); - - const existingSubscription = await getSubscriptionForOrg(org.id, prisma); - if (!isServiceError(existingSubscription)) { - return { - statusCode: StatusCodes.BAD_REQUEST, - errorCode: ErrorCode.SUBSCRIPTION_ALREADY_EXISTS, - message: "Attempted to create a trial subscription for an organization that already has an active subscription", - } satisfies ServiceError; - } - - - const prices = await stripeClient.prices.list({ - product: env.STRIPE_PRODUCT_ID, - expand: ['data.product'], - }); - - try { - const subscription = await stripeClient.subscriptions.create({ - customer: customerId, - items: [{ - price: prices.data[0].id, - }], - trial_period_days: 14, - trial_settings: { - end_behavior: { - missing_payment_method: 'cancel', - }, - }, - payment_settings: { - save_default_payment_method: 'on_subscription', - }, - }); - - if (!subscription) { - return { - statusCode: StatusCodes.INTERNAL_SERVER_ERROR, - errorCode: ErrorCode.STRIPE_CHECKOUT_ERROR, - message: "Failed to create subscription", - } satisfies ServiceError; - } - - return { - subscriptionId: subscription.id, - } - } catch (e) { - logger.error(e); - return { - statusCode: StatusCodes.INTERNAL_SERVER_ERROR, - errorCode: ErrorCode.STRIPE_CHECKOUT_ERROR, - message: "Failed to create subscription", - } satisfies ServiceError; - } - }, /* minRequiredRole = */ OrgRole.OWNER) - )); - -export const createStripeCheckoutSession = async (domain: string) => sew(() => - withAuth((userId) => - withOrgMembership(userId, domain, async ({ org }) => { - if (!org.stripeCustomerId) { - return notFound(); - } - - if (!stripeClient) { - return stripeClientNotInitialized(); - } - - const orgMembers = await prisma.userToOrg.findMany({ - where: { - orgId: org.id, - }, - select: { - userId: true, - } - }); - const numOrgMembers = orgMembers.length; - - const origin = (await headers()).get('origin')!; - const prices = await stripeClient.prices.list({ - product: env.STRIPE_PRODUCT_ID, - expand: ['data.product'], - }); - - const stripeSession = await stripeClient.checkout.sessions.create({ - customer: org.stripeCustomerId as string, - payment_method_types: ['card'], - line_items: [ - { - price: prices.data[0].id, - quantity: numOrgMembers - } - ], - mode: 'subscription', - payment_method_collection: 'always', - success_url: `${origin}/${domain}/settings/billing`, - cancel_url: `${origin}/${domain}`, - }); - - if (!stripeSession.url) { - return { - statusCode: StatusCodes.INTERNAL_SERVER_ERROR, - errorCode: ErrorCode.STRIPE_CHECKOUT_ERROR, - message: "Failed to create checkout session", - } satisfies ServiceError; - } - - return { - url: stripeSession.url, - } - }) - )); - -export const getCustomerPortalSessionLink = async (domain: string): Promise => sew(() => - withAuth((userId) => - withOrgMembership(userId, domain, async ({ org }) => { - if (!org.stripeCustomerId) { - return notFound(); - } - - if (!stripeClient) { - return stripeClientNotInitialized(); - } - - const origin = (await headers()).get('origin')!; - const portalSession = await stripeClient.billingPortal.sessions.create({ - customer: org.stripeCustomerId as string, - return_url: `${origin}/${domain}/settings/billing`, - }); - - return portalSession.url; - }, /* minRequiredRole = */ OrgRole.OWNER) - )); - -export const getSubscriptionBillingEmail = async (domain: string): Promise => sew(() => - withAuth(async (userId) => - withOrgMembership(userId, domain, async ({ org }) => { - if (!org.stripeCustomerId) { - return notFound(); - } - - if (!stripeClient) { - return stripeClientNotInitialized(); - } - - const customer = await stripeClient.customers.retrieve(org.stripeCustomerId); - if (!('email' in customer) || customer.deleted) { - 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 }) => { - if (!org.stripeCustomerId) { - return notFound(); - } - - if (!stripeClient) { - return stripeClientNotInitialized(); - } - - await stripeClient.customers.update(org.stripeCustomerId, { - email: newEmail, - }); - - return { - success: true, - } - }, /* minRequiredRole = */ OrgRole.OWNER) - )); - -export const getSubscriptionInfo = async (domain: string) => sew(() => - withAuth(async (userId) => - withOrgMembership(userId, domain, async ({ org }) => { - const subscription = await getSubscriptionForOrg(org.id, prisma); - - if (isServiceError(subscription)) { - return subscription; - } - - return { - status: subscription.status, - plan: "Team", - seats: subscription.items.data[0].quantity!, - perSeatPrice: subscription.items.data[0].price.unit_amount! / 100, - nextBillingDate: subscription.current_period_end!, - } - }) - )); diff --git a/packages/web/src/ee/features/billing/components/changeBillingEmailCard.tsx b/packages/web/src/ee/features/billing/components/changeBillingEmailCard.tsx deleted file mode 100644 index 674b8fac8..000000000 --- a/packages/web/src/ee/features/billing/components/changeBillingEmailCard.tsx +++ /dev/null @@ -1,109 +0,0 @@ -"use client" - -import { useToast } from "@/components/hooks/use-toast" -import { Button } from "@/components/ui/button" -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" -import { Form, FormControl, FormField, FormItem, FormMessage } from "@/components/ui/form" -import { Input } from "@/components/ui/input" -import { changeSubscriptionBillingEmail } from "@/ee/features/billing/actions" -import useCaptureEvent from "@/hooks/useCaptureEvent" -import { useDomain } from "@/hooks/useDomain" -import { isServiceError } from "@/lib/utils" -import { zodResolver } from "@hookform/resolvers/zod" -import { OrgRole } from "@sourcebot/db" -import { Loader2 } from "lucide-react" -import { useRouter } from "next/navigation" -import { useState } from "react" -import { useForm } from "react-hook-form" -import * as z from "zod" - -const formSchema = z.object({ - email: z.string().email("Please enter a valid email address"), -}) - -interface ChangeBillingEmailCardProps { - currentUserRole: OrgRole, - billingEmail: string -} - -export function ChangeBillingEmailCard({ currentUserRole, billingEmail }: ChangeBillingEmailCardProps) { - const domain = useDomain() - const [isLoading, setIsLoading] = useState(false) - const { toast } = useToast() - const captureEvent = useCaptureEvent(); - const router = useRouter() - - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: { - email: billingEmail, - }, - }) - - const onSubmit = async (values: z.infer) => { - setIsLoading(true) - const newEmail = values.email || billingEmail - const result = await changeSubscriptionBillingEmail(domain, newEmail) - if (!isServiceError(result)) { - toast({ - description: "✅ Billing email updated successfully!", - }) - captureEvent('wa_billing_email_updated_success', {}) - router.refresh() - } else { - toast({ - description: "❌ Failed to update billing email. Please try again.", - }) - captureEvent('wa_billing_email_updated_fail', { - errorCode: result.errorCode, - }) - } - setIsLoading(false) - } - - return ( - - - - Billing Email - - The email address for your billing account - - -
- - ( - - - - - - - )} - /> -
- -
- - -
-
- ) -} - diff --git a/packages/web/src/ee/features/billing/components/checkout.tsx b/packages/web/src/ee/features/billing/components/checkout.tsx deleted file mode 100644 index 41dc7dc41..000000000 --- a/packages/web/src/ee/features/billing/components/checkout.tsx +++ /dev/null @@ -1,90 +0,0 @@ -'use client'; - -import { SourcebotLogo } from "@/app/components/sourcebotLogo"; -import { useToast } from "@/components/hooks/use-toast"; -import { Button } from "@/components/ui/button"; -import { useDomain } from "@/hooks/useDomain"; -import { useNonEmptyQueryParam } from "@/hooks/useNonEmptyQueryParam"; -import { ErrorCode } from "@/lib/errorCodes"; -import { isServiceError } from "@/lib/utils"; -import { Check, Loader2 } from "lucide-react"; -import { useCallback, useEffect, useState } from "react"; -import { useRouter } from "next/navigation"; -import { OnboardingSteps, TEAM_FEATURES } from "@/lib/constants"; -import useCaptureEvent from "@/hooks/useCaptureEvent"; -import { createOnboardingSubscription } from "../actions"; - -export const Checkout = () => { - const domain = useDomain(); - const { toast } = useToast(); - const errorCode = useNonEmptyQueryParam('errorCode'); - const errorMessage = useNonEmptyQueryParam('errorMessage'); - const [isLoading, setIsLoading] = useState(false); - const router = useRouter(); - const captureEvent = useCaptureEvent(); - - useEffect(() => { - if (errorCode === ErrorCode.STRIPE_CHECKOUT_ERROR && errorMessage) { - toast({ - description: `⚠️ Stripe checkout failed with error: ${errorMessage}`, - variant: "destructive", - }); - captureEvent('wa_onboard_checkout_fail', { - errorCode: errorMessage, - }); - } - }, [errorCode, errorMessage, toast, captureEvent]); - - const onCheckout = useCallback(() => { - setIsLoading(true); - createOnboardingSubscription(domain) - .then((response) => { - if (isServiceError(response)) { - toast({ - description: `❌ Stripe checkout failed with error: ${response.message}`, - variant: "destructive", - }) - captureEvent('wa_onboard_checkout_fail', { - errorCode: response.errorCode, - }); - } else { - captureEvent('wa_onboard_checkout_success', {}); - router.push(`/${domain}/onboard?step=${OnboardingSteps.Complete}`); - } - }) - .finally(() => { - setIsLoading(false); - }); - }, [domain, router, toast, captureEvent]); - - return ( -
- -

Start your 14 day free trial

-

Cancel anytime. No credit card required.

-
    - {TEAM_FEATURES.map((feature, index) => ( -
  • -
    - -
    -

    {feature}

    -
  • - ))} -
-
- -
-
- ) -} \ No newline at end of file diff --git a/packages/web/src/ee/features/billing/components/enterpriseUpgradeCard.tsx b/packages/web/src/ee/features/billing/components/enterpriseUpgradeCard.tsx deleted file mode 100644 index 74de5cf08..000000000 --- a/packages/web/src/ee/features/billing/components/enterpriseUpgradeCard.tsx +++ /dev/null @@ -1,29 +0,0 @@ -'use client'; - -import { ENTERPRISE_FEATURES, SOURCEBOT_SUPPORT_EMAIL } from "@/lib/constants"; -import { UpgradeCard } from "./upgradeCard"; -import Link from "next/link"; -import useCaptureEvent from "@/hooks/useCaptureEvent"; - - -export const EnterpriseUpgradeCard = () => { - const captureEvent = useCaptureEvent(); - - const onClick = () => { - captureEvent('wa_enterprise_upgrade_card_pressed', {}); - } - - return ( - - - - ) -} \ No newline at end of file diff --git a/packages/web/src/ee/features/billing/components/manageSubscriptionButton.tsx b/packages/web/src/ee/features/billing/components/manageSubscriptionButton.tsx deleted file mode 100644 index 25a04a86a..000000000 --- a/packages/web/src/ee/features/billing/components/manageSubscriptionButton.tsx +++ /dev/null @@ -1,49 +0,0 @@ -"use client" - -import { useState } from "react" -import { useRouter } from "next/navigation" -import { isServiceError } from "@/lib/utils" -import { Button } from "@/components/ui/button" -import { useDomain } from "@/hooks/useDomain"; -import { OrgRole } from "@sourcebot/db"; -import useCaptureEvent from "@/hooks/useCaptureEvent"; -import { ExternalLink, Loader2 } from "lucide-react"; -import { getCustomerPortalSessionLink } from "@/ee/features/billing/actions" - -export function ManageSubscriptionButton({ currentUserRole }: { currentUserRole: OrgRole }) { - const [isLoading, setIsLoading] = useState(false) - const router = useRouter() - const domain = useDomain(); - const captureEvent = useCaptureEvent(); - - const redirectToCustomerPortal = async () => { - setIsLoading(true) - const session = await getCustomerPortalSessionLink(domain); - if (isServiceError(session)) { - captureEvent('wa_manage_subscription_button_create_portal_session_fail', { - errorCode: session.errorCode, - }); - setIsLoading(false); - } else { - captureEvent('wa_manage_subscription_button_create_portal_session_success', {}) - router.push(session) - // @note: we don't want to set isLoading to false here since we want to show the loading - // spinner until the page is redirected. - } - } - - const isOwner = currentUserRole === OrgRole.OWNER - return ( -
- -
- ) -} \ No newline at end of file diff --git a/packages/web/src/ee/features/billing/components/teamUpgradeCard.tsx b/packages/web/src/ee/features/billing/components/teamUpgradeCard.tsx deleted file mode 100644 index 26ff5276f..000000000 --- a/packages/web/src/ee/features/billing/components/teamUpgradeCard.tsx +++ /dev/null @@ -1,59 +0,0 @@ -'use client'; - -import { UpgradeCard } from "./upgradeCard"; -import { useToast } from "@/components/hooks/use-toast"; -import { useDomain } from "@/hooks/useDomain"; -import { isServiceError } from "@/lib/utils"; -import { useCallback, useState } from "react"; -import { useRouter } from "next/navigation"; -import { TEAM_FEATURES } from "@/lib/constants"; -import useCaptureEvent from "@/hooks/useCaptureEvent"; -import { createStripeCheckoutSession } from "../actions"; - -interface TeamUpgradeCardProps { - buttonText: string; -} - -export const TeamUpgradeCard = ({ buttonText }: TeamUpgradeCardProps) => { - const domain = useDomain(); - const { toast } = useToast(); - const router = useRouter(); - const [isLoading, setIsLoading] = useState(false); - const captureEvent = useCaptureEvent(); - - const onClick = useCallback(() => { - captureEvent('wa_team_upgrade_card_pressed', {}); - setIsLoading(true); - createStripeCheckoutSession(domain) - .then((response) => { - if (isServiceError(response)) { - toast({ - description: `❌ Stripe checkout failed with error: ${response.message}`, - variant: "destructive", - }); - captureEvent('wa_team_upgrade_checkout_fail', { - errorCode: response.errorCode, - }); - } else { - router.push(response.url); - captureEvent('wa_team_upgrade_checkout_success', {}); - } - }) - .finally(() => { - setIsLoading(false); - }); - }, [domain, router, toast, captureEvent]); - - return ( - - ) -} \ No newline at end of file diff --git a/packages/web/src/ee/features/billing/components/upgradeCard.tsx b/packages/web/src/ee/features/billing/components/upgradeCard.tsx deleted file mode 100644 index 9d24b254b..000000000 --- a/packages/web/src/ee/features/billing/components/upgradeCard.tsx +++ /dev/null @@ -1,55 +0,0 @@ -'use client'; - -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card" -import { Check, Loader2 } from "lucide-react"; - - -interface UpgradeCardProps { - title: string; - description: string; - price: string; - priceDescription: string; - features: string[]; - buttonText: string; - onClick?: () => void; - isLoading?: boolean; -} - -export const UpgradeCard = ({ title, description, price, priceDescription, features, buttonText, onClick, isLoading = false }: UpgradeCardProps) => { - return ( - onClick?.()} - > - - {title} - {description} - - -
-

{price}

-

{priceDescription}

-
-
    - {features.map((feature, index) => ( -
  • - - {feature} -
  • - ))} -
-
- - - -
- ) -} \ No newline at end of file diff --git a/packages/web/src/ee/features/billing/serverUtils.ts b/packages/web/src/ee/features/billing/serverUtils.ts deleted file mode 100644 index bf2eb6a8c..000000000 --- a/packages/web/src/ee/features/billing/serverUtils.ts +++ /dev/null @@ -1,80 +0,0 @@ -import 'server-only'; - -import { notFound, orgInvalidSubscription, ServiceError, stripeClientNotInitialized } from "@/lib/serviceError"; -import { isServiceError } from "@/lib/utils"; -import { Prisma } from "@sourcebot/db"; -import Stripe from "stripe"; -import { stripeClient } from "./stripe"; - -export const incrementOrgSeatCount = async (orgId: number, prisma: Prisma.TransactionClient) => { - if (!stripeClient) { - return stripeClientNotInitialized(); - } - - const subscription = await getSubscriptionForOrg(orgId, prisma); - if (isServiceError(subscription)) { - return subscription; - } - - const existingSeatCount = subscription.items.data[0].quantity; - const newSeatCount = (existingSeatCount || 1) + 1; - - await stripeClient.subscriptionItems.update( - subscription.items.data[0].id, - { - quantity: newSeatCount, - proration_behavior: 'create_prorations', - } - ); -} - -export const decrementOrgSeatCount = async (orgId: number, prisma: Prisma.TransactionClient) => { - if (!stripeClient) { - return stripeClientNotInitialized(); - } - - const subscription = await getSubscriptionForOrg(orgId, prisma); - if (isServiceError(subscription)) { - return subscription; - } - - const existingSeatCount = subscription.items.data[0].quantity; - const newSeatCount = (existingSeatCount || 1) - 1; - - await stripeClient.subscriptionItems.update( - subscription.items.data[0].id, - { - quantity: newSeatCount, - proration_behavior: 'create_prorations', - } - ); -} - -export const getSubscriptionForOrg = async (orgId: number, prisma: Prisma.TransactionClient): Promise => { - const org = await prisma.org.findUnique({ - where: { - id: orgId, - }, - }); - - if (!org) { - return notFound(); - } - - if (!org.stripeCustomerId) { - return notFound(); - } - - if (!stripeClient) { - return stripeClientNotInitialized(); - } - - const subscriptions = await stripeClient.subscriptions.list({ - customer: org.stripeCustomerId - }); - - if (subscriptions.data.length === 0) { - return orgInvalidSubscription(); - } - return subscriptions.data[0]; -} \ No newline at end of file diff --git a/packages/web/src/ee/features/billing/stripe.ts b/packages/web/src/ee/features/billing/stripe.ts deleted file mode 100644 index c8ca0af7a..000000000 --- a/packages/web/src/ee/features/billing/stripe.ts +++ /dev/null @@ -1,11 +0,0 @@ -import 'server-only'; -import { env } from '@sourcebot/shared' -import Stripe from "stripe"; -import { hasEntitlement } from '@sourcebot/shared'; - -export const IS_BILLING_ENABLED = hasEntitlement('billing') && env.STRIPE_SECRET_KEY !== undefined; - -export const stripeClient = - IS_BILLING_ENABLED - ? new Stripe(env.STRIPE_SECRET_KEY!) - : undefined; \ No newline at end of file diff --git a/packages/web/src/ee/features/codeNav/components/exploreMenu/referenceList.tsx b/packages/web/src/ee/features/codeNav/components/exploreMenu/referenceList.tsx index 88e212eb8..3a65d967c 100644 --- a/packages/web/src/ee/features/codeNav/components/exploreMenu/referenceList.tsx +++ b/packages/web/src/ee/features/codeNav/components/exploreMenu/referenceList.tsx @@ -9,7 +9,6 @@ import { useMemo, useRef } from "react"; import useCaptureEvent from "@/hooks/useCaptureEvent"; import { useVirtualizer } from "@tanstack/react-virtual"; import Link from "next/link"; -import { useDomain } from "@/hooks/useDomain"; interface ReferenceListProps { data: FindRelatedSymbolsResponse; @@ -23,7 +22,6 @@ export const ReferenceList = ({ data, revisionName, }: ReferenceListProps) => { - const domain = useDomain(); const repoInfoMap = useMemo(() => { return data.repositoryInfo.reduce((acc, repo) => { acc[repo.id] = repo; @@ -112,7 +110,6 @@ export const ReferenceList = ({ path: file.fileName, pathType: 'blob', highlightRange: match.range, - domain, })} onClick={() => { captureEvent('wa_explore_menu_reference_clicked', {}); 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..9945dbf92 100644 --- a/packages/web/src/ee/features/userManagement/actions.ts +++ b/packages/web/src/ee/features/userManagement/actions.ts @@ -1,11 +1,11 @@ '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 +19,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, prisma }) => withMinimumOrgRole(role, OrgRole.OWNER, async () => { if (!hasEntitlement('org-management')) { return orgManagementNotAvailable(); @@ -81,7 +81,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, prisma }) => withMinimumOrgRole(role, OrgRole.OWNER, async () => { if (!hasEntitlement('org-management')) { return orgManagementNotAvailable(); diff --git a/packages/web/src/emails/inviteUserEmail.tsx b/packages/web/src/emails/inviteUserEmail.tsx index eb35ea8b2..fbcd0304d 100644 --- a/packages/web/src/emails/inviteUserEmail.tsx +++ b/packages/web/src/emails/inviteUserEmail.tsx @@ -18,6 +18,7 @@ import { EmailFooter } from './emailFooter'; import { SOURCEBOT_LOGO_LIGHT_LARGE_URL, SOURCEBOT_ARROW_IMAGE_URL, SOURCEBOT_PLACEHOLDER_AVATAR_URL } from './constants'; interface InviteUserEmailProps { + baseUrl: string; inviteLink: string; host: { email: string; @@ -32,6 +33,7 @@ interface InviteUserEmailProps { } export const InviteUserEmail = ({ + baseUrl, host, recipient, orgName, @@ -71,7 +73,7 @@ export const InviteUserEmail = ({ @@ -128,17 +130,16 @@ const InvitedByText = ({ email, name }: { email: string, name?: string }) => { } InviteUserEmail.PreviewProps = { + baseUrl: 'http://localhost:3000', host: { name: 'Alan Turing', email: 'alan.turing@example.com', - avatarUrl: SOURCEBOT_PLACEHOLDER_AVATAR_URL, }, recipient: { // name: 'alanturing', }, orgName: 'Enigma', - orgImageUrl: SOURCEBOT_PLACEHOLDER_AVATAR_URL, - inviteLink: 'https://sourcebot.example.com/redeem?invite_id=1234', + inviteLink: 'http://localhost:3000/redeem?invite_id=1234', } satisfies InviteUserEmailProps; export default InviteUserEmail; \ No newline at end of file diff --git a/packages/web/src/emails/joinRequestSubmittedEmail.tsx b/packages/web/src/emails/joinRequestSubmittedEmail.tsx index de9752064..dd690fcf8 100644 --- a/packages/web/src/emails/joinRequestSubmittedEmail.tsx +++ b/packages/web/src/emails/joinRequestSubmittedEmail.tsx @@ -69,7 +69,7 @@ export const JoinRequestSubmittedEmail = ({ Requestor avatar { } JoinRequestSubmittedEmail.PreviewProps = { - baseUrl: 'https://sourcebot.example.com', + baseUrl: 'http://localhost:3000', requestor: { name: 'Alan Turing', email: 'alan.turing@example.com', - avatarUrl: SOURCEBOT_PLACEHOLDER_AVATAR_URL, }, orgName: 'Enigma', orgDomain: '~', - orgImageUrl: SOURCEBOT_PLACEHOLDER_AVATAR_URL, } satisfies JoinRequestSubmittedEmailProps; export default JoinRequestSubmittedEmail; 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/chat/components/chatThread/messageAvatar.tsx b/packages/web/src/features/chat/components/chatThread/messageAvatar.tsx index 904a8c053..6261a631a 100644 --- a/packages/web/src/features/chat/components/chatThread/messageAvatar.tsx +++ b/packages/web/src/features/chat/components/chatThread/messageAvatar.tsx @@ -5,6 +5,7 @@ import { cn } from '@/lib/utils'; import { useThemeNormalized } from '@/hooks/useThemeNormalized'; import { useSession } from 'next-auth/react'; import { SBChatMessage } from '../../types'; +import { UserAvatar } from '@/components/userAvatar'; interface MessageAvatarProps { role: SBChatMessage['role']; @@ -16,16 +17,22 @@ export const MessageAvatar = ({ role, className }: MessageAvatarProps) => { const { data: session } = useSession(); const { theme } = useThemeNormalized(); + if (role === "user") { + return ( + + ); + } + return ( - {role === "user" ? "U" : "AI"} - {role === "user" ? ( - - ) : ( - - )} + AI + ) } diff --git a/packages/web/src/features/chat/components/chatThread/tools/fileRow.tsx b/packages/web/src/features/chat/components/chatThread/tools/fileRow.tsx index ea95cd6d6..f2485e6cb 100644 --- a/packages/web/src/features/chat/components/chatThread/tools/fileRow.tsx +++ b/packages/web/src/features/chat/components/chatThread/tools/fileRow.tsx @@ -2,7 +2,6 @@ import { VscodeFileIcon } from "@/app/components/vscodeFileIcon"; import { getBrowsePath } from "@/app/[domain]/browse/hooks/utils"; -import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants"; import Link from "next/link"; type FileInfo = { @@ -22,7 +21,6 @@ export const FileRow = ({ file }: { file: FileInfo }) => { revisionName: file.revision, path: file.path, pathType: 'blob', - domain: SINGLE_TENANT_ORG_DOMAIN, }); return ( diff --git a/packages/web/src/features/chat/components/chatThread/tools/listTreeToolComponent.tsx b/packages/web/src/features/chat/components/chatThread/tools/listTreeToolComponent.tsx index bc49c56ef..e7021c8c8 100644 --- a/packages/web/src/features/chat/components/chatThread/tools/listTreeToolComponent.tsx +++ b/packages/web/src/features/chat/components/chatThread/tools/listTreeToolComponent.tsx @@ -4,7 +4,6 @@ import { ListTreeMetadata, ToolResult } from "@/features/tools"; import { RepoBadge } from "./repoBadge"; import { Separator } from "@/components/ui/separator"; import { getBrowsePath } from "@/app/[domain]/browse/hooks/utils"; -import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants"; import { FolderIcon } from "lucide-react"; import Link from "next/link"; @@ -18,7 +17,6 @@ export const ListTreeToolComponent = ({ metadata }: ToolResult revisionName: metadata.ref, path: metadata.path, pathType: 'tree', - domain: SINGLE_TENANT_ORG_DOMAIN, })} onClick={(e) => e.stopPropagation()} className="inline-flex items-center gap-1 text-xs bg-muted hover:bg-accent px-1.5 py-0.5 rounded truncate text-foreground font-medium transition-colors min-w-0" diff --git a/packages/web/src/features/chat/components/chatThread/tools/readFileToolComponent.tsx b/packages/web/src/features/chat/components/chatThread/tools/readFileToolComponent.tsx index 1adfe23d7..586afc276 100644 --- a/packages/web/src/features/chat/components/chatThread/tools/readFileToolComponent.tsx +++ b/packages/web/src/features/chat/components/chatThread/tools/readFileToolComponent.tsx @@ -3,7 +3,6 @@ import { ReadFileMetadata, ToolResult } from "@/features/tools"; import { VscodeFileIcon } from "@/app/components/vscodeFileIcon"; import { getBrowsePath } from "@/app/[domain]/browse/hooks/utils"; -import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants"; import { Separator } from "@/components/ui/separator"; import Link from "next/link"; import { RepoBadge } from "./repoBadge"; @@ -15,7 +14,6 @@ export const ReadFileToolComponent = ({ metadata }: ToolResult revisionName: metadata.ref, path: metadata.path, pathType: 'blob', - domain: SINGLE_TENANT_ORG_DOMAIN, highlightRange: (metadata.isTruncated || metadata.startLine > 1) ? { start: { lineNumber: metadata.startLine }, end: { lineNumber: metadata.endLine }, diff --git a/packages/web/src/features/chat/components/chatThread/tools/repoBadge.tsx b/packages/web/src/features/chat/components/chatThread/tools/repoBadge.tsx index 68b69bfef..a5a352957 100644 --- a/packages/web/src/features/chat/components/chatThread/tools/repoBadge.tsx +++ b/packages/web/src/features/chat/components/chatThread/tools/repoBadge.tsx @@ -1,7 +1,6 @@ 'use client'; import { getBrowsePath } from "@/app/[domain]/browse/hooks/utils"; -import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants"; import { getCodeHostIcon } from "@/lib/utils"; import { CodeHostType } from "@sourcebot/db"; import Image from "next/image"; @@ -13,7 +12,6 @@ export const RepoBadge = ({ repo }: { repo: { name: string; displayName: string; repoName: repo.name, path: '', pathType: 'tree', - domain: SINGLE_TENANT_ORG_DOMAIN, }); return ( diff --git a/packages/web/src/features/chat/components/chatThread/tools/repoHeader.tsx b/packages/web/src/features/chat/components/chatThread/tools/repoHeader.tsx index 4ce148ca7..13f69cb42 100644 --- a/packages/web/src/features/chat/components/chatThread/tools/repoHeader.tsx +++ b/packages/web/src/features/chat/components/chatThread/tools/repoHeader.tsx @@ -1,7 +1,6 @@ 'use client'; import { getBrowsePath } from "@/app/[domain]/browse/hooks/utils"; -import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants"; import { cn, getCodeHostIcon } from "@/lib/utils"; import { CodeHostType } from "@sourcebot/db"; import Image from "next/image"; @@ -20,7 +19,6 @@ export const RepoHeader = ({ repo, repoName, isPrimary }: { repo: RepoInfo | und repoName: repoName, path: '', pathType: 'tree', - domain: SINGLE_TENANT_ORG_DOMAIN, }); const className = cn("top-0 flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium bg-popover border-b border-border", diff --git a/packages/web/src/features/chat/utils.ts b/packages/web/src/features/chat/utils.ts index a1c9fd9f4..c1cbaaf2f 100644 --- a/packages/web/src/features/chat/utils.ts +++ b/packages/web/src/features/chat/utils.ts @@ -1,5 +1,4 @@ import { BrowseHighlightRange, getBrowsePath } from "@/app/[domain]/browse/hooks/utils"; -import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants"; import { CreateUIMessage, TextUIPart, UIMessagePart } from "ai"; import { Descendant, Editor, Point, Range, Transforms } from "slate"; import { ANSWER_TAG, FILE_REFERENCE_PREFIX, FILE_REFERENCE_REGEX } from "./constants"; @@ -271,7 +270,6 @@ export const convertLLMOutputToPortableMarkdown = (text: string, baseUrl: string repoName: repo, path: fileName, pathType: 'blob', - domain: SINGLE_TENANT_ORG_DOMAIN, highlightRange, }); 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/getCommitApi.ts b/packages/web/src/features/git/getCommitApi.ts new file mode 100644 index 000000000..5e4843629 --- /dev/null +++ b/packages/web/src/features/git/getCommitApi.ts @@ -0,0 +1,103 @@ +import { sew } from "@/middleware/sew"; +import { invalidGitRef, notFound, ServiceError, unexpectedError } from '@/lib/serviceError'; +import { withOptionalAuth } from '@/middleware/withAuth'; +import { getRepoPath } from '@sourcebot/shared'; +import { z } from 'zod'; +import { simpleGit } from 'simple-git'; +import { commitDetailSchema } from './schemas'; +import { isGitRefValid } from './utils'; + +export type CommitDetail = z.infer; + +type GetCommitRequest = { + repo: string; + ref: string; +} + +// Field separator that won't appear in commit data +const FIELD_SEP = '\x1f'; +const FORMAT = [ + '%H', // hash + '%aI', // author date ISO 8601 + '%s', // subject + '%D', // refs + '%b', // body + '%aN', // author name + '%aE', // author email + '%P', // parent hashes (space-separated) +].join(FIELD_SEP); + +export const getCommit = async ({ + repo: repoName, + ref, +}: GetCommitRequest): Promise => sew(() => + withOptionalAuth(async ({ org, prisma }) => { + const repo = await prisma.repo.findFirst({ + where: { + name: repoName, + orgId: org.id, + }, + }); + + if (!repo) { + return notFound(`Repository "${repoName}" not found.`); + } + + if (!isGitRefValid(ref)) { + return invalidGitRef(ref); + } + + const { path: repoPath } = getRepoPath(repo); + const git = simpleGit().cwd(repoPath); + + try { + const output = (await git.raw([ + 'log', + '-1', + `--format=${FORMAT}`, + ref, + ])).trim(); + + const fields = output.split(FIELD_SEP); + if (fields.length < 8) { + return unexpectedError(`Failed to parse commit data for revision "${ref}".`); + } + + const [hash, date, message, refs, body, authorName, authorEmail, parentStr] = fields; + const parents = parentStr.trim() === '' ? [] : parentStr.trim().split(' '); + + return { + hash, + date, + message, + refs, + body, + authorName, + authorEmail, + parents, + }; + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + + if (errorMessage.includes('not a git repository')) { + return unexpectedError( + `Invalid git repository at ${repoPath}. ` + + `The directory exists but is not a valid git repository.` + ); + } + + if (errorMessage.includes('unknown revision') || errorMessage.includes('bad object')) { + return notFound(`Revision "${ref}" not found in repository "${repoName}".`); + } + + if (error instanceof Error) { + throw new Error( + `Failed to get commit in repository ${repoName}: ${error.message}` + ); + } else { + throw new Error( + `Failed to get commit in repository ${repoName}: ${errorMessage}` + ); + } + } + })); diff --git a/packages/web/src/features/git/getDiffApi.ts b/packages/web/src/features/git/getDiffApi.ts index 6031724dd..6218280d7 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'; @@ -19,8 +19,8 @@ export interface DiffHunk { } export interface FileDiff { - oldPath: string; - newPath: string; + oldPath: string | null; + newPath: string | null; hunks: DiffHunk[]; } @@ -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); } @@ -67,8 +67,8 @@ export const getDiff = async ({ const files = parseDiff(rawDiff); const nodes: FileDiff[] = files.map((file) => ({ - oldPath: file.from ?? '/dev/null', - newPath: file.to ?? '/dev/null', + oldPath: file.from && file.from !== '/dev/null' ? file.from : null, + newPath: file.to && file.to !== '/dev/null' ? file.to : null, hunks: file.chunks.map((chunk) => { // chunk.content is the full @@ header line, e.g.: // "@@ -7,6 +7,8 @@ some heading text" diff --git a/packages/web/src/features/git/getFileSourceApi.ts b/packages/web/src/features/git/getFileSourceApi.ts index 2f4fcb8f0..03399d995 100644 --- a/packages/web/src/features/git/getFileSourceApi.ts +++ b/packages/web/src/features/git/getFileSourceApi.ts @@ -1,12 +1,11 @@ -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'; import { parseGitAttributes, resolveLanguageFromGitAttributes } from '@/lib/gitattributes'; 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 +17,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({ @@ -90,7 +89,6 @@ export const getFileSource = async ({ path: filePath, repo: repoName, ref }: Fil revisionName: ref, path: filePath, pathType: 'blob', - domain: SINGLE_TENANT_ORG_DOMAIN, })}`; return { 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/index.ts b/packages/web/src/features/git/index.ts index 4adb81966..579d81017 100644 --- a/packages/web/src/features/git/index.ts +++ b/packages/web/src/features/git/index.ts @@ -1,3 +1,4 @@ +export * from './getCommitApi'; export * from './getDiffApi'; export * from './getFilesApi'; export * from './getFolderContentsApi'; diff --git a/packages/web/src/features/git/listCommitsApi.test.ts b/packages/web/src/features/git/listCommitsApi.test.ts index e0e9ffa93..1c4249f87 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,9 +47,9 @@ 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 => { - // Mock withOptionalAuthV2 to provide org and prisma context +vi.mock('@/middleware/withAuth', () => ({ + withOptionalAuth: async (fn: (args: { org: { id: number; name: string }; prisma: unknown }) => Promise): Promise => { + // Mock withOptionalAuth to provide org and prisma context const mockOrg = { id: 1, name: 'test-org' }; const mockPrisma = { repo: { @@ -314,7 +314,7 @@ describe('searchCommits', () => { describe('successful responses', () => { it('should return commits and totalCount from git log', async () => { - const mockCommits = [ + const gitLogOutput = [ { hash: 'abc123', date: '2024-06-15', @@ -335,14 +335,35 @@ describe('searchCommits', () => { }, ]; - mockGitLog.mockResolvedValue({ all: mockCommits }); + const expectedCommits = [ + { + hash: 'abc123', + date: '2024-06-15', + message: 'feat: add feature', + refs: 'HEAD -> main', + body: '', + authorName: 'John Doe', + authorEmail: 'john@example.com', + }, + { + hash: 'def456', + date: '2024-06-14', + message: 'fix: bug fix', + refs: '', + body: '', + authorName: 'Jane Smith', + authorEmail: 'jane@example.com', + }, + ]; + + mockGitLog.mockResolvedValue({ all: gitLogOutput }); mockGitRaw.mockResolvedValue('2'); const result = await listCommits({ repo: 'github.com/test/repo', }); - expect(result).toEqual({ commits: mockCommits, totalCount: 2 }); + expect(result).toEqual({ commits: expectedCommits, totalCount: 2 }); }); it('should return empty commits array when no commits match', async () => { @@ -451,7 +472,7 @@ describe('searchCommits', () => { describe('integration scenarios', () => { it('should handle a typical commit search with filters', async () => { - const mockCommits = [ + const gitLogOutput = [ { hash: 'abc123', date: '2024-06-10T14:30:00Z', @@ -463,9 +484,21 @@ describe('searchCommits', () => { }, ]; + const expectedCommits = [ + { + hash: 'abc123', + date: '2024-06-10T14:30:00Z', + message: 'fix: resolve authentication bug', + refs: 'HEAD -> main', + body: 'Fixed issue with JWT token validation', + authorName: 'Security Team', + authorEmail: 'security@example.com', + }, + ]; + vi.spyOn(dateUtils, 'validateDateRange').mockReturnValue(null); vi.spyOn(dateUtils, 'toGitDate').mockImplementation((date) => date); - mockGitLog.mockResolvedValue({ all: mockCommits }); + mockGitLog.mockResolvedValue({ all: gitLogOutput }); mockGitRaw.mockResolvedValue('1'); const result = await listCommits({ @@ -477,7 +510,7 @@ describe('searchCommits', () => { maxCount: 20, }); - expect(result).toEqual({ commits: mockCommits, totalCount: 1 }); + expect(result).toEqual({ commits: expectedCommits, totalCount: 1 }); expect(mockGitLog).toHaveBeenCalledWith([ '--max-count=20', '--since=30 days ago', @@ -555,7 +588,7 @@ describe('searchCommits', () => { }); it('should work end-to-end with repository lookup', async () => { - const mockCommits = [ + const gitLogOutput = [ { hash: 'xyz789', date: '2024-06-20T10:00:00Z', @@ -567,10 +600,22 @@ describe('searchCommits', () => { }, ]; + const expectedCommits = [ + { + hash: 'xyz789', + date: '2024-06-20T10:00:00Z', + message: 'feat: new feature', + refs: 'main', + body: 'Added new functionality', + authorName: 'Developer', + authorEmail: 'dev@example.com', + }, + ]; + mockFindFirst.mockResolvedValue({ id: 555, name: 'github.com/test/repository' }); vi.spyOn(dateUtils, 'validateDateRange').mockReturnValue(null); vi.spyOn(dateUtils, 'toGitDate').mockImplementation((date) => date); - mockGitLog.mockResolvedValue({ all: mockCommits }); + mockGitLog.mockResolvedValue({ all: gitLogOutput }); mockGitRaw.mockResolvedValue('1'); const result = await listCommits({ @@ -580,7 +625,7 @@ describe('searchCommits', () => { author: 'Developer', }); - expect(result).toEqual({ commits: mockCommits, totalCount: 1 }); + expect(result).toEqual({ commits: expectedCommits, totalCount: 1 }); expect(mockCwd).toHaveBeenCalledWith('/mock/cache/dir/555'); }); }); diff --git a/packages/web/src/features/git/listCommitsApi.ts b/packages/web/src/features/git/listCommitsApi.ts index a57128058..362f8c78b 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'; @@ -10,10 +10,10 @@ import { isGitRefValid } from './utils'; export type Commit = z.infer; -export interface SearchCommitsResult { +export type ListCommitsResponse = { commits: Commit[]; totalCount: number; -} +}; type ListCommitsRequest = { repo: string; @@ -44,8 +44,8 @@ export const listCommits = async ({ path, maxCount = 50, skip = 0, -}: ListCommitsRequest): Promise => sew(() => - withOptionalAuthV2(async ({ org, prisma }) => { +}: ListCommitsRequest): Promise => sew(() => + withOptionalAuth(async ({ org, prisma }) => { const repo = await prisma.repo.findFirst({ where: { name: repoName, @@ -117,7 +117,17 @@ export const listCommits = async ({ const totalCount = parseInt((await git.raw(countArgs)).trim(), 10); - return { commits: log.all as unknown as Commit[], totalCount }; + const commits: Commit[] = log.all.map((c) => ({ + hash: c.hash, + date: c.date, + message: c.message, + refs: c.refs, + body: c.body, + authorName: c.author_name, + authorEmail: c.author_email, + })); + + return { commits, totalCount }; } catch (error: unknown) { // Provide detailed error messages for common git errors const errorMessage = error instanceof Error ? error.message : String(error); diff --git a/packages/web/src/features/git/schemas.ts b/packages/web/src/features/git/schemas.ts index 4a41bba38..5247527ca 100644 --- a/packages/web/src/features/git/schemas.ts +++ b/packages/web/src/features/git/schemas.ts @@ -56,8 +56,8 @@ const diffHunkSchema = z.object({ }); const fileDiffSchema = z.object({ - oldPath: z.string().describe('The file path before the change. `/dev/null` for added files.'), - newPath: z.string().describe('The file path after the change. `/dev/null` for deleted files.'), + oldPath: z.string().nullable().describe('The file path before the change. `null` for added files.'), + newPath: z.string().nullable().describe('The file path after the change. `null` for deleted files.'), hunks: z.array(diffHunkSchema).describe('The list of changed regions within the file.'), }); @@ -83,6 +83,15 @@ export const commitSchema = z.object({ message: z.string().describe('The commit subject line.'), refs: z.string().describe('Refs pointing to this commit (e.g. branch or tag names).'), body: z.string().describe('The commit body (everything after the subject line).'), - author_name: z.string(), - author_email: z.string(), + authorName: z.string(), + authorEmail: z.string(), +}); + +export const getCommitQueryParamsSchema = z.object({ + repo: z.string().describe('The fully-qualified repository name.'), + ref: z.string().describe('The git ref (commit SHA, branch, or tag).'), +}); + +export const commitDetailSchema = commitSchema.extend({ + parents: z.array(z.string()).describe('The parent commit SHAs.'), }); 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/search/zoektSearcher.ts b/packages/web/src/features/search/zoektSearcher.ts index 7203521ba..be853b02f 100644 --- a/packages/web/src/features/search/zoektSearcher.ts +++ b/packages/web/src/features/search/zoektSearcher.ts @@ -19,7 +19,6 @@ import { isBranchQuery, QueryIR, someInQueryIR } from './ir'; import { RepositoryInfo, SearchResponse, SearchResultFile, SearchStats, SourceRange, StreamedSearchErrorResponse, StreamedSearchResponse } from "./types"; import { captureEvent } from "@/lib/posthog"; import { getBrowsePath } from "@/app/[domain]/browse/hooks/utils"; -import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants"; const logger = createLogger("zoekt-searcher"); @@ -430,7 +429,6 @@ const transformZoektSearchResponse = async (response: ZoektGrpcSearchResponse, r repoName: repo.name, path: fileName, pathType: 'blob', - domain: SINGLE_TENANT_ORG_DOMAIN, revisionName: branchName, })}`, externalWebUrl: getCodeHostBrowseFileAtBranchUrl({ 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/tools/listCommits.ts b/packages/web/src/features/tools/listCommits.ts index 34d61eeeb..abdb1f59c 100644 --- a/packages/web/src/features/tools/listCommits.ts +++ b/packages/web/src/features/tools/listCommits.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import { isServiceError } from "@/lib/utils"; -import { listCommits, SearchCommitsResult } from "@/features/git"; +import { listCommits, ListCommitsResponse } from "@/features/git"; import { ToolDefinition } from "./types"; import { logger } from "./logger"; import description from "./listCommits.txt"; @@ -25,7 +25,7 @@ export type ListCommitsRepoInfo = { codeHostType: CodeHostType; }; -export type ListCommitsMetadata = SearchCommitsResult & { +export type ListCommitsMetadata = ListCommitsResponse & { repo: string; repoInfo: ListCommitsRepoInfo; }; diff --git a/packages/web/src/features/userManagement/actions.ts b/packages/web/src/features/userManagement/actions.ts index 02869c659..f19546ad9 100644 --- a/packages/web/src/features/userManagement/actions.ts +++ b/packages/web/src/features/userManagement/actions.ts @@ -1,18 +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 { isServiceError } from "@/lib/utils"; 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 { IS_BILLING_ENABLED } from "@/ee/features/billing/stripe"; -import { decrementOrgSeatCount } from "@/ee/features/billing/serverUtils"; 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({ @@ -54,13 +52,6 @@ export const removeMemberFromOrg = async (memberId: string): Promise<{ success: } }); - if (IS_BILLING_ENABLED) { - const result = await decrementOrgSeatCount(org.id, tx); - if (isServiceError(result)) { - throw result; - } - } - return null; }, { isolationLevel: Prisma.TransactionIsolationLevel.Serializable }); @@ -73,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({ @@ -101,13 +92,6 @@ export const leaveOrg = async (): Promise<{ success: boolean } | ServiceError> = } }); - if (IS_BILLING_ENABLED) { - const result = await decrementOrgSeatCount(org.id, tx); - if (isServiceError(result)) { - throw result; - } - } - return null; }, { isolationLevel: Prisma.TransactionIsolationLevel.Serializable }); 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/hooks/useFindLanguageDescription.ts b/packages/web/src/hooks/useFindLanguageDescription.ts deleted file mode 100644 index 344f93871..000000000 --- a/packages/web/src/hooks/useFindLanguageDescription.ts +++ /dev/null @@ -1,23 +0,0 @@ -'use client'; - -import { LanguageDescription } from "@codemirror/language"; -import { useMemo } from "react"; -import { languages as builtinLanguages } from '@codemirror/language-data' - -interface UseFindLanguageDescriptionProps { - languageName: string; - fuzzyMatch?: boolean; -} - -export const useFindLanguageDescription = ({ languageName, fuzzyMatch = true }: UseFindLanguageDescriptionProps) => { - const languageDescription = useMemo(() => { - const description = LanguageDescription.matchLanguageName( - builtinLanguages, - languageName, - fuzzyMatch - ); - return description; - }, [languageName, fuzzyMatch]); - - return languageDescription; -} \ No newline at end of file diff --git a/packages/web/src/initialize.ts b/packages/web/src/initialize.ts index 06647e57d..3f1f2bc2a 100644 --- a/packages/web/src/initialize.ts +++ b/packages/web/src/initialize.ts @@ -1,5 +1,4 @@ import { createGuestUser } from '@/lib/authUtils'; -import { SOURCEBOT_SUPPORT_EMAIL } from "@/lib/constants"; import { prisma } from "@/prisma"; import { OrgRole } from '@sourcebot/db'; import { createLogger, env, hasEntitlement, loadConfig } from "@sourcebot/shared"; @@ -35,7 +34,7 @@ const pruneOldGuestUser = async () => { } } -const initSingleTenancy = async () => { +const init = async () => { // This is needed because v4 introduces the GUEST org role as well as making authentication required. // To keep things simple, we'll just delete the old guest user if it exists in the DB await pruneOldGuestUser(); @@ -66,7 +65,7 @@ const initSingleTenancy = async () => { // search contexts that may be present in the DB. This could happen if a deployment had // the entitlement, synced search contexts, and then no longer had the entitlement const hasSearchContextEntitlement = hasEntitlement("search-contexts") - if(!hasSearchContextEntitlement) { + if (!hasSearchContextEntitlement) { await prisma.searchContext.deleteMany({ where: { orgId: SINGLE_TENANT_ORG_ID, @@ -115,20 +114,6 @@ const initSingleTenancy = async () => { } } -const initMultiTenancy = async () => { - const hasMultiTenancyEntitlement = hasEntitlement("multi-tenancy"); - if (!hasMultiTenancyEntitlement) { - logger.error(`SOURCEBOT_TENANCY_MODE is set to ${env.SOURCEBOT_TENANCY_MODE} but your license doesn't have multi-tenancy entitlement. Please contact ${SOURCEBOT_SUPPORT_EMAIL} to request a license upgrade.`); - process.exit(1); - } -} - (async () => { - if (env.SOURCEBOT_TENANCY_MODE === 'single') { - await initSingleTenancy(); - } else if (env.SOURCEBOT_TENANCY_MODE === 'multi') { - await initMultiTenancy(); - } else { - throw new Error(`Invalid SOURCEBOT_TENANCY_MODE: ${env.SOURCEBOT_TENANCY_MODE}`); - } + await init(); })(); diff --git a/packages/web/src/lib/authUtils.ts b/packages/web/src/lib/authUtils.ts index f250ad233..6326be64b 100644 --- a/packages/web/src/lib/authUtils.ts +++ b/packages/web/src/lib/authUtils.ts @@ -9,8 +9,6 @@ import { createLogger } from "@sourcebot/shared"; import { getAuditService } from "@/ee/features/audit/factory"; import { StatusCodes } from "http-status-codes"; import { ErrorCode } from "./errorCodes"; -import { IS_BILLING_ENABLED } from "@/ee/features/billing/stripe"; -import { incrementOrgSeatCount } from "@/ee/features/billing/serverUtils"; import { getOrgFromDomain } from "@/data/org"; const logger = createLogger('web-auth-utils'); @@ -262,13 +260,6 @@ export const addUserToOrganization = async (userId: string, orgId: number): Prom } }); - if (IS_BILLING_ENABLED) { - const result = await incrementOrgSeatCount(orgId, tx); - if (isServiceError(result)) { - throw result; - } - } - // Delete the account request if it exists since we've added the user to the org const accountRequest = await tx.accountRequest.findUnique({ where: { diff --git a/packages/web/src/lib/constants.ts b/packages/web/src/lib/constants.ts index 21bb97d54..168dd1a7a 100644 --- a/packages/web/src/lib/constants.ts +++ b/packages/web/src/lib/constants.ts @@ -1,27 +1,3 @@ - -// @note: Order is important here. -export enum OnboardingSteps { - CreateOrg = 'create-org', - ConnectCodeHost = 'connect-code-host', - InviteTeam = 'invite-team', - Checkout = 'checkout', - Complete = 'complete', -} - -export const ENTERPRISE_FEATURES = [ - "All Team features", - "Dedicated Slack support channel", - "Single tenant deployment", - "Advanced security features", -] - -export const TEAM_FEATURES = [ - "Index thousands of repos from multiple code hosts (GitHub, GitLab, Gerrit, Gitea, etc.). Self-hosted code hosts supported.", - "Public and private repos supported.", - "Create shareable links to code snippets.", - "Built on-top of zoekt, Google's code search engine. Blazingly fast and powerful (regex, symbol) code search.", -] - export const MOBILE_UNSUPPORTED_SPLASH_SCREEN_DISMISSED_COOKIE_NAME = 'sb.mobile-unsupported-splash-screen-dismissed'; export const AGENTIC_SEARCH_TUTORIAL_DISMISSED_COOKIE_NAME = 'sb.agentic-search-tutorial-dismissed'; export const OPTIONAL_PROVIDERS_LINK_SKIPPED_COOKIE_NAME = 'sb.optional-providers-link-skipped'; diff --git a/packages/web/src/lib/errorCodes.ts b/packages/web/src/lib/errorCodes.ts index a3e897eac..0f589f28b 100644 --- a/packages/web/src/lib/errorCodes.ts +++ b/packages/web/src/lib/errorCodes.ts @@ -12,7 +12,6 @@ export enum ErrorCode { ORG_NOT_FOUND = 'ORG_NOT_FOUND', CONNECTION_SYNC_ALREADY_SCHEDULED = 'CONNECTION_SYNC_ALREADY_SCHEDULED', ORG_DOMAIN_ALREADY_EXISTS = 'ORG_DOMAIN_ALREADY_EXISTS', - ORG_INVALID_SUBSCRIPTION = 'ORG_INVALID_SUBSCRIPTION', INVALID_CREDENTIALS = 'INVALID_CREDENTIALS', INSUFFICIENT_PERMISSIONS = 'INSUFFICIENT_PERMISSIONS', CONNECTION_NOT_FAILED = 'CONNECTION_NOT_FAILED', @@ -20,10 +19,7 @@ export enum ErrorCode { INVALID_INVITE = 'INVALID_INVITE', INVALID_INVITE_LINK = 'INVALID_INVITE_LINK', INVITE_LINK_NOT_ENABLED = 'INVITE_LINK_NOT_ENABLED', - STRIPE_CHECKOUT_ERROR = 'STRIPE_CHECKOUT_ERROR', SECRET_ALREADY_EXISTS = 'SECRET_ALREADY_EXISTS', - SUBSCRIPTION_ALREADY_EXISTS = 'SUBSCRIPTION_ALREADY_EXISTS', - STRIPE_CLIENT_NOT_INITIALIZED = 'STRIPE_CLIENT_NOT_INITIALIZED', ACTION_DISALLOWED_IN_TENANCY_MODE = 'ACTION_DISALLOWED_IN_TENANCY_MODE', SEARCH_CONTEXT_NOT_FOUND = 'SEARCH_CONTEXT_NOT_FOUND', MISSING_ORG_DOMAIN_HEADER = 'MISSING_ORG_DOMAIN_HEADER', diff --git a/packages/web/src/lib/extensions/lineOffsetExtension.ts b/packages/web/src/lib/extensions/lineOffsetExtension.ts deleted file mode 100644 index 60f890e00..000000000 --- a/packages/web/src/lib/extensions/lineOffsetExtension.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Compartment } from "@codemirror/state"; -import { lineNumbers } from "@codemirror/view"; - -const gutter = new Compartment(); - -/** - * Offsets the line numbers by the given amount - * @see: https://discuss.codemirror.net/t/codemirror-6-offset-line-numbers/2675/8 - */ -export const lineOffsetExtension = (lineOffset: number) => { - const lines = lineNumbers({ - formatNumber: (n) => { - return (n + lineOffset).toString(); - } - }); - - return [ - gutter.of(lines) - ] -} \ No newline at end of file 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/lib/schemas.ts b/packages/web/src/lib/schemas.ts index 3abfe73b0..4fdae9cad 100644 --- a/packages/web/src/lib/schemas.ts +++ b/packages/web/src/lib/schemas.ts @@ -1,15 +1,6 @@ import { z } from "zod"; import { CodeHostType } from "@sourcebot/db"; -export const secretCreateRequestSchema = z.object({ - key: z.string(), - value: z.string(), -}); - -export const secreteDeleteRequestSchema = z.object({ - key: z.string(), -}); - export const repositoryQuerySchema = z.object({ codeHostType: z.nativeEnum(CodeHostType), repoId: z.number(), diff --git a/packages/web/src/lib/serviceError.ts b/packages/web/src/lib/serviceError.ts index cb4af0227..f874cdb7b 100644 --- a/packages/web/src/lib/serviceError.ts +++ b/packages/web/src/lib/serviceError.ts @@ -53,25 +53,6 @@ export const queryParamsSchemaValidationError = (error: ZodError): ServiceError }; } -export const invalidZoektResponse = async (zoektResponse: Response): Promise => { - const zoektMessage = await (async () => { - try { - const zoektResponseBody = await zoektResponse.json(); - if (zoektResponseBody.Error) { - return zoektResponseBody.Error; - } - } catch (_e) { - return "Unknown error"; - } - })(); - - return { - statusCode: StatusCodes.INTERNAL_SERVER_ERROR, - errorCode: ErrorCode.INVALID_REQUEST_BODY, - message: `Zoekt request failed with status code ${zoektResponse.status} and message: "${zoektMessage}"`, - }; -} - export const fileNotFound = (fileName: string, repository: string): ServiceError => { return { statusCode: StatusCodes.NOT_FOUND, @@ -120,30 +101,6 @@ export const orgNotFound = (): ServiceError => { } } -export const orgDomainExists = (): ServiceError => { - return { - statusCode: StatusCodes.CONFLICT, - errorCode: ErrorCode.ORG_DOMAIN_ALREADY_EXISTS, - message: "Organization domain already exists, please try a different one.", - } -} - -export const orgInvalidSubscription = (): ServiceError => { - return { - statusCode: StatusCodes.BAD_REQUEST, - errorCode: ErrorCode.ORG_INVALID_SUBSCRIPTION, - message: "Invalid subscription", - } -} - -export const secretAlreadyExists = (): ServiceError => { - return { - statusCode: StatusCodes.CONFLICT, - errorCode: ErrorCode.SECRET_ALREADY_EXISTS, - message: "Secret already exists", - } -} - export const invalidGitRef = (ref: string): ServiceError => { return { statusCode: StatusCodes.BAD_REQUEST, @@ -152,10 +109,3 @@ export const invalidGitRef = (ref: string): ServiceError => { }; } -export const stripeClientNotInitialized = (): ServiceError => { - return { - statusCode: StatusCodes.INTERNAL_SERVER_ERROR, - errorCode: ErrorCode.STRIPE_CLIENT_NOT_INITIALIZED, - message: "Stripe client is not initialized.", - } -} \ No newline at end of file diff --git a/packages/web/src/lib/types.ts b/packages/web/src/lib/types.ts index 20c58529f..3d1933437 100644 --- a/packages/web/src/lib/types.ts +++ b/packages/web/src/lib/types.ts @@ -1,6 +1,5 @@ import { z } from "zod"; import { listReposResponseSchema, getVersionResponseSchema, repositoryQuerySchema, searchContextQuerySchema, listReposQueryParamsSchema } from "./schemas"; -import { tenancyModeSchema } from "@sourcebot/shared"; export type KeymapType = "default" | "vim"; @@ -26,7 +25,6 @@ export type NewsItem = { read?: boolean; } -export type TenancyMode = z.infer; export type RepositoryQuery = z.infer; export type SearchContextQuery = z.infer; export type ListReposResponse = z.infer; diff --git a/packages/web/src/lib/utils.ts b/packages/web/src/lib/utils.ts index 43a1ad3ae..2f6dedfe6 100644 --- a/packages/web/src/lib/utils.ts +++ b/packages/web/src/lib/utils.ts @@ -14,9 +14,6 @@ import microsoftLogo from "@/public/microsoft_entra.svg"; import authentikLogo from "@/public/authentik.svg"; import jumpcloudLogo from "@/public/jumpcloud.svg"; import { ServiceError } from "./serviceError"; -import { StatusCodes } from "http-status-codes"; -import { ErrorCode } from "./errorCodes"; -import { NextRequest } from "next/server"; import { ConnectionType, Org } from "@sourcebot/db"; import { OrgMetadata, orgMetadataSchema } from "@/types"; import { SINGLE_TENANT_ORG_DOMAIN } from "./constants"; @@ -441,21 +438,6 @@ export const getCodeHostBrowseFileAtBranchUrl = ({ } } -export const isAuthSupportedForCodeHost = (codeHostType: CodeHostType): boolean => { - switch (codeHostType) { - case "github": - case "gitlab": - case "gitea": - case "bitbucketCloud": - case "bitbucketServer": - case "azuredevops": - return true; - case "genericGitHost": - case "gerrit": - return false; - } -} - export const isServiceError = (data: unknown): data is ServiceError => { return typeof data === 'object' && data !== null && @@ -464,11 +446,6 @@ export const isServiceError = (data: unknown): data is ServiceError => { 'message' in data; } -// @see: https://stackoverflow.com/a/65959350/23221295 -export const isDefined = (arg: T | null | undefined): arg is T extends null | undefined ? never : T => { - return arg !== null && arg !== undefined; -} - export const getFormattedDate = (date: Date) => { const now = new Date(); const diffMinutes = (now.getTime() - date.getTime()) / (1000 * 60); @@ -574,18 +551,6 @@ export const unwrapServiceError = async (promise: Promise): return data; } -export const requiredQueryParamGuard = (request: NextRequest, param: string): ServiceError | string => { - const value = request.nextUrl.searchParams.get(param); - if (!value) { - return { - statusCode: StatusCodes.BAD_REQUEST, - errorCode: ErrorCode.MISSING_REQUIRED_QUERY_PARAMETER, - message: `Missing required query param: ${param}`, - }; - } - return value; -} - export const getRepoImageSrc = (imageUrl: string | undefined, repoId: number): string | undefined => { if (!imageUrl) return undefined; 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..4d9170fae 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, })); @@ -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({ @@ -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,13 +722,13 @@ 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()); }); }); -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({ @@ -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 e950829e4..33e5be772 100644 --- a/packages/web/src/withAuthV2.ts +++ b/packages/web/src/middleware/withAuth.ts @@ -2,29 +2,50 @@ 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"; -interface OptionalAuthContext { +type OptionalAuthContext = { user?: UserWithAccounts; org: Org; role: OrgRole; prisma: PrismaClient; } -interface RequiredAuthContext { +type RequiredAuthContext = { user: UserWithAccounts; org: Org; role: Exclude; prisma: PrismaClient; } -export const withAuthV2 = async (fn: (params: RequiredAuthContext) => Promise) => { +/** + * Requires a logged-in user but does NOT check org membership. + * 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 withAuth_skipOrgMembershipCheck = async (fn: (params: Omit & { role: OrgRole; }) => Promise) => { + const authContext = await getAuthContext(); + + if (isServiceError(authContext)) { + return authContext; + } + + const { user, prisma, org, role } = authContext; + + if (!user) { + return notAuthenticated(); + } + + return fn({ user, prisma, org, role }); +}; + +export const withAuth = async (fn: (params: RequiredAuthContext) => Promise) => { const authContext = await getAuthContext(); if (isServiceError(authContext)) { @@ -40,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; @@ -249,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(); +}; diff --git a/packages/web/src/openapi/publicApiDocument.ts b/packages/web/src/openapi/publicApiDocument.ts index b6ac5eccb..147c43c88 100644 --- a/packages/web/src/openapi/publicApiDocument.ts +++ b/packages/web/src/openapi/publicApiDocument.ts @@ -16,6 +16,8 @@ import { publicGetDiffResponseSchema, publicGetTreeRequestSchema, publicHealthResponseSchema, + publicCommitDetailSchema, + publicGetCommitQuerySchema, publicListCommitsQuerySchema, publicListCommitsResponseSchema, publicListReposQueryParamsSchema, @@ -31,8 +33,13 @@ const searchTag = { name: 'Search & Navigation', description: 'Code search and s const reposTag = { name: 'Repositories', description: 'Repository listing and metadata endpoints.' }; const gitTag = { name: 'Git', description: 'Git history, diff, and file content endpoints.' }; const systemTag = { name: 'System', description: 'System health and version endpoints.' }; -const eeUserManagementTag = { name: 'User Management (EE)', description: 'User management endpoints. Requires the `org-management` entitlement and OWNER role.' }; -const eeAuditTag = { name: 'Audit (EE)', description: 'Audit log endpoints. Requires the `audit` entitlement and OWNER role.' }; +const eeTag = { name: 'Enterprise (EE)', description: 'Enterprise endpoints for user management and audit logging.' }; + +const EE_LICENSE_KEY_NOTE = dedent` + +This API is only available with an active Enterprise license. Please add your [license key](/docs/license-key) to activate it. + +`; const publicFileTreeNodeSchema: SchemaObject = { type: 'object', @@ -67,13 +74,13 @@ const securitySchemes: Record`, where `` is your API key.', }, [securitySchemeNames.apiKeyHeader]: { type: 'apiKey', in: 'header', name: 'X-Sourcebot-Api-Key', - description: 'Send a Sourcebot API key (`sbk_...`) in the X-Sourcebot-Api-Key header.', + description: 'Header of the form `X-Sourcebot-Api-Key: `, where `` is your API key.', }, }; @@ -334,12 +341,33 @@ export function createPublicOpenApiDocument(version: string) { }, }); + registry.registerPath({ + method: 'get', + path: '/api/commit', + operationId: 'getCommit', + tags: [gitTag.name], + summary: 'Get commit details', + description: 'Returns details for a single commit, including parent commit SHAs.', + request: { + query: publicGetCommitQuerySchema, + }, + responses: { + 200: { + description: 'Commit details.', + content: jsonContent(publicCommitDetailSchema), + }, + 400: errorJson('Invalid query parameters or git ref.'), + 404: errorJson('Repository or revision not found.'), + 500: errorJson('Unexpected failure.'), + }, + }); + // EE: User Management registry.registerPath({ method: 'get', path: '/api/ee/user', operationId: 'getUser', - tags: [eeUserManagementTag.name], + tags: [eeTag.name], summary: 'Get a user', description: 'Fetches profile details for a single organization member by `userId`. Only organization owners can access this endpoint.', request: { @@ -357,13 +385,16 @@ export function createPublicOpenApiDocument(version: string) { 404: errorJson('User not found.'), 500: errorJson('Unexpected failure.'), }, + 'x-mint': { + content: EE_LICENSE_KEY_NOTE, + }, }); registry.registerPath({ method: 'delete', path: '/api/ee/user', operationId: 'deleteUser', - tags: [eeUserManagementTag.name], + tags: [eeTag.name], summary: 'Delete a user', description: 'Permanently deletes a user and all associated records. Only organization owners can delete other users.', request: { @@ -381,13 +412,16 @@ export function createPublicOpenApiDocument(version: string) { 404: errorJson('User not found.'), 500: errorJson('Unexpected failure.'), }, + 'x-mint': { + content: EE_LICENSE_KEY_NOTE, + }, }); registry.registerPath({ method: 'get', path: '/api/ee/users', operationId: 'listUsers', - tags: [eeUserManagementTag.name], + tags: [eeTag.name], summary: 'List users', description: 'Returns all members of the organization. Only organization owners can access this endpoint.', responses: { @@ -398,6 +432,9 @@ export function createPublicOpenApiDocument(version: string) { 403: errorJson('Insufficient permissions or entitlement not enabled.'), 500: errorJson('Unexpected failure.'), }, + 'x-mint': { + content: EE_LICENSE_KEY_NOTE, + }, }); // EE: Audit @@ -405,7 +442,7 @@ export function createPublicOpenApiDocument(version: string) { method: 'get', path: '/api/ee/audit', operationId: 'listAuditRecords', - tags: [eeAuditTag.name], + tags: [eeTag.name], summary: 'List audit records', description: 'Returns a paginated list of audit log entries. Only organization owners can access this endpoint.', request: { @@ -430,6 +467,9 @@ export function createPublicOpenApiDocument(version: string) { 403: errorJson('Insufficient permissions or entitlement not enabled.'), 500: errorJson('Unexpected failure.'), }, + 'x-mint': { + content: EE_LICENSE_KEY_NOTE, + }, }); const generator = new OpenApiGeneratorV3(registry.definitions); @@ -441,7 +481,7 @@ export function createPublicOpenApiDocument(version: string) { version, description: 'OpenAPI description for the public Sourcebot REST endpoints used for search, repository listing, and file browsing. Authentication is instance-dependent: API keys are the standard integration mechanism, OAuth bearer tokens are EE-only, and some instances may allow anonymous access.', }, - tags: [searchTag, reposTag, gitTag, systemTag, eeUserManagementTag, eeAuditTag], + tags: [searchTag, reposTag, gitTag, systemTag, eeTag], security: [ { [securitySchemeNames.bearerToken]: [] }, { [securitySchemeNames.apiKeyHeader]: [] }, diff --git a/packages/web/src/openapi/publicApiSchemas.ts b/packages/web/src/openapi/publicApiSchemas.ts index 99726090b..dae94afc1 100644 --- a/packages/web/src/openapi/publicApiSchemas.ts +++ b/packages/web/src/openapi/publicApiSchemas.ts @@ -5,13 +5,13 @@ import { findRelatedSymbolsResponseSchema, } from '../features/codeNav/types.js'; import { + commitDetailSchema, commitSchema, fileSourceRequestSchema, fileSourceResponseSchema, + getCommitQueryParamsSchema, getDiffRequestSchema, getDiffResponseSchema, - getFilesRequestSchema, - getFilesResponseSchema, getTreeRequestSchema, listCommitsQueryParamsSchema, } from '../features/git/schemas.js'; @@ -36,8 +36,6 @@ export const publicServiceErrorSchema = serviceErrorSchema.openapi('PublicApiSer export const publicSearchRequestSchema = searchRequestSchema.openapi('PublicSearchRequest'); export const publicSearchResponseSchema = searchResponseSchema.openapi('PublicSearchResponse'); export const publicGetTreeRequestSchema = getTreeRequestSchema.openapi('PublicGetTreeRequest'); -export const publicGetFilesRequestSchema = getFilesRequestSchema.openapi('PublicGetFilesRequest'); -export const publicGetFilesResponseSchema = getFilesResponseSchema.openapi('PublicGetFilesResponse'); export const publicFileSourceRequestSchema = fileSourceRequestSchema.openapi('PublicFileSourceRequest'); export const publicFileSourceResponseSchema = fileSourceResponseSchema.openapi('PublicFileSourceResponse'); export const publicVersionResponseSchema = getVersionResponseSchema.openapi('PublicVersionResponse'); @@ -50,6 +48,8 @@ export const publicFindSymbolsResponseSchema = findRelatedSymbolsResponseSchema. export const publicListCommitsQuerySchema = listCommitsQueryParamsSchema.openapi('PublicListCommitsQuery'); export const publicCommitSchema = commitSchema.openapi('PublicCommit'); export const publicListCommitsResponseSchema = z.array(publicCommitSchema).openapi('PublicListCommitsResponse'); +export const publicGetCommitQuerySchema = getCommitQueryParamsSchema.openapi('PublicGetCommitQuery'); +export const publicCommitDetailSchema = commitDetailSchema.openapi('PublicCommitDetail'); export const publicHealthResponseSchema = z.object({ status: z.enum(['ok']), 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' }, diff --git a/yarn.lock b/yarn.lock index b2cd9e5d5..4b64b86a3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -840,7 +840,7 @@ __metadata: languageName: node linkType: hard -"@babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.27.1": +"@babel/code-frame@npm:^7.10.4": version: 7.27.1 resolution: "@babel/code-frame@npm:7.27.1" dependencies: @@ -946,7 +946,7 @@ __metadata: languageName: node linkType: hard -"@babel/generator@npm:^7.26.2, @babel/generator@npm:^7.29.0": +"@babel/generator@npm:^7.26.2, @babel/generator@npm:^7.27.0, @babel/generator@npm:^7.29.0": version: 7.29.1 resolution: "@babel/generator@npm:7.29.1" dependencies: @@ -959,19 +959,6 @@ __metadata: languageName: node linkType: hard -"@babel/generator@npm:^7.28.5": - version: 7.28.5 - resolution: "@babel/generator@npm:7.28.5" - dependencies: - "@babel/parser": "npm:^7.28.5" - "@babel/types": "npm:^7.28.5" - "@jridgewell/gen-mapping": "npm:^0.3.12" - "@jridgewell/trace-mapping": "npm:^0.3.28" - jsesc: "npm:^3.0.2" - checksum: 10c0/9f219fe1d5431b6919f1a5c60db8d5d34fe546c0d8f5a8511b32f847569234ffc8032beb9e7404649a143f54e15224ecb53a3d11b6bb85c3203e573d91fca752 - languageName: node - linkType: hard - "@babel/helper-compilation-targets@npm:^7.26.5": version: 7.26.5 resolution: "@babel/helper-compilation-targets@npm:7.26.5" @@ -1120,6 +1107,17 @@ __metadata: languageName: node linkType: hard +"@babel/parser@npm:7.27.0": + version: 7.27.0 + resolution: "@babel/parser@npm:7.27.0" + dependencies: + "@babel/types": "npm:^7.27.0" + bin: + parser: ./bin/babel-parser.js + checksum: 10c0/ba2ed3f41735826546a3ef2a7634a8d10351df221891906e59b29b0a0cd748f9b0e7a6f07576858a9de8e77785aad925c8389ddef146de04ea2842047c9d2859 + languageName: node + linkType: hard + "@babel/parser@npm:^7.24.4, @babel/parser@npm:^7.28.6, @babel/parser@npm:^7.29.0": version: 7.29.0 resolution: "@babel/parser@npm:7.29.0" @@ -1142,7 +1140,7 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.27.0, @babel/parser@npm:^7.27.2, @babel/parser@npm:^7.28.5": +"@babel/parser@npm:^7.27.0": version: 7.28.5 resolution: "@babel/parser@npm:7.28.5" dependencies: @@ -1194,18 +1192,7 @@ __metadata: languageName: node linkType: hard -"@babel/template@npm:^7.27.2": - version: 7.27.2 - resolution: "@babel/template@npm:7.27.2" - dependencies: - "@babel/code-frame": "npm:^7.27.1" - "@babel/parser": "npm:^7.27.2" - "@babel/types": "npm:^7.27.1" - checksum: 10c0/ed9e9022651e463cc5f2cc21942f0e74544f1754d231add6348ff1b472985a3b3502041c0be62dc99ed2d12cfae0c51394bf827452b98a2f8769c03b87aadc81 - languageName: node - linkType: hard - -"@babel/template@npm:^7.28.6": +"@babel/template@npm:^7.27.0, @babel/template@npm:^7.28.6": version: 7.28.6 resolution: "@babel/template@npm:7.28.6" dependencies: @@ -1216,6 +1203,21 @@ __metadata: languageName: node linkType: hard +"@babel/traverse@npm:7.27.0": + version: 7.27.0 + resolution: "@babel/traverse@npm:7.27.0" + dependencies: + "@babel/code-frame": "npm:^7.26.2" + "@babel/generator": "npm:^7.27.0" + "@babel/parser": "npm:^7.27.0" + "@babel/template": "npm:^7.27.0" + "@babel/types": "npm:^7.27.0" + debug: "npm:^4.3.1" + globals: "npm:^11.1.0" + checksum: 10c0/c7af29781960dacaae51762e8bc6c4b13d6ab4b17312990fbca9fc38e19c4ad7fecaae24b1cf52fb844e8e6cdc76c70ad597f90e496bcb3cc0a1d66b41a0aa5b + languageName: node + linkType: hard + "@babel/traverse@npm:^7.25.9, @babel/traverse@npm:^7.26.10": version: 7.26.10 resolution: "@babel/traverse@npm:7.26.10" @@ -1231,21 +1233,6 @@ __metadata: languageName: node linkType: hard -"@babel/traverse@npm:^7.27.0": - version: 7.28.5 - resolution: "@babel/traverse@npm:7.28.5" - dependencies: - "@babel/code-frame": "npm:^7.27.1" - "@babel/generator": "npm:^7.28.5" - "@babel/helper-globals": "npm:^7.28.0" - "@babel/parser": "npm:^7.28.5" - "@babel/template": "npm:^7.27.2" - "@babel/types": "npm:^7.28.5" - debug: "npm:^4.3.1" - checksum: 10c0/f6c4a595993ae2b73f2d4cd9c062f2e232174d293edd4abe1d715bd6281da8d99e47c65857e8d0917d9384c65972f4acdebc6749a7c40a8fcc38b3c7fb3e706f - languageName: node - linkType: hard - "@babel/traverse@npm:^7.28.6, @babel/traverse@npm:^7.29.0": version: 7.29.0 resolution: "@babel/traverse@npm:7.29.0" @@ -1271,7 +1258,7 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.26.0, @babel/types@npm:^7.28.6, @babel/types@npm:^7.29.0": +"@babel/types@npm:^7.26.0, @babel/types@npm:^7.27.0, @babel/types@npm:^7.28.6, @babel/types@npm:^7.29.0": version: 7.29.0 resolution: "@babel/types@npm:7.29.0" dependencies: @@ -1281,7 +1268,7 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.27.1, @babel/types@npm:^7.28.5": +"@babel/types@npm:^7.28.5": version: 7.28.5 resolution: "@babel/types@npm:7.28.5" dependencies: @@ -1906,16 +1893,16 @@ __metadata: languageName: node linkType: hard -"@esbuild/aix-ppc64@npm:0.25.10": - version: 0.25.10 - resolution: "@esbuild/aix-ppc64@npm:0.25.10" +"@esbuild/aix-ppc64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/aix-ppc64@npm:0.25.12" conditions: os=aix & cpu=ppc64 languageName: node linkType: hard -"@esbuild/aix-ppc64@npm:0.25.12": - version: 0.25.12 - resolution: "@esbuild/aix-ppc64@npm:0.25.12" +"@esbuild/aix-ppc64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/aix-ppc64@npm:0.27.3" conditions: os=aix & cpu=ppc64 languageName: node linkType: hard @@ -1934,16 +1921,16 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-arm64@npm:0.25.10": - version: 0.25.10 - resolution: "@esbuild/android-arm64@npm:0.25.10" +"@esbuild/android-arm64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/android-arm64@npm:0.25.12" conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@esbuild/android-arm64@npm:0.25.12": - version: 0.25.12 - resolution: "@esbuild/android-arm64@npm:0.25.12" +"@esbuild/android-arm64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/android-arm64@npm:0.27.3" conditions: os=android & cpu=arm64 languageName: node linkType: hard @@ -1962,16 +1949,16 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-arm@npm:0.25.10": - version: 0.25.10 - resolution: "@esbuild/android-arm@npm:0.25.10" +"@esbuild/android-arm@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/android-arm@npm:0.25.12" conditions: os=android & cpu=arm languageName: node linkType: hard -"@esbuild/android-arm@npm:0.25.12": - version: 0.25.12 - resolution: "@esbuild/android-arm@npm:0.25.12" +"@esbuild/android-arm@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/android-arm@npm:0.27.3" conditions: os=android & cpu=arm languageName: node linkType: hard @@ -1990,16 +1977,16 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-x64@npm:0.25.10": - version: 0.25.10 - resolution: "@esbuild/android-x64@npm:0.25.10" +"@esbuild/android-x64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/android-x64@npm:0.25.12" conditions: os=android & cpu=x64 languageName: node linkType: hard -"@esbuild/android-x64@npm:0.25.12": - version: 0.25.12 - resolution: "@esbuild/android-x64@npm:0.25.12" +"@esbuild/android-x64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/android-x64@npm:0.27.3" conditions: os=android & cpu=x64 languageName: node linkType: hard @@ -2018,16 +2005,16 @@ __metadata: languageName: node linkType: hard -"@esbuild/darwin-arm64@npm:0.25.10": - version: 0.25.10 - resolution: "@esbuild/darwin-arm64@npm:0.25.10" +"@esbuild/darwin-arm64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/darwin-arm64@npm:0.25.12" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@esbuild/darwin-arm64@npm:0.25.12": - version: 0.25.12 - resolution: "@esbuild/darwin-arm64@npm:0.25.12" +"@esbuild/darwin-arm64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/darwin-arm64@npm:0.27.3" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard @@ -2046,16 +2033,16 @@ __metadata: languageName: node linkType: hard -"@esbuild/darwin-x64@npm:0.25.10": - version: 0.25.10 - resolution: "@esbuild/darwin-x64@npm:0.25.10" +"@esbuild/darwin-x64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/darwin-x64@npm:0.25.12" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@esbuild/darwin-x64@npm:0.25.12": - version: 0.25.12 - resolution: "@esbuild/darwin-x64@npm:0.25.12" +"@esbuild/darwin-x64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/darwin-x64@npm:0.27.3" conditions: os=darwin & cpu=x64 languageName: node linkType: hard @@ -2074,16 +2061,16 @@ __metadata: languageName: node linkType: hard -"@esbuild/freebsd-arm64@npm:0.25.10": - version: 0.25.10 - resolution: "@esbuild/freebsd-arm64@npm:0.25.10" +"@esbuild/freebsd-arm64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/freebsd-arm64@npm:0.25.12" conditions: os=freebsd & cpu=arm64 languageName: node linkType: hard -"@esbuild/freebsd-arm64@npm:0.25.12": - version: 0.25.12 - resolution: "@esbuild/freebsd-arm64@npm:0.25.12" +"@esbuild/freebsd-arm64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/freebsd-arm64@npm:0.27.3" conditions: os=freebsd & cpu=arm64 languageName: node linkType: hard @@ -2102,16 +2089,16 @@ __metadata: languageName: node linkType: hard -"@esbuild/freebsd-x64@npm:0.25.10": - version: 0.25.10 - resolution: "@esbuild/freebsd-x64@npm:0.25.10" +"@esbuild/freebsd-x64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/freebsd-x64@npm:0.25.12" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@esbuild/freebsd-x64@npm:0.25.12": - version: 0.25.12 - resolution: "@esbuild/freebsd-x64@npm:0.25.12" +"@esbuild/freebsd-x64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/freebsd-x64@npm:0.27.3" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard @@ -2130,16 +2117,16 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-arm64@npm:0.25.10": - version: 0.25.10 - resolution: "@esbuild/linux-arm64@npm:0.25.10" +"@esbuild/linux-arm64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/linux-arm64@npm:0.25.12" conditions: os=linux & cpu=arm64 languageName: node linkType: hard -"@esbuild/linux-arm64@npm:0.25.12": - version: 0.25.12 - resolution: "@esbuild/linux-arm64@npm:0.25.12" +"@esbuild/linux-arm64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/linux-arm64@npm:0.27.3" conditions: os=linux & cpu=arm64 languageName: node linkType: hard @@ -2158,16 +2145,16 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-arm@npm:0.25.10": - version: 0.25.10 - resolution: "@esbuild/linux-arm@npm:0.25.10" +"@esbuild/linux-arm@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/linux-arm@npm:0.25.12" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@esbuild/linux-arm@npm:0.25.12": - version: 0.25.12 - resolution: "@esbuild/linux-arm@npm:0.25.12" +"@esbuild/linux-arm@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/linux-arm@npm:0.27.3" conditions: os=linux & cpu=arm languageName: node linkType: hard @@ -2186,16 +2173,16 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-ia32@npm:0.25.10": - version: 0.25.10 - resolution: "@esbuild/linux-ia32@npm:0.25.10" +"@esbuild/linux-ia32@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/linux-ia32@npm:0.25.12" conditions: os=linux & cpu=ia32 languageName: node linkType: hard -"@esbuild/linux-ia32@npm:0.25.12": - version: 0.25.12 - resolution: "@esbuild/linux-ia32@npm:0.25.12" +"@esbuild/linux-ia32@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/linux-ia32@npm:0.27.3" conditions: os=linux & cpu=ia32 languageName: node linkType: hard @@ -2214,16 +2201,16 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-loong64@npm:0.25.10": - version: 0.25.10 - resolution: "@esbuild/linux-loong64@npm:0.25.10" +"@esbuild/linux-loong64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/linux-loong64@npm:0.25.12" conditions: os=linux & cpu=loong64 languageName: node linkType: hard -"@esbuild/linux-loong64@npm:0.25.12": - version: 0.25.12 - resolution: "@esbuild/linux-loong64@npm:0.25.12" +"@esbuild/linux-loong64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/linux-loong64@npm:0.27.3" conditions: os=linux & cpu=loong64 languageName: node linkType: hard @@ -2242,16 +2229,16 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-mips64el@npm:0.25.10": - version: 0.25.10 - resolution: "@esbuild/linux-mips64el@npm:0.25.10" +"@esbuild/linux-mips64el@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/linux-mips64el@npm:0.25.12" conditions: os=linux & cpu=mips64el languageName: node linkType: hard -"@esbuild/linux-mips64el@npm:0.25.12": - version: 0.25.12 - resolution: "@esbuild/linux-mips64el@npm:0.25.12" +"@esbuild/linux-mips64el@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/linux-mips64el@npm:0.27.3" conditions: os=linux & cpu=mips64el languageName: node linkType: hard @@ -2270,16 +2257,16 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-ppc64@npm:0.25.10": - version: 0.25.10 - resolution: "@esbuild/linux-ppc64@npm:0.25.10" +"@esbuild/linux-ppc64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/linux-ppc64@npm:0.25.12" conditions: os=linux & cpu=ppc64 languageName: node linkType: hard -"@esbuild/linux-ppc64@npm:0.25.12": - version: 0.25.12 - resolution: "@esbuild/linux-ppc64@npm:0.25.12" +"@esbuild/linux-ppc64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/linux-ppc64@npm:0.27.3" conditions: os=linux & cpu=ppc64 languageName: node linkType: hard @@ -2298,16 +2285,16 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-riscv64@npm:0.25.10": - version: 0.25.10 - resolution: "@esbuild/linux-riscv64@npm:0.25.10" +"@esbuild/linux-riscv64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/linux-riscv64@npm:0.25.12" conditions: os=linux & cpu=riscv64 languageName: node linkType: hard -"@esbuild/linux-riscv64@npm:0.25.12": - version: 0.25.12 - resolution: "@esbuild/linux-riscv64@npm:0.25.12" +"@esbuild/linux-riscv64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/linux-riscv64@npm:0.27.3" conditions: os=linux & cpu=riscv64 languageName: node linkType: hard @@ -2326,16 +2313,16 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-s390x@npm:0.25.10": - version: 0.25.10 - resolution: "@esbuild/linux-s390x@npm:0.25.10" +"@esbuild/linux-s390x@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/linux-s390x@npm:0.25.12" conditions: os=linux & cpu=s390x languageName: node linkType: hard -"@esbuild/linux-s390x@npm:0.25.12": - version: 0.25.12 - resolution: "@esbuild/linux-s390x@npm:0.25.12" +"@esbuild/linux-s390x@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/linux-s390x@npm:0.27.3" conditions: os=linux & cpu=s390x languageName: node linkType: hard @@ -2354,16 +2341,16 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-x64@npm:0.25.10": - version: 0.25.10 - resolution: "@esbuild/linux-x64@npm:0.25.10" +"@esbuild/linux-x64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/linux-x64@npm:0.25.12" conditions: os=linux & cpu=x64 languageName: node linkType: hard -"@esbuild/linux-x64@npm:0.25.12": - version: 0.25.12 - resolution: "@esbuild/linux-x64@npm:0.25.12" +"@esbuild/linux-x64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/linux-x64@npm:0.27.3" conditions: os=linux & cpu=x64 languageName: node linkType: hard @@ -2375,16 +2362,16 @@ __metadata: languageName: node linkType: hard -"@esbuild/netbsd-arm64@npm:0.25.10": - version: 0.25.10 - resolution: "@esbuild/netbsd-arm64@npm:0.25.10" +"@esbuild/netbsd-arm64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/netbsd-arm64@npm:0.25.12" conditions: os=netbsd & cpu=arm64 languageName: node linkType: hard -"@esbuild/netbsd-arm64@npm:0.25.12": - version: 0.25.12 - resolution: "@esbuild/netbsd-arm64@npm:0.25.12" +"@esbuild/netbsd-arm64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/netbsd-arm64@npm:0.27.3" conditions: os=netbsd & cpu=arm64 languageName: node linkType: hard @@ -2403,16 +2390,16 @@ __metadata: languageName: node linkType: hard -"@esbuild/netbsd-x64@npm:0.25.10": - version: 0.25.10 - resolution: "@esbuild/netbsd-x64@npm:0.25.10" +"@esbuild/netbsd-x64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/netbsd-x64@npm:0.25.12" conditions: os=netbsd & cpu=x64 languageName: node linkType: hard -"@esbuild/netbsd-x64@npm:0.25.12": - version: 0.25.12 - resolution: "@esbuild/netbsd-x64@npm:0.25.12" +"@esbuild/netbsd-x64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/netbsd-x64@npm:0.27.3" conditions: os=netbsd & cpu=x64 languageName: node linkType: hard @@ -2424,16 +2411,16 @@ __metadata: languageName: node linkType: hard -"@esbuild/openbsd-arm64@npm:0.25.10": - version: 0.25.10 - resolution: "@esbuild/openbsd-arm64@npm:0.25.10" +"@esbuild/openbsd-arm64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/openbsd-arm64@npm:0.25.12" conditions: os=openbsd & cpu=arm64 languageName: node linkType: hard -"@esbuild/openbsd-arm64@npm:0.25.12": - version: 0.25.12 - resolution: "@esbuild/openbsd-arm64@npm:0.25.12" +"@esbuild/openbsd-arm64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/openbsd-arm64@npm:0.27.3" conditions: os=openbsd & cpu=arm64 languageName: node linkType: hard @@ -2452,13 +2439,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/openbsd-x64@npm:0.25.10": - version: 0.25.10 - resolution: "@esbuild/openbsd-x64@npm:0.25.10" - conditions: os=openbsd & cpu=x64 - languageName: node - linkType: hard - "@esbuild/openbsd-x64@npm:0.25.12": version: 0.25.12 resolution: "@esbuild/openbsd-x64@npm:0.25.12" @@ -2466,10 +2446,10 @@ __metadata: languageName: node linkType: hard -"@esbuild/openharmony-arm64@npm:0.25.10": - version: 0.25.10 - resolution: "@esbuild/openharmony-arm64@npm:0.25.10" - conditions: os=openharmony & cpu=arm64 +"@esbuild/openbsd-x64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/openbsd-x64@npm:0.27.3" + conditions: os=openbsd & cpu=x64 languageName: node linkType: hard @@ -2480,6 +2460,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/openharmony-arm64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/openharmony-arm64@npm:0.27.3" + conditions: os=openharmony & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/sunos-x64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/sunos-x64@npm:0.21.5" @@ -2494,16 +2481,16 @@ __metadata: languageName: node linkType: hard -"@esbuild/sunos-x64@npm:0.25.10": - version: 0.25.10 - resolution: "@esbuild/sunos-x64@npm:0.25.10" +"@esbuild/sunos-x64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/sunos-x64@npm:0.25.12" conditions: os=sunos & cpu=x64 languageName: node linkType: hard -"@esbuild/sunos-x64@npm:0.25.12": - version: 0.25.12 - resolution: "@esbuild/sunos-x64@npm:0.25.12" +"@esbuild/sunos-x64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/sunos-x64@npm:0.27.3" conditions: os=sunos & cpu=x64 languageName: node linkType: hard @@ -2522,16 +2509,16 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-arm64@npm:0.25.10": - version: 0.25.10 - resolution: "@esbuild/win32-arm64@npm:0.25.10" +"@esbuild/win32-arm64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/win32-arm64@npm:0.25.12" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@esbuild/win32-arm64@npm:0.25.12": - version: 0.25.12 - resolution: "@esbuild/win32-arm64@npm:0.25.12" +"@esbuild/win32-arm64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/win32-arm64@npm:0.27.3" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard @@ -2550,16 +2537,16 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-ia32@npm:0.25.10": - version: 0.25.10 - resolution: "@esbuild/win32-ia32@npm:0.25.10" +"@esbuild/win32-ia32@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/win32-ia32@npm:0.25.12" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@esbuild/win32-ia32@npm:0.25.12": - version: 0.25.12 - resolution: "@esbuild/win32-ia32@npm:0.25.12" +"@esbuild/win32-ia32@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/win32-ia32@npm:0.27.3" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard @@ -2578,16 +2565,16 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-x64@npm:0.25.10": - version: 0.25.10 - resolution: "@esbuild/win32-x64@npm:0.25.10" +"@esbuild/win32-x64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/win32-x64@npm:0.25.12" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"@esbuild/win32-x64@npm:0.25.12": - version: 0.25.12 - resolution: "@esbuild/win32-x64@npm:0.25.12" +"@esbuild/win32-x64@npm:0.27.3": + version: 0.27.3 + resolution: "@esbuild/win32-x64@npm:0.27.3" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -3951,6 +3938,13 @@ __metadata: languageName: node linkType: hard +"@next/env@npm:16.1.7": + version: 16.1.7 + resolution: "@next/env@npm:16.1.7" + checksum: 10c0/89a9e657b29d01be04394cd8a4c917cfdd76aec76ea9a0f7670896efe7668e665713adcf72632958b0c19ce66cf7e1f39961cfd9ba69d10c5a3e08ee20d2370a + languageName: node + linkType: hard + "@next/eslint-plugin-next@npm:16.1.6": version: 16.1.6 resolution: "@next/eslint-plugin-next@npm:16.1.6" @@ -3967,6 +3961,13 @@ __metadata: languageName: node linkType: hard +"@next/swc-darwin-arm64@npm:16.1.7": + version: 16.1.7 + resolution: "@next/swc-darwin-arm64@npm:16.1.7" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@next/swc-darwin-x64@npm:16.1.6": version: 16.1.6 resolution: "@next/swc-darwin-x64@npm:16.1.6" @@ -3974,6 +3975,13 @@ __metadata: languageName: node linkType: hard +"@next/swc-darwin-x64@npm:16.1.7": + version: 16.1.7 + resolution: "@next/swc-darwin-x64@npm:16.1.7" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@next/swc-linux-arm64-gnu@npm:16.1.6": version: 16.1.6 resolution: "@next/swc-linux-arm64-gnu@npm:16.1.6" @@ -3981,6 +3989,13 @@ __metadata: languageName: node linkType: hard +"@next/swc-linux-arm64-gnu@npm:16.1.7": + version: 16.1.7 + resolution: "@next/swc-linux-arm64-gnu@npm:16.1.7" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + "@next/swc-linux-arm64-musl@npm:16.1.6": version: 16.1.6 resolution: "@next/swc-linux-arm64-musl@npm:16.1.6" @@ -3988,6 +4003,13 @@ __metadata: languageName: node linkType: hard +"@next/swc-linux-arm64-musl@npm:16.1.7": + version: 16.1.7 + resolution: "@next/swc-linux-arm64-musl@npm:16.1.7" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + "@next/swc-linux-x64-gnu@npm:16.1.6": version: 16.1.6 resolution: "@next/swc-linux-x64-gnu@npm:16.1.6" @@ -3995,6 +4017,13 @@ __metadata: languageName: node linkType: hard +"@next/swc-linux-x64-gnu@npm:16.1.7": + version: 16.1.7 + resolution: "@next/swc-linux-x64-gnu@npm:16.1.7" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + "@next/swc-linux-x64-musl@npm:16.1.6": version: 16.1.6 resolution: "@next/swc-linux-x64-musl@npm:16.1.6" @@ -4002,6 +4031,13 @@ __metadata: languageName: node linkType: hard +"@next/swc-linux-x64-musl@npm:16.1.7": + version: 16.1.7 + resolution: "@next/swc-linux-x64-musl@npm:16.1.7" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + "@next/swc-win32-arm64-msvc@npm:16.1.6": version: 16.1.6 resolution: "@next/swc-win32-arm64-msvc@npm:16.1.6" @@ -4009,6 +4045,13 @@ __metadata: languageName: node linkType: hard +"@next/swc-win32-arm64-msvc@npm:16.1.7": + version: 16.1.7 + resolution: "@next/swc-win32-arm64-msvc@npm:16.1.7" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "@next/swc-win32-x64-msvc@npm:16.1.6": version: 16.1.6 resolution: "@next/swc-win32-x64-msvc@npm:16.1.6" @@ -4016,6 +4059,13 @@ __metadata: languageName: node linkType: hard +"@next/swc-win32-x64-msvc@npm:16.1.7": + version: 16.1.7 + resolution: "@next/swc-win32-x64-msvc@npm:16.1.7" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@nodelib/fs.scandir@npm:2.1.5": version: 2.1.5 resolution: "@nodelib/fs.scandir@npm:2.1.5" @@ -7180,13 +7230,13 @@ __metadata: languageName: node linkType: hard -"@react-email/preview-server@npm:5.2.8": - version: 5.2.8 - resolution: "@react-email/preview-server@npm:5.2.8" +"@react-email/preview-server@npm:5.2.10": + version: 5.2.10 + resolution: "@react-email/preview-server@npm:5.2.10" dependencies: - esbuild: "npm:0.25.10" - next: "npm:16.1.6" - checksum: 10c0/e3832f80d73aeab214423c8675072eb8ce635d0750fec5b3683359ed4a7b69451ebcef314e3a27b0b8d9c58c009005cea426e83706557d386c4e420fb74c6d83 + esbuild: "npm:0.27.3" + next: "npm:16.1.7" + checksum: 10c0/6b4c59048c088c94fa1020e44d74c91527ab48a2c02f331ddd56a855fe4254f2bb4158f6a8e2f6ec62ddaba7acb8c5b44f81374879b7d377a9d6b0629f09a330 languageName: node linkType: hard @@ -8769,7 +8819,7 @@ __metadata: posthog-node: "npm:^5.24.15" prom-client: "npm:^15.1.3" redlock: "npm:5.0.0-beta.2" - simple-git: "npm:^3.27.0" + simple-git: "npm:^3.33.0" tsc-watch: "npm:^6.2.0" tsx: "npm:^4.19.1" typescript: "npm:^5.6.2" @@ -8958,7 +9008,7 @@ __metadata: "@radix-ui/react-toggle": "npm:^1.1.0" "@radix-ui/react-tooltip": "npm:^1.1.4" "@react-email/components": "npm:^1.0.2" - "@react-email/preview-server": "npm:5.2.8" + "@react-email/preview-server": "npm:5.2.10" "@react-email/render": "npm:^2.0.0" "@react-grab/mcp": "npm:^0.1.23" "@replit/codemirror-lang-csharp": "npm:^6.2.0" @@ -8974,8 +9024,6 @@ __metadata: "@sourcebot/schemas": "workspace:*" "@sourcebot/shared": "workspace:*" "@ssddanbrown/codemirror-lang-twig": "npm:^1.0.0" - "@stripe/react-stripe-js": "npm:^3.1.1" - "@stripe/stripe-js": "npm:^5.6.0" "@tailwindcss/typography": "npm:^0.5.16" "@tanstack/eslint-plugin-query": "npm:^5.74.7" "@tanstack/react-query": "npm:^5.53.3" @@ -9044,6 +9092,7 @@ __metadata: linguist-languages: "npm:^9.3.1" lucide-react: "npm:^0.517.0" micromatch: "npm:^4.0.8" + minidenticons: "npm:^4.2.1" next: "npm:16.1.6" next-auth: "npm:^5.0.0-beta.30" next-navigation-guard: "npm:^0.2.0" @@ -9062,7 +9111,7 @@ __metadata: react: "npm:19.2.4" react-device-detect: "npm:^2.2.3" react-dom: "npm:19.2.4" - react-email: "npm:^5.1.0" + react-email: "npm:^5.2.10" react-grab: "npm:^0.1.23" react-hook-form: "npm:^7.53.0" react-hotkeys-hook: "npm:^4.5.1" @@ -9077,13 +9126,12 @@ __metadata: scroll-into-view-if-needed: "npm:^3.1.0" server-only: "npm:^0.0.1" sharp: "npm:^0.33.5" - simple-git: "npm:^3.27.0" + simple-git: "npm:^3.33.0" slate: "npm:^0.117.0" slate-dom: "npm:^0.116.0" slate-history: "npm:^0.113.1" slate-react: "npm:^0.117.1" strip-json-comments: "npm:^5.0.1" - stripe: "npm:^17.6.0" tailwind-merge: "npm:^2.5.2" tailwindcss: "npm:^3.4.1" tailwindcss-animate: "npm:^1.0.7" @@ -9119,26 +9167,6 @@ __metadata: languageName: node linkType: hard -"@stripe/react-stripe-js@npm:^3.1.1": - version: 3.5.1 - resolution: "@stripe/react-stripe-js@npm:3.5.1" - dependencies: - prop-types: "npm:^15.7.2" - peerDependencies: - "@stripe/stripe-js": ">=1.44.1 <7.0.0" - react: ">=16.8.0 <20.0.0" - react-dom: ">=16.8.0 <20.0.0" - checksum: 10c0/999f52c420657a9a4f287de12f4e1c560e168f10f8e12f6c6f4314fb170478d4c594fc8dcec7f3bdc4413f21cbc23155f8849b19ed0589e5bcb558f065c63cfe - languageName: node - linkType: hard - -"@stripe/stripe-js@npm:^5.6.0": - version: 5.10.0 - resolution: "@stripe/stripe-js@npm:5.10.0" - checksum: 10c0/0309007baaf939931de0e18ec8230bdfa41b1644cd5f65d72168ef0a05ea4d14da9a95ff628c94f9f1e222577296369b7490534803ce1ce18d08a557ae562644 - languageName: node - linkType: hard - "@swc/helpers@npm:0.5.15": version: 0.5.15 resolution: "@swc/helpers@npm:0.5.15" @@ -9603,7 +9631,7 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:>=10.0.0, @types/node@npm:>=8.1.0, @types/node@npm:^22.7.5": +"@types/node@npm:*, @types/node@npm:>=10.0.0, @types/node@npm:^22.7.5": version: 22.13.11 resolution: "@types/node@npm:22.13.11" dependencies: @@ -10987,6 +11015,15 @@ __metadata: languageName: node linkType: hard +"baseline-browser-mapping@npm:^2.9.19": + version: 2.10.13 + resolution: "baseline-browser-mapping@npm:2.10.13" + bin: + baseline-browser-mapping: dist/cli.cjs + checksum: 10c0/3296604492f600927a9f519c81164522ac26456e63eb7b6816e39bfbb184494b48c58490639f2c0e35be97969d3a03613fddddbfdd3074710592369ed36957d5 + languageName: node + linkType: hard + "bcryptjs@npm:^3.0.2": version: 3.0.2 resolution: "bcryptjs@npm:3.0.2" @@ -11923,7 +11960,7 @@ __metadata: languageName: node linkType: hard -"consola@npm:^3.2.3, consola@npm:^3.4.0": +"consola@npm:^3.2.3, consola@npm:^3.4.2": version: 3.4.2 resolution: "consola@npm:3.4.2" checksum: 10c0/7cebe57ecf646ba74b300bcce23bff43034ed6fbec9f7e39c27cee1dc00df8a21cd336b466ad32e304ea70fba04ec9e890c200270de9a526ce021ba8a7e4c11a @@ -13125,36 +13162,36 @@ __metadata: languageName: node linkType: hard -"esbuild@npm:0.25.10": - version: 0.25.10 - resolution: "esbuild@npm:0.25.10" - dependencies: - "@esbuild/aix-ppc64": "npm:0.25.10" - "@esbuild/android-arm": "npm:0.25.10" - "@esbuild/android-arm64": "npm:0.25.10" - "@esbuild/android-x64": "npm:0.25.10" - "@esbuild/darwin-arm64": "npm:0.25.10" - "@esbuild/darwin-x64": "npm:0.25.10" - "@esbuild/freebsd-arm64": "npm:0.25.10" - "@esbuild/freebsd-x64": "npm:0.25.10" - "@esbuild/linux-arm": "npm:0.25.10" - "@esbuild/linux-arm64": "npm:0.25.10" - "@esbuild/linux-ia32": "npm:0.25.10" - "@esbuild/linux-loong64": "npm:0.25.10" - "@esbuild/linux-mips64el": "npm:0.25.10" - "@esbuild/linux-ppc64": "npm:0.25.10" - "@esbuild/linux-riscv64": "npm:0.25.10" - "@esbuild/linux-s390x": "npm:0.25.10" - "@esbuild/linux-x64": "npm:0.25.10" - "@esbuild/netbsd-arm64": "npm:0.25.10" - "@esbuild/netbsd-x64": "npm:0.25.10" - "@esbuild/openbsd-arm64": "npm:0.25.10" - "@esbuild/openbsd-x64": "npm:0.25.10" - "@esbuild/openharmony-arm64": "npm:0.25.10" - "@esbuild/sunos-x64": "npm:0.25.10" - "@esbuild/win32-arm64": "npm:0.25.10" - "@esbuild/win32-ia32": "npm:0.25.10" - "@esbuild/win32-x64": "npm:0.25.10" +"esbuild@npm:0.27.3": + version: 0.27.3 + resolution: "esbuild@npm:0.27.3" + dependencies: + "@esbuild/aix-ppc64": "npm:0.27.3" + "@esbuild/android-arm": "npm:0.27.3" + "@esbuild/android-arm64": "npm:0.27.3" + "@esbuild/android-x64": "npm:0.27.3" + "@esbuild/darwin-arm64": "npm:0.27.3" + "@esbuild/darwin-x64": "npm:0.27.3" + "@esbuild/freebsd-arm64": "npm:0.27.3" + "@esbuild/freebsd-x64": "npm:0.27.3" + "@esbuild/linux-arm": "npm:0.27.3" + "@esbuild/linux-arm64": "npm:0.27.3" + "@esbuild/linux-ia32": "npm:0.27.3" + "@esbuild/linux-loong64": "npm:0.27.3" + "@esbuild/linux-mips64el": "npm:0.27.3" + "@esbuild/linux-ppc64": "npm:0.27.3" + "@esbuild/linux-riscv64": "npm:0.27.3" + "@esbuild/linux-s390x": "npm:0.27.3" + "@esbuild/linux-x64": "npm:0.27.3" + "@esbuild/netbsd-arm64": "npm:0.27.3" + "@esbuild/netbsd-x64": "npm:0.27.3" + "@esbuild/openbsd-arm64": "npm:0.27.3" + "@esbuild/openbsd-x64": "npm:0.27.3" + "@esbuild/openharmony-arm64": "npm:0.27.3" + "@esbuild/sunos-x64": "npm:0.27.3" + "@esbuild/win32-arm64": "npm:0.27.3" + "@esbuild/win32-ia32": "npm:0.27.3" + "@esbuild/win32-x64": "npm:0.27.3" dependenciesMeta: "@esbuild/aix-ppc64": optional: true @@ -13210,7 +13247,7 @@ __metadata: optional: true bin: esbuild: bin/esbuild - checksum: 10c0/8ee5fdd43ed0d4092ce7f41577c63147f54049d5617763f0549c638bbe939e8adaa8f1a2728adb63417eb11df51956b7b0d8eb88ee08c27ad1d42960256158fa + checksum: 10c0/fdc3f87a3f08b3ef98362f37377136c389a0d180fda4b8d073b26ba930cf245521db0a368f119cc7624bc619248fff1439f5811f062d853576f8ffa3df8ee5f1 languageName: node linkType: hard @@ -14673,7 +14710,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^11.0.0, glob@npm:^11.1.0": +"glob@npm:^11.1.0": version: 11.1.0 resolution: "glob@npm:11.1.0" dependencies: @@ -17321,6 +17358,13 @@ __metadata: languageName: node linkType: hard +"minidenticons@npm:^4.2.1": + version: 4.2.1 + resolution: "minidenticons@npm:4.2.1" + checksum: 10c0/086f78ce0fa36030275a206c66e8c228dc5c035b4909ea42e298c0595b305f178733230d9d11d9ff8411d6d4a1a390f07fe75871bfb82c1afdaeca4ae253bf1a + languageName: node + linkType: hard + "minimalistic-assert@npm:^1.0.0": version: 1.0.1 resolution: "minimalistic-assert@npm:1.0.1" @@ -17702,6 +17746,66 @@ __metadata: languageName: node linkType: hard +"next@npm:16.1.7": + version: 16.1.7 + resolution: "next@npm:16.1.7" + dependencies: + "@next/env": "npm:16.1.7" + "@next/swc-darwin-arm64": "npm:16.1.7" + "@next/swc-darwin-x64": "npm:16.1.7" + "@next/swc-linux-arm64-gnu": "npm:16.1.7" + "@next/swc-linux-arm64-musl": "npm:16.1.7" + "@next/swc-linux-x64-gnu": "npm:16.1.7" + "@next/swc-linux-x64-musl": "npm:16.1.7" + "@next/swc-win32-arm64-msvc": "npm:16.1.7" + "@next/swc-win32-x64-msvc": "npm:16.1.7" + "@swc/helpers": "npm:0.5.15" + baseline-browser-mapping: "npm:^2.9.19" + caniuse-lite: "npm:^1.0.30001579" + postcss: "npm:8.4.31" + sharp: "npm:^0.34.4" + styled-jsx: "npm:5.1.6" + peerDependencies: + "@opentelemetry/api": ^1.1.0 + "@playwright/test": ^1.51.1 + babel-plugin-react-compiler: "*" + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + sass: ^1.3.0 + dependenciesMeta: + "@next/swc-darwin-arm64": + optional: true + "@next/swc-darwin-x64": + optional: true + "@next/swc-linux-arm64-gnu": + optional: true + "@next/swc-linux-arm64-musl": + optional: true + "@next/swc-linux-x64-gnu": + optional: true + "@next/swc-linux-x64-musl": + optional: true + "@next/swc-win32-arm64-msvc": + optional: true + "@next/swc-win32-x64-msvc": + optional: true + sharp: + optional: true + peerDependenciesMeta: + "@opentelemetry/api": + optional: true + "@playwright/test": + optional: true + babel-plugin-react-compiler: + optional: true + sass: + optional: true + bin: + next: dist/bin/next + checksum: 10c0/f6cebb3ce57d267cd92fb364b56710004c883baafea9cfed36434c0cd51db74299d59094ab782674d9813f10ce2891b633bc6d94e7e38aa9af7a1870194818d0 + languageName: node + linkType: hard + "nice-try@npm:^1.0.4": version: 1.0.5 resolution: "nice-try@npm:1.0.5" @@ -17905,18 +18009,18 @@ __metadata: languageName: node linkType: hard -"nypm@npm:0.6.0": - version: 0.6.0 - resolution: "nypm@npm:0.6.0" +"nypm@npm:0.6.2": + version: 0.6.2 + resolution: "nypm@npm:0.6.2" dependencies: citty: "npm:^0.1.6" - consola: "npm:^3.4.0" + consola: "npm:^3.4.2" pathe: "npm:^2.0.3" - pkg-types: "npm:^2.0.0" - tinyexec: "npm:^0.3.2" + pkg-types: "npm:^2.3.0" + tinyexec: "npm:^1.0.1" bin: nypm: dist/cli.mjs - checksum: 10c0/899f16c2df1bdf3ef4de5f7d4ed5530e2e1ca097cc7dedbaa25abb6b8e44bb470c25cd26639f6e3e4f5734867e61f7f77c4ed5dfbe86b2a1bdef4525a2dc0026 + checksum: 10c0/b1aca658e29ed616ad6e487f9c3fd76773485ad75c1f99efe130ccb304de60b639a3dda43c3ce6c060113a3eebaee7ccbea554f5fbd1f244474181dc9bf3f17c languageName: node linkType: hard @@ -18649,7 +18753,7 @@ __metadata: languageName: node linkType: hard -"pkg-types@npm:^2.0.0": +"pkg-types@npm:^2.3.0": version: 2.3.0 resolution: "pkg-types@npm:2.3.0" dependencies: @@ -19011,7 +19115,7 @@ __metadata: languageName: node linkType: hard -"prop-types@npm:^15.6.2, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1": +"prop-types@npm:^15.6.2, prop-types@npm:^15.8.1": version: 15.8.1 resolution: "prop-types@npm:15.8.1" dependencies: @@ -19262,30 +19366,30 @@ __metadata: languageName: node linkType: hard -"react-email@npm:^5.1.0": - version: 5.1.0 - resolution: "react-email@npm:5.1.0" +"react-email@npm:^5.2.10": + version: 5.2.10 + resolution: "react-email@npm:5.2.10" dependencies: - "@babel/parser": "npm:^7.27.0" - "@babel/traverse": "npm:^7.27.0" + "@babel/parser": "npm:7.27.0" + "@babel/traverse": "npm:7.27.0" chokidar: "npm:^4.0.3" commander: "npm:^13.0.0" conf: "npm:^15.0.2" debounce: "npm:^2.0.0" - esbuild: "npm:^0.25.0" - glob: "npm:^11.0.0" + esbuild: "npm:0.27.3" + glob: "npm:^13.0.6" jiti: "npm:2.4.2" log-symbols: "npm:^7.0.0" mime-types: "npm:^3.0.0" normalize-path: "npm:^3.0.0" - nypm: "npm:0.6.0" + nypm: "npm:0.6.2" ora: "npm:^8.0.0" prompts: "npm:2.4.2" socket.io: "npm:^4.8.1" tsconfig-paths: "npm:4.2.0" bin: - email: dist/index.js - checksum: 10c0/7dd6d344a9d27c4ac9238fe7c67ed0e9288200176a1ecdc27470163ea3ad48e3b782e895c21ee7870430dc873dbe930c7ea37d02199308d93eee25e16206db70 + email: dist/index.mjs + checksum: 10c0/0749af457911de31fd65fc3efde5e706c23ea745293f0d4e347d6587a4b3cf14485b9bb8d2ec9408b06160943a55175275e8c5be0d392388f42959950269be21 languageName: node linkType: hard @@ -20666,14 +20770,14 @@ __metadata: languageName: node linkType: hard -"simple-git@npm:^3.27.0": - version: 3.27.0 - resolution: "simple-git@npm:3.27.0" +"simple-git@npm:^3.33.0": + version: 3.33.0 + resolution: "simple-git@npm:3.33.0" dependencies: "@kwsites/file-exists": "npm:^1.1.1" "@kwsites/promise-deferred": "npm:^1.1.1" - debug: "npm:^4.3.5" - checksum: 10c0/ef56cabea585377d3e0ca30e4e93447f465d91f23eaf751693cc31f366b5f7636facf52ad5bcd598bfdf295fa60732e7a394303d378995b52e2d221d92e5f9f4 + debug: "npm:^4.4.0" + checksum: 10c0/463e91f3ee04b7fc445284c64502a4ee3d607f626f18c8bcc036815a30fe178d2216976e683c6368edd7b3093801d6e534deeb8e700a4863a76ef23f881a0712 languageName: node linkType: hard @@ -21242,16 +21346,6 @@ __metadata: languageName: node linkType: hard -"stripe@npm:^17.6.0": - version: 17.7.0 - resolution: "stripe@npm:17.7.0" - dependencies: - "@types/node": "npm:>=8.1.0" - qs: "npm:^6.11.0" - checksum: 10c0/df67c6d455bd0dd87140640924c220fa9581fc00c3267d171f407c8d088f946f61e3ae7e88a89e7dd705b10fd5254630fc943222eb6f003390ebafbd391f81b2 - languageName: node - linkType: hard - "strnum@npm:^2.1.2": version: 2.1.2 resolution: "strnum@npm:2.1.2" @@ -21574,13 +21668,20 @@ __metadata: languageName: node linkType: hard -"tinyexec@npm:^0.3.1, tinyexec@npm:^0.3.2": +"tinyexec@npm:^0.3.1": version: 0.3.2 resolution: "tinyexec@npm:0.3.2" checksum: 10c0/3efbf791a911be0bf0821eab37a3445c2ba07acc1522b1fa84ae1e55f10425076f1290f680286345ed919549ad67527d07281f1c19d584df3b74326909eb1f90 languageName: node linkType: hard +"tinyexec@npm:^1.0.1": + version: 1.0.4 + resolution: "tinyexec@npm:1.0.4" + checksum: 10c0/d4a5bbcf6bdb23527a4b74c4aa566f41432167112fe76f420ec7e3a90a3ecfd3a7d944383e2719fc3987b69400f7b928daf08700d145fb527c2e80ec01e198bd + languageName: node + linkType: hard + "tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.9": version: 0.2.12 resolution: "tinyglobby@npm:0.2.12"