diff --git a/.env.development b/.env.development
index e9029f539..89b9b9e14 100644
--- a/.env.development
+++ b/.env.development
@@ -13,7 +13,6 @@ CTAGS_COMMAND=ctags
# @see: https://authjs.dev/getting-started/deployment#auth_secret
AUTH_SECRET="00000000000000000000000000000000000000000000"
AUTH_URL="http://localhost:3000"
-# AUTH_CREDENTIALS_LOGIN_ENABLED=true
DATA_CACHE_DIR=${PWD}/.sourcebot # Path to the sourcebot cache dir (ex. ~/sourcebot/.sourcebot)
SOURCEBOT_PUBLIC_KEY_PATH=${PWD}/public.pem
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ab841e434..73582aabb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+### Changed
+- Changed the workspace "Access" page to a "Security" page. [#1303](https://github.com/sourcebot-dev/sourcebot/pull/1303)
+
+### Added
+- Added the ability to configure email code and credentials login from the security settings. [#1303](https://github.com/sourcebot-dev/sourcebot/pull/1303)
+- Added a list of configured SSO providers from the security settings. [#1303](https://github.com/sourcebot-dev/sourcebot/pull/1303)
+
## [5.0.2] - 2026-06-11
### Changed
diff --git a/docs/docs/configuration/auth/access-settings.mdx b/docs/docs/configuration/auth/access-settings.mdx
index add0c3827..96ad0c5a7 100644
--- a/docs/docs/configuration/auth/access-settings.mdx
+++ b/docs/docs/configuration/auth/access-settings.mdx
@@ -11,9 +11,11 @@ By default, Sourcebot requires new members to be approved by an owner of the dep
to configure this behavior.
### Configuration
-Member approval can be configured by an owner of the deployment by navigating to **Settings -> Access**, or by setting the `REQUIRE_APPROVAL_NEW_MEMBERS` environment variable. When the environment variable is set, the UI toggle is disabled and the setting is controlled by the environment variable.
+Member approval can be configured by an owner of the deployment by navigating to **Settings -> Security**.
-
+
+
+
### Managing Requests
@@ -27,7 +29,9 @@ Owners can see and manage all pending join requests by navigating to **Settings
If member approval is required, an owner of the deployment can enable an invite link. When enabled, users
can use this invite link to register and be automatically added to the organization without approval:
-
+
+
+
# Anonymous access
@@ -36,6 +40,10 @@ can use this invite link to register and be automatically added to the organizat
By default, your Sourcebot deployment is gated with a login page. If you'd like users to access the deployment anonymously, you can enable anonymous access.
-This can be enabled by navigating to **Settings -> Access** or by setting the `FORCE_ENABLE_ANONYMOUS_ACCESS` environment variable.
+This can be enabled by navigating to **Settings -> Access**.
+
+
+
+
When accessing Sourcebot anonymously, a user's permissions are limited to that of the [Guest](/docs/configuration/auth/roles-and-permissions) role.
diff --git a/docs/docs/configuration/auth/providers.mdx b/docs/docs/configuration/auth/providers.mdx
index 61e682a09..b28f8c32c 100644
--- a/docs/docs/configuration/auth/providers.mdx
+++ b/docs/docs/configuration/auth/providers.mdx
@@ -10,19 +10,18 @@ If there's an authentication provider you'd like us to support, please [reach ou
# Core Authentication Providers
### Email / Password
----
-Email / password authentication is enabled by default. It can be **disabled** by setting `AUTH_CREDENTIALS_LOGIN_ENABLED` to `false`.
+Email / password authentication is enabled by default. You can toggle it from **Settings → Security** using the **Email login** setting.
-### Email codes
----
-Email codes are 6 digit codes sent to a provided email. Email codes are enabled when transactional emails are configured using the following environment variables:
-
-- `AUTH_EMAIL_CODE_LOGIN_ENABLED`
-- `SMTP_CONNECTION_URL`
-- `EMAIL_FROM_ADDRESS`
+
+
+
+### Email codes
+Email codes are 6 digit codes sent to a provided email. Email codes are enabled when [transactional emails](/docs/configuration/transactional-emails) and the **Email code** setting is toggled from **Settings → Security**:
-See [transactional emails](/docs/configuration/transactional-emails) for more details.
+
+
+
# Enterprise Authentication Providers
diff --git a/docs/docs/configuration/config-file.mdx b/docs/docs/configuration/config-file.mdx
index d6162451d..ccf3e2301 100644
--- a/docs/docs/configuration/config-file.mdx
+++ b/docs/docs/configuration/config-file.mdx
@@ -49,7 +49,6 @@ The following are settings that can be provided in your config file to modify So
| `maxRepoGarbageCollectionJobConcurrency` | number | 8 | 1 | Concurrent repo‑garbage‑collection jobs. |
| `repoGarbageCollectionGracePeriodMs` | number | 10 seconds | 1 | Grace period to avoid deleting shards while loading. |
| `repoIndexTimeoutMs` | number | 2 hours | 1 | Timeout for a single repo‑indexing run. |
-| `enablePublicAccess` **(deprecated)** | boolean | false | — | Use the `FORCE_ENABLE_ANONYMOUS_ACCESS` environment variable instead. |
| `repoDrivenPermissionSyncIntervalMs` | number | 24 hours | 1 | Interval at which the repo permission syncer should run. |
| `userDrivenPermissionSyncIntervalMs` | number | 24 hours | 1 | Interval at which the user permission syncer should run. |
| `experiment_repoDrivenPermissionSyncIntervalMs` **(deprecated)** | number | 24 hours | 1 | Use `repoDrivenPermissionSyncIntervalMs` instead. |
diff --git a/docs/docs/configuration/environment-variables.mdx b/docs/docs/configuration/environment-variables.mdx
index 8c0415fc4..2d4c57acb 100644
--- a/docs/docs/configuration/environment-variables.mdx
+++ b/docs/docs/configuration/environment-variables.mdx
@@ -10,8 +10,6 @@ The following environment variables allow you to configure your Sourcebot deploy
| Variable | Default | Description |
| :------- | :------ | :---------- |
-| `AUTH_CREDENTIALS_LOGIN_ENABLED` | `true` |
Enables/disables authentication with basic credentials. Username and passwords are stored encrypted at rest within the postgres database. Checkout the [auth docs](/docs/configuration/auth/authentication) for more info
| -| `AUTH_EMAIL_CODE_LOGIN_ENABLED` | `false` |Enables/disables authentication with a login code that's sent to a users email. `SMTP_CONNECTION_URL` and `EMAIL_FROM_ADDRESS` must also be set. Checkout the [auth docs](/docs/configuration/auth/authentication) for more info
| | `AUTH_SECRET` **(required)** | - |Used to validate login session cookies. Genearte one with `openssl rand -base64 33`.
| | `AUTH_SESSION_MAX_AGE_SECONDS` | `2592000` (30 days) |Relative time from now in seconds when to expire the session.
| | `AUTH_SESSION_UPDATE_AGE_SECONDS` | `86400` (1 day) |How often the session should be updated in seconds. If set to `0`, session is updated every time.
| @@ -24,8 +22,6 @@ The following environment variables allow you to configure your Sourcebot deploy | `DATA_DIR` | `/data` |The directory within the container to store all persistent data. Typically, this directory will be volume mapped such that data is persisted across container restarts (e.g., `docker run -v $(pwd):/data`)
| | `DATABASE_URL` **(required)** | - |Connection string of your Postgres database, e.g. `postgresql://user:password@host:5432/sourcebot`.
If you'd like to use a non-default schema, you can provide it as a parameter in the database url.
You can also use `DATABASE_HOST`, `DATABASE_USERNAME`, `DATABASE_PASSWORD`, `DATABASE_NAME`, and `DATABASE_ARGS` to construct the database url.
| | `EMAIL_FROM_ADDRESS` | `-` |The email address that transactional emails will be sent from. See [this doc](/docs/configuration/transactional-emails) for more info.
| -| `FORCE_ENABLE_ANONYMOUS_ACCESS` | `false` |When enabled, [anonymous access](/docs/configuration/auth/access-settings#anonymous-access) to the organization will always be enabled
-| `REQUIRE_APPROVAL_NEW_MEMBERS` | - |When set, controls whether new users require approval before accessing your deployment. If not set, the setting can be configured via the UI. See [member approval](/docs/configuration/auth/access-settings#member-approval) for more info.
| `REDIS_URL` **(required)** | - |Connection string of your Redis instance, e.g. `redis://host:6379`.
To enable TLS, see [this doc](/docs/deployment/infrastructure/redis#tls).
| | `REDIS_REMOVE_ON_COMPLETE` | `0` |Controls how many completed jobs are allowed to remain in Redis queues
| | `REDIS_REMOVE_ON_FAIL` | `100` |Controls how many failed jobs are allowed to remain in Redis queues
| @@ -54,10 +50,8 @@ The following environment variables allow you to configure your Sourcebot deploy | `AUTH_EE_GCP_IAP_AUDIENCE` | - |The GCP IAP audience to use when verifying JWT tokens. Must be set to enable GCP IAP JIT provisioning
| | `PERMISSION_SYNC_ENABLED` | `false` |Enables [permission syncing](/docs/features/permission-syncing).
| | `PERMISSION_SYNC_REPO_DRIVEN_ENABLED` | `true` |Enables/disables [repo-driven permission syncing](/docs/features/permission-syncing#how-it-works). Only applies when `PERMISSION_SYNC_ENABLED` is `true`.
| -| `EXPERIMENT_EE_PERMISSION_SYNC_ENABLED` **(deprecated)** | `false` |Deprecated. Use `PERMISSION_SYNC_ENABLED` instead.
| | `AUTH_EE_ALLOW_EMAIL_ACCOUNT_LINKING` | `true` |When enabled, different SSO accounts with the same email address will automatically be linked.
| | `DISABLE_API_KEY_CREATION_FOR_NON_OWNER_USERS` | `false` |When enabled, only organization owners can create API keys. Non-owner members will receive a `403` error if they attempt to create one.
| -| `EXPERIMENT_DISABLE_API_KEY_CREATION_FOR_NON_ADMIN_USERS` **(deprecated)** | `false` |Deprecated. Use `DISABLE_API_KEY_CREATION_FOR_NON_OWNER_USERS` instead.
| | `DISABLE_API_KEY_USAGE_FOR_NON_OWNER_USERS` | `false` |When enabled, only organization owners can create or use API keys. Non-owner members will receive a `403` error if they attempt to create or authenticate with an API key. If you only want to restrict creation (not usage), use `DISABLE_API_KEY_CREATION_FOR_NON_OWNER_USERS` instead.
| diff --git a/docs/images/anonymous_access_toggle.png b/docs/images/anonymous_access_toggle.png new file mode 100644 index 000000000..bc86be974 Binary files /dev/null and b/docs/images/anonymous_access_toggle.png differ diff --git a/docs/images/demote_to_member.png b/docs/images/demote_to_member.png index 5b8282f95..775e79546 100644 Binary files a/docs/images/demote_to_member.png and b/docs/images/demote_to_member.png differ diff --git a/docs/images/email_code_login_setting.png b/docs/images/email_code_login_setting.png new file mode 100644 index 000000000..870e6f40e Binary files /dev/null and b/docs/images/email_code_login_setting.png differ diff --git a/docs/images/email_password_login_setting.png b/docs/images/email_password_login_setting.png new file mode 100644 index 000000000..def59a00e Binary files /dev/null and b/docs/images/email_password_login_setting.png differ diff --git a/docs/images/invite_link_toggle.png b/docs/images/invite_link_toggle.png index 979033e82..78391408a 100644 Binary files a/docs/images/invite_link_toggle.png and b/docs/images/invite_link_toggle.png differ diff --git a/docs/images/managing_owners.png b/docs/images/managing_owners.png index 7a4d0e04c..c3afa0124 100644 Binary files a/docs/images/managing_owners.png and b/docs/images/managing_owners.png differ diff --git a/docs/images/member_approval_toggle.png b/docs/images/member_approval_toggle.png index e6c2cfac0..1de0666a5 100644 Binary files a/docs/images/member_approval_toggle.png and b/docs/images/member_approval_toggle.png differ diff --git a/docs/images/owner_leave_org.png b/docs/images/owner_leave_org.png index da293c2d4..581a0eb2a 100644 Binary files a/docs/images/owner_leave_org.png and b/docs/images/owner_leave_org.png differ diff --git a/docs/images/promote_to_owner.png b/docs/images/promote_to_owner.png index 7f197e113..a1c9b85de 100644 Binary files a/docs/images/promote_to_owner.png and b/docs/images/promote_to_owner.png differ diff --git a/packages/db/prisma/migrations/20260611195453_add_org_access_settings/migration.sql b/packages/db/prisma/migrations/20260611195453_add_org_access_settings/migration.sql new file mode 100644 index 000000000..f3657aea3 --- /dev/null +++ b/packages/db/prisma/migrations/20260611195453_add_org_access_settings/migration.sql @@ -0,0 +1,11 @@ +-- AlterTable +ALTER TABLE "Org" ADD COLUMN "isAnonymousAccessEnabled" BOOLEAN NOT NULL DEFAULT false, +ADD COLUMN "isCredentialsLoginEnabled" BOOLEAN NOT NULL DEFAULT true, +ADD COLUMN "isEmailCodeLoginEnabled" BOOLEAN NOT NULL DEFAULT false; + +-- Backfill the new dedicated column from the legacy `metadata.anonymousAccessEnabled` +-- value (see orgMetadataSchema in packages/web/src/types.ts) so existing deployments +-- that had anonymous access enabled keep it after upgrading. +UPDATE "Org" +SET "isAnonymousAccessEnabled" = true +WHERE "metadata"->>'anonymousAccessEnabled' = 'true'; diff --git a/packages/db/prisma/migrations/20260612182049_remove_org_metadata_field/migration.sql b/packages/db/prisma/migrations/20260612182049_remove_org_metadata_field/migration.sql new file mode 100644 index 000000000..0dece6bdc --- /dev/null +++ b/packages/db/prisma/migrations/20260612182049_remove_org_metadata_field/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - You are about to drop the column `metadata` on the `Org` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "Org" DROP COLUMN "metadata"; diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index 815788d64..ae0a53840 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -274,10 +274,31 @@ model Org { apiKeys ApiKey[] isOnboarded Boolean @default(false) imageUrl String? - metadata Json? // For schema see orgMetadataSchema in packages/web/src/types.ts + /// @deprecated This property can be controlled by the environment + /// variable `REQUIRE_APPROVAL_NEW_MEMBERS`. To ensure that we use + /// the correct setting, use the helper function `isMemberApprovalRequired` + /// in shared/src/utils.ts memberApprovalRequired Boolean @default(true) + /// @deprecated This property can be controlled by the environment + /// variable `AUTH_CREDENTIALS_LOGIN_ENABLED`. To ensure that we use + /// the correct setting, use the helper function `isCredentialsLoginEnabled` + /// in shared/src/utils.ts + isCredentialsLoginEnabled Boolean @default(true) + + /// @deprecated This property can be controlled by the environment + /// variable `AUTH_EMAIL_CODE_LOGIN_ENABLED`. To ensure that we use + /// the correct setting, use the helper function `isEmailCodeLoginEnabled` + /// in shared/src/utils.ts + isEmailCodeLoginEnabled Boolean @default(false) + + /// @deprecated This property can be overriden by the environment + /// variable `FORCE_ENABLE_ANONYMOUS_ACCESS`, as well as the org's + /// available entitlements. Use the helper function `isAnonymousAccessEnabled` + /// in web/src/lib/entitlements.ts + isAnonymousAccessEnabled Boolean @default(false) + /// List of pending invites to this organization invites Invite[] diff --git a/packages/shared/src/constants.ts b/packages/shared/src/constants.ts index 180b38b68..5bb33d146 100644 --- a/packages/shared/src/constants.ts +++ b/packages/shared/src/constants.ts @@ -1,7 +1,7 @@ import { CodeHostType } from "@sourcebot/db"; import { ConfigSettings, IdentityProviderType } from "./types.js"; -export const SOURCEBOT_SUPPORT_EMAIL = 'team@sourcebot.dev'; +export const SOURCEBOT_SUPPORT_EMAIL = 'support@sourcebot.dev'; /** * @deprecated Use API_KEY_PREFIX instead. diff --git a/packages/shared/src/env.server.ts b/packages/shared/src/env.server.ts index 3401f7a24..b6d49327c 100644 --- a/packages/shared/src/env.server.ts +++ b/packages/shared/src/env.server.ts @@ -173,12 +173,8 @@ const options = { ZOEKT_WEBSERVER_URL: z.string().url().default("http://localhost:6070"), // Auth - FORCE_ENABLE_ANONYMOUS_ACCESS: booleanSchema.default('false'), - REQUIRE_APPROVAL_NEW_MEMBERS: booleanSchema.optional(), AUTH_SECRET: z.string(), AUTH_URL: z.string().url(), - AUTH_CREDENTIALS_LOGIN_ENABLED: booleanSchema.default('true'), - AUTH_EMAIL_CODE_LOGIN_ENABLED: booleanSchema.default('false'), /** * Relative time from now in seconds when to expire the session. @@ -308,20 +304,11 @@ const options = { GOOGLE_VERTEX_REGION: z.string().default('us-central1'), GOOGLE_APPLICATION_CREDENTIALS: z.string().optional(), - /** - * @deprecated Use `thinkingBudget` in the language model config instead. - */ - GOOGLE_VERTEX_THINKING_BUDGET_TOKENS: numberSchema.optional(), - AWS_ACCESS_KEY_ID: z.string().optional(), AWS_SECRET_ACCESS_KEY: z.string().optional(), AWS_SESSION_TOKEN: z.string().optional(), AWS_REGION: z.string().optional(), - /** - * @deprecated Use per-model `temperature` in the language model config instead. - */ - SOURCEBOT_CHAT_MODEL_TEMPERATURE: numberSchema.optional(), SOURCEBOT_CHAT_MAX_STEP_COUNT: numberSchema.default(100), SOURCEBOT_CHAT_PROMPT_CACHING_ENABLED: booleanSchema.default('true'), SOURCEBOT_MCP_TOOL_CALL_TIMEOUT_MS: numberSchema.int().positive().max(maxTimerDelayMs).default(60000), @@ -342,12 +329,6 @@ const options = { return value ?? ((process.env.EXPERIMENT_DISABLE_API_KEY_CREATION_FOR_NON_ADMIN_USERS as 'true' | 'false') ?? 'false'); }), - /** - * @deprecated Use `DISABLE_API_KEY_CREATION_FOR_NON_OWNER_USERS` instead. - */ - EXPERIMENT_DISABLE_API_KEY_CREATION_FOR_NON_ADMIN_USERS: booleanSchema.default('false'), - - // Experimental Environment Variables // @note: These environment variables are subject to change at any time and are not garunteed to be backwards compatible. EXPERIMENT_SELF_SERVE_REPO_INDEXING_ENABLED: booleanSchema.default('false'), @@ -407,11 +388,6 @@ const options = { return value ?? ((process.env.EXPERIMENT_EE_PERMISSION_SYNC_ENABLED as 'true' | 'false') ?? 'false'); }), - /** - * @deprecated Use `PERMISSION_SYNC_ENABLED` instead. - */ - EXPERIMENT_EE_PERMISSION_SYNC_ENABLED: booleanSchema.default('false'), - /** * Configure whether to send telemetry events. * By default, all events are anonymized and do not contain PII data, @@ -425,6 +401,59 @@ const options = { * ignored. */ SOURCEBOT_TELEMETRY_PII_COLLECTION_ENABLED: booleanSchema.default('false'), + + //////////// Deprecated //////////// + /** + * @deprecated Configure this setting via the "Require approval + * for new members" toggle in Settings → Security intsead. + */ + REQUIRE_APPROVAL_NEW_MEMBERS: booleanSchema.optional(), + + /** + * @deprecated Configure email + password login via the "Email & password login" + * toggle in Settings → Security instead. When set, this env var overrides the UI + * setting and locks the toggle; when unset, the DB-backed + * `Org.isCredentialsLoginEnabled` setting is used. + */ + AUTH_CREDENTIALS_LOGIN_ENABLED: booleanSchema.optional(), + + /** + * @deprecated Configure email code login via the UI in Settings → Security + * instead. When set, this env var overrides the UI setting and locks the toggle; + * when unset, the DB-backed `Org.isEmailCodeLoginEnabled` setting is used. Left + * optional (rather than defaulting to 'false') so we can detect whether it was + * explicitly set. + */ + AUTH_EMAIL_CODE_LOGIN_ENABLED: booleanSchema.optional(), + + /** + * @deprecated Configure anonymous access via the UI in Settings → Security + * instead. When set, this env var overrides the UI setting and locks the toggle; + * when unset, the DB-backed `Org.isAnonymousAccessEnabled` setting is used. Left + * optional (rather than defaulting to 'false') so we can detect whether it was + * explicitly set. + */ + FORCE_ENABLE_ANONYMOUS_ACCESS: booleanSchema.optional(), + + /** + * @deprecated Use `PERMISSION_SYNC_ENABLED` instead. + */ + EXPERIMENT_EE_PERMISSION_SYNC_ENABLED: booleanSchema.default('false'), + + /** + * @deprecated Use `thinkingBudget` in the language model config instead. + */ + GOOGLE_VERTEX_THINKING_BUDGET_TOKENS: numberSchema.optional(), + + /** + * @deprecated Use per-model `temperature` in the language model config instead. + */ + SOURCEBOT_CHAT_MODEL_TEMPERATURE: numberSchema.optional(), + + /** + * @deprecated Use `DISABLE_API_KEY_CREATION_FOR_NON_OWNER_USERS` instead. + */ + EXPERIMENT_DISABLE_API_KEY_CREATION_FOR_NON_ADMIN_USERS: booleanSchema.default('false'), }, runtimeEnv, emptyStringAsUndefined: true, diff --git a/packages/shared/src/index.server.ts b/packages/shared/src/index.server.ts index ee5fe9979..0c8f281a4 100644 --- a/packages/shared/src/index.server.ts +++ b/packages/shared/src/index.server.ts @@ -31,6 +31,9 @@ export { getConfigSettings, getRepoPath, getRepoIdFromPath, + isCredentialsLoginEnabled, + isEmailCodeLoginEnabled, + isMemberApprovalRequired, } from "./utils.js"; export * from "./constants.js"; export { diff --git a/packages/shared/src/utils.ts b/packages/shared/src/utils.ts index 562674e84..848b941eb 100644 --- a/packages/shared/src/utils.ts +++ b/packages/shared/src/utils.ts @@ -3,9 +3,10 @@ import stripJsonComments from 'strip-json-comments'; import { z } from "zod"; import { DEFAULT_CONFIG_SETTINGS } from "./constants.js"; import { ConfigSettings } from "./types.js"; -import { Repo } from "@sourcebot/db"; +import { Org, Repo } from "@sourcebot/db"; import path from "path"; import { env, isRemotePath, loadConfig } from "./env.server.js"; +import { isAnonymousAccessAvailable } from './entitlements.js'; // From https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem export const base64Decode = (base64: string): string => { @@ -118,4 +119,28 @@ export const getRepoPath = (repo: Repo): { path: string, isReadOnly: boolean } = path: path.join(reposPath, repo.id.toString()), isReadOnly: false, } +} + +export const isCredentialsLoginEnabled = (org: Org): boolean => { + if (env.AUTH_CREDENTIALS_LOGIN_ENABLED !== undefined) { + return env.AUTH_CREDENTIALS_LOGIN_ENABLED === 'true'; + } + + return org.isCredentialsLoginEnabled; +} + +export const isEmailCodeLoginEnabled = (org: Org): boolean => { + if (env.AUTH_EMAIL_CODE_LOGIN_ENABLED !== undefined) { + return env.AUTH_EMAIL_CODE_LOGIN_ENABLED === 'true'; + } + + return org.isEmailCodeLoginEnabled; +} + +export const isMemberApprovalRequired = (org: Org): boolean => { + if (env.REQUIRE_APPROVAL_NEW_MEMBERS !== undefined) { + return env.REQUIRE_APPROVAL_NEW_MEMBERS === 'true'; + } + + return org.memberApprovalRequired; } \ No newline at end of file diff --git a/packages/web/src/__mocks__/prisma.ts b/packages/web/src/__mocks__/prisma.ts index 6093209b7..5e5c28682 100644 --- a/packages/web/src/__mocks__/prisma.ts +++ b/packages/web/src/__mocks__/prisma.ts @@ -19,6 +19,9 @@ export const MOCK_ORG: Org = { imageUrl: null, metadata: null, memberApprovalRequired: false, + isCredentialsLoginEnabled: true, + isEmailCodeLoginEnabled: false, + isAnonymousAccessEnabled: false, inviteLinkEnabled: false, inviteLinkId: null, } diff --git a/packages/web/src/actions.ts b/packages/web/src/actions.ts index 8fe400fb3..9dfc71a9c 100644 --- a/packages/web/src/actions.ts +++ b/packages/web/src/actions.ts @@ -4,7 +4,6 @@ import { createAudit } from "@/ee/features/audit/audit"; import { env, getSMTPConnectionURL } from "@sourcebot/shared"; import { ErrorCode } from "@/lib/errorCodes"; import { notAuthenticated, notFound, ServiceError } from "@/lib/serviceError"; -import { getOrgMetadata, isHttpError } from "@/lib/utils"; import { __unsafePrisma } from "@/prisma"; import { render } from "@react-email/components"; import { generateApiKey, getTokenFromConfig } from "@sourcebot/shared"; @@ -13,16 +12,13 @@ import { createLogger } from "@sourcebot/shared"; import { GiteaConnectionConfig } from "@sourcebot/schemas/v3/gitea.type"; import { GithubConnectionConfig } from "@sourcebot/schemas/v3/github.type"; import { GitlabConnectionConfig } from "@sourcebot/schemas/v3/gitlab.type"; -import { isAnonymousAccessAvailable } from "@/lib/entitlements"; import { StatusCodes } from "http-status-codes"; import { cookies } from "next/headers"; import { createTransport } from "nodemailer"; -import { Octokit } from "octokit"; import JoinRequestSubmittedEmail from "./emails/joinRequestSubmittedEmail"; -import { AGENTIC_SEARCH_TUTORIAL_DISMISSED_COOKIE_NAME, MOBILE_UNSUPPORTED_SPLASH_SCREEN_DISMISSED_COOKIE_NAME, SINGLE_TENANT_ORG_ID, SOURCEBOT_SUPPORT_EMAIL } from "./lib/constants"; +import { AGENTIC_SEARCH_TUTORIAL_DISMISSED_COOKIE_NAME, MOBILE_UNSUPPORTED_SPLASH_SCREEN_DISMISSED_COOKIE_NAME, SINGLE_TENANT_ORG_ID } from "./lib/constants"; import { RepositoryQuery } from "./lib/types"; import { getAuthenticatedUser, withAuth, withOptionalAuth } from "./middleware/withAuth"; -import { withMinimumOrgRole } from "./middleware/withMinimumOrgRole"; import { getBrowsePath } from "./app/(app)/browse/hooks/utils"; import { sew } from "@/middleware/sew"; @@ -379,142 +375,6 @@ export const getRepoInfoByName = async (repoName: string) => sew(() => } })); -export const experimental_addGithubRepositoryByUrl = async (repositoryUrl: string): Promise<{ connectionId: number } | ServiceError> => sew(() => - withOptionalAuth(async ({ org, prisma }) => { - if (env.EXPERIMENT_SELF_SERVE_REPO_INDEXING_ENABLED !== 'true') { - return { - statusCode: StatusCodes.BAD_REQUEST, - errorCode: ErrorCode.INVALID_REQUEST_BODY, - message: "This feature is not enabled.", - } satisfies ServiceError; - } - - // Parse repository URL to extract owner/repo - const repoInfo = (() => { - const url = repositoryUrl.trim(); - - // Handle various GitHub URL formats - const patterns = [ - // https://github.com/owner/repo or https://github.com/owner/repo.git - /^https?:\/\/github\.com\/([a-zA-Z0-9_.-]+)\/([a-zA-Z0-9_.-]+?)(?:\.git)?\/?$/, - // github.com/owner/repo - /^github\.com\/([a-zA-Z0-9_.-]+)\/([a-zA-Z0-9_.-]+?)(?:\.git)?\/?$/, - // owner/repo - /^([a-zA-Z0-9_.-]+)\/([a-zA-Z0-9_.-]+)$/ - ]; - - for (const pattern of patterns) { - const match = url.match(pattern); - if (match) { - return { - owner: match[1], - repo: match[2] - }; - } - } - - return null; - })(); - - if (!repoInfo) { - return { - statusCode: StatusCodes.BAD_REQUEST, - errorCode: ErrorCode.INVALID_REQUEST_BODY, - message: "Invalid repository URL format. Please use 'owner/repo' or 'https://github.com/owner/repo' format.", - } satisfies ServiceError; - } - - const { owner, repo } = repoInfo; - - // Use GitHub API to fetch repository information and get the external_id - const octokit = new Octokit({ - auth: env.EXPERIMENT_SELF_SERVE_REPO_INDEXING_GITHUB_TOKEN - }); - - let githubRepo; - try { - const response = await octokit.rest.repos.get({ - owner, - repo, - }); - githubRepo = response.data; - } catch (error) { - if (isHttpError(error, 404)) { - return { - statusCode: StatusCodes.NOT_FOUND, - errorCode: ErrorCode.INVALID_REQUEST_BODY, - message: `Repository '${owner}/${repo}' not found or is private. Only public repositories can be added.`, - } satisfies ServiceError; - } - - if (isHttpError(error, 403)) { - return { - statusCode: StatusCodes.FORBIDDEN, - errorCode: ErrorCode.INVALID_REQUEST_BODY, - message: `Access to repository '${owner}/${repo}' is forbidden. Only public repositories can be added.`, - } satisfies ServiceError; - } - - return { - statusCode: StatusCodes.INTERNAL_SERVER_ERROR, - errorCode: ErrorCode.INVALID_REQUEST_BODY, - message: `Failed to fetch repository information: ${error instanceof Error ? error.message : 'Unknown error'}`, - } satisfies ServiceError; - } - - if (githubRepo.private) { - return { - statusCode: StatusCodes.BAD_REQUEST, - errorCode: ErrorCode.INVALID_REQUEST_BODY, - message: "Only public repositories can be added.", - } satisfies ServiceError; - } - - // Check if this repository is already connected using the external_id - const existingRepo = await prisma.repo.findFirst({ - where: { - orgId: org.id, - external_id: githubRepo.id.toString(), - external_codeHostType: 'github', - external_codeHostUrl: 'https://github.com', - } - }); - - if (existingRepo) { - return { - statusCode: StatusCodes.BAD_REQUEST, - errorCode: ErrorCode.CONNECTION_ALREADY_EXISTS, - message: "This repository already exists.", - } satisfies ServiceError; - } - - const connectionName = `${owner}-${repo}-${Date.now()}`; - - // Create GitHub connection config - const connectionConfig: GithubConnectionConfig = { - type: "github" as const, - repos: [`${owner}/${repo}`], - ...(env.EXPERIMENT_SELF_SERVE_REPO_INDEXING_GITHUB_TOKEN ? { - token: { - env: 'EXPERIMENT_SELF_SERVE_REPO_INDEXING_GITHUB_TOKEN' - } - } : {}) - }; - - const connection = await prisma.connection.create({ - data: { - orgId: org.id, - name: connectionName, - config: connectionConfig as unknown as Prisma.InputJsonValue, - connectionType: 'github', - } - }); - - return { - connectionId: connection.id, - } - })); - // eslint-disable-next-line authz/require-auth-wrapper -- calls getAuthenticatedUser() directly; runs pre-org-membership so cannot use withAuth export const createAccountRequest = async () => sew(async () => { const authResult = await getAuthenticatedUser(); @@ -619,36 +479,6 @@ export const createAccountRequest = async () => sew(async () => { } }); -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 }, - }); - - return { - success: true, - }; - }) - ) -); - -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 }, - }); - - return { - success: true, - }; - }) - ) -); - export const getSearchContexts = async () => sew(() => withOptionalAuth(async ({ org, prisma }) => { const searchContexts = await prisma.searchContext.findMany({ @@ -737,39 +567,6 @@ export const getRepoImage = async (repoId: number): PromiseConfigure how users can access your Sourcebot deployment.{" "} - - Learn more - -
-{name}
-{description}
+{name}
+ {description && ( +{description}
+ )}{name}
+ {provider.issuerUrl && ( +{provider.issuerUrl}
+ )} +Single sign-on is a paid feature
+Upgrade to let users authenticate with providers like GitHub, Google, and Okta.
+Organization access
+Configure how users can access your Sourcebot deployment.{" "} + + Learn more + +
+Email login
+ +Single Sign-On
+ {!hasSSOEntitlement &&Let users sign in with an external identity provider such as GitHub, Google, or Okta. Providers are managed in your config file.{" "} + + Learn more + +
+- When enabled, users can access your deployment without logging in. -
- {showPlanMessage && ( -- - - Your current plan doesn't allow for anonymous access. Please{" "} - - reach out - - {" "}for assistance. - -
-
-
-
- FORCE_ENABLE_ANONYMOUS_ACCESS is set, so this cannot be changed from the UI.
-
-
- When enabled, team members can use the invite link to join your organization without requiring approval. -
-- You can find this link again in the Settings → Members page. -
-- When enabled, new users will need approval from an organization owner before they can access your deployment. -
- {isControlledByEnvVar && ( -
-
-
- This setting is controlled by the REQUIRE_APPROVAL_NEW_MEMBERS environment variable.
-
-
+ Having trouble?{" "} + + Contact support + +
+- Having trouble?{" "} - - Contact support - -
-Login verification failed
-- Something went wrong when trying to verify your login. Please try again. -
-+ Having trouble?{" "} + + Contact support + +
+