diff --git a/.env.development b/.env.development index 0309b5fb..334e6d30 100644 --- a/.env.development +++ b/.env.development @@ -23,6 +23,9 @@ AUTH_URL="http://localhost:3000" # AUTH_EE_GOOGLE_CLIENT_ID="" # AUTH_EE_GOOGLE_CLIENT_SECRET="" +# FORCE_ENABLE_ANONYMOUS_ACCESS="false" +# FORCE_MEMBER_APPROVAL_REQUIRED="false" + DATA_CACHE_DIR=${PWD}/.sourcebot # Path to the sourcebot cache dir (ex. ~/sourcebot/.sourcebot) SOURCEBOT_PUBLIC_KEY_PATH=${PWD}/public.pem # CONFIG_PATH=${PWD}/config.json # Path to the sourcebot config file (if one exists) diff --git a/CHANGELOG.md b/CHANGELOG.md index c54d1c34..4b6535db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +- Enable configuration of member approval via Env Var. [#542](https://github.com/sourcebot-dev/sourcebot/pull/542) ## [4.7.2] - 2025-09-22 diff --git a/docs/docs/configuration/environment-variables.mdx b/docs/docs/configuration/environment-variables.mdx index 70da72d8..e6ab5e8a 100644 --- a/docs/docs/configuration/environment-variables.mdx +++ b/docs/docs/configuration/environment-variables.mdx @@ -22,6 +22,7 @@ The following environment variables allow you to configure your Sourcebot deploy | `DATABASE_URL` | `postgresql://postgres@ localhost:5432/sourcebot` |

Connection string of your Postgres database. By default, a Postgres database is automatically provisioned at startup within the container.

If you'd like to use a non-default schema, you can provide it as a parameter in 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

+| `FORCE_MEMBER_APPROVAL_REQUIRED` | `-` |

When set to `true` or `false`, forces the member approval requirement setting and disables the UI toggle. When enabled, new users will need approval from an organization owner before they can access your deployment. See [access settings docs](/docs/configuration/auth/access-settings) for more info

| `REDIS_DATA_DIR` | `$DATA_CACHE_DIR/redis` |

The data directory for the default Redis instance.

| | `REDIS_URL` | `redis://localhost:6379` |

Connection string of your Redis instance. By default, a Redis database is automatically provisioned at startup within the container.

| | `REDIS_REMOVE_ON_COMPLETE` | `0` |

Controls how many completed jobs are allowed to remain in Redis queues

| diff --git a/packages/web/src/app/components/memberApprovalRequiredToggle.tsx b/packages/web/src/app/components/memberApprovalRequiredToggle.tsx index 9d3cf1bc..d12ffb26 100644 --- a/packages/web/src/app/components/memberApprovalRequiredToggle.tsx +++ b/packages/web/src/app/components/memberApprovalRequiredToggle.tsx @@ -10,9 +10,10 @@ import { useToast } from "@/components/hooks/use-toast" interface MemberApprovalRequiredToggleProps { memberApprovalRequired: boolean onToggleChange?: (checked: boolean) => void + forceMemberApprovalRequired?: string } -export function MemberApprovalRequiredToggle({ memberApprovalRequired, onToggleChange }: MemberApprovalRequiredToggleProps) { +export function MemberApprovalRequiredToggle({ memberApprovalRequired, onToggleChange, forceMemberApprovalRequired }: MemberApprovalRequiredToggleProps) { const [enabled, setEnabled] = useState(memberApprovalRequired) const [isLoading, setIsLoading] = useState(false) const { toast } = useToast() @@ -45,6 +46,9 @@ export function MemberApprovalRequiredToggle({ memberApprovalRequired, onToggleC } } + const isDisabled = isLoading || forceMemberApprovalRequired !== undefined; + const showForceMessage = forceMemberApprovalRequired !== undefined; + return (
@@ -56,13 +60,35 @@ export function MemberApprovalRequiredToggle({ memberApprovalRequired, onToggleC

When enabled, new users will need approval from an organization owner before they can access your deployment.

+ {showForceMessage && ( +
+

+ + + + + The FORCE_MEMBER_APPROVAL_REQUIRED environment variable is set, so this cannot be changed from the UI. + +

+
+ )}
diff --git a/packages/web/src/app/components/organizationAccessSettings.tsx b/packages/web/src/app/components/organizationAccessSettings.tsx index e240a374..a318607d 100644 --- a/packages/web/src/app/components/organizationAccessSettings.tsx +++ b/packages/web/src/app/components/organizationAccessSettings.tsx @@ -24,6 +24,7 @@ export async function OrganizationAccessSettings() { const hasAnonymousAccessEntitlement = hasEntitlement("anonymous-access"); const forceEnableAnonymousAccess = env.FORCE_ENABLE_ANONYMOUS_ACCESS === 'true'; + const forceMemberApprovalRequired = env.FORCE_MEMBER_APPROVAL_REQUIRED; return (
@@ -37,6 +38,7 @@ export async function OrganizationAccessSettings() { memberApprovalRequired={org.memberApprovalRequired} inviteLinkEnabled={org.inviteLinkEnabled} inviteLink={inviteLink} + forceMemberApprovalRequired={forceMemberApprovalRequired} />
) diff --git a/packages/web/src/app/components/organizationAccessSettingsWrapper.tsx b/packages/web/src/app/components/organizationAccessSettingsWrapper.tsx index 19fa0a09..e7f74455 100644 --- a/packages/web/src/app/components/organizationAccessSettingsWrapper.tsx +++ b/packages/web/src/app/components/organizationAccessSettingsWrapper.tsx @@ -8,12 +8,14 @@ interface OrganizationAccessSettingsWrapperProps { memberApprovalRequired: boolean inviteLinkEnabled: boolean inviteLink: string | null + forceMemberApprovalRequired?: string } export function OrganizationAccessSettingsWrapper({ memberApprovalRequired, inviteLinkEnabled, - inviteLink + inviteLink, + forceMemberApprovalRequired }: OrganizationAccessSettingsWrapperProps) { const [showInviteLink, setShowInviteLink] = useState(memberApprovalRequired) @@ -27,6 +29,7 @@ export function OrganizationAccessSettingsWrapper({ diff --git a/packages/web/src/env.mjs b/packages/web/src/env.mjs index 922b2b84..d1bf6660 100644 --- a/packages/web/src/env.mjs +++ b/packages/web/src/env.mjs @@ -21,6 +21,7 @@ export const env = createEnv({ // Auth FORCE_ENABLE_ANONYMOUS_ACCESS: booleanSchema.default('false'), + FORCE_MEMBER_APPROVAL_REQUIRED: booleanSchema.optional(), AUTH_SECRET: z.string(), AUTH_URL: z.string().url(), diff --git a/packages/web/src/initialize.ts b/packages/web/src/initialize.ts index 4c7b0355..2a902178 100644 --- a/packages/web/src/initialize.ts +++ b/packages/web/src/initialize.ts @@ -131,6 +131,19 @@ const syncDeclarativeConfig = async (configPath: string) => { } } + // Apply FORCE_MEMBER_APPROVAL_REQUIRED environment variable setting + if (env.FORCE_MEMBER_APPROVAL_REQUIRED !== undefined) { + const forceMemberApprovalRequired = env.FORCE_MEMBER_APPROVAL_REQUIRED === 'true'; + const org = await getOrgFromDomain(SINGLE_TENANT_ORG_DOMAIN); + if (org) { + await prisma.org.update({ + where: { id: org.id }, + data: { memberApprovalRequired: forceMemberApprovalRequired }, + }); + logger.info(`Member approval required set to ${forceMemberApprovalRequired} via FORCE_MEMBER_APPROVAL_REQUIRED environment variable`); + } + } + await syncConnections(config.connections); await syncSearchContexts({ contexts: config.contexts, @@ -180,6 +193,9 @@ const initSingleTenancy = async () => { name: SINGLE_TENANT_ORG_NAME, domain: SINGLE_TENANT_ORG_DOMAIN, inviteLinkId: crypto.randomUUID(), + memberApprovalRequired: env.FORCE_MEMBER_APPROVAL_REQUIRED === 'true' ? true : + env.FORCE_MEMBER_APPROVAL_REQUIRED === 'false' ? false : + true, // default to true if FORCE_MEMBER_APPROVAL_REQUIRED is not set } }); } else if (!org.inviteLinkId) { @@ -220,6 +236,19 @@ const initSingleTenancy = async () => { } } + // Apply FORCE_MEMBER_APPROVAL_REQUIRED environment variable setting + if (env.FORCE_MEMBER_APPROVAL_REQUIRED !== undefined) { + const forceMemberApprovalRequired = env.FORCE_MEMBER_APPROVAL_REQUIRED === 'true'; + const org = await getOrgFromDomain(SINGLE_TENANT_ORG_DOMAIN); + if (org) { + await prisma.org.update({ + where: { id: org.id }, + data: { memberApprovalRequired: forceMemberApprovalRequired }, + }); + logger.info(`Member approval required set to ${forceMemberApprovalRequired} via FORCE_MEMBER_APPROVAL_REQUIRED environment variable`); + } + } + // Load any connections defined declaratively in the config file. const configPath = env.CONFIG_PATH; if (configPath) {