Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
- Added support to set "Require approval for new members" via config with (`REQUIRE_APPROVAL_NEW_MEMBERS`). [#858](https://github.com/sourcebot-dev/sourcebot/pull/858)
Comment thread
brendan-kellam marked this conversation as resolved.
Outdated

## [4.10.27] - 2026-02-05

Expand Down
2 changes: 1 addition & 1 deletion docs/docs/configuration/auth/access-settings.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ By default, Sourcebot requires new members to be approved by the owner of the de
to configure this behavior.

### Configuration
Member approval can be configured by the owner of the deployment by navigating to **Settings -> Members**:
Member approval can be configured by the 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 Toggle](/images/member_approval_toggle.png)

Expand Down
1 change: 1 addition & 0 deletions docs/docs/configuration/environment-variables.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ The following environment variables allow you to configure your Sourcebot deploy
| `DATABASE_URL` | `postgresql://postgres@ localhost:5432/sourcebot` | <p>Connection string of your Postgres database. By default, a Postgres database is automatically provisioned at startup within the container.</p><p>If you'd like to use a non-default schema, you can provide it as a parameter in the database url.</p><p>You can also use `DATABASE_HOST`, `DATABASE_USERNAME`, `DATABASE_PASSWORD`, `DATABASE_NAME`, and `DATABASE_ARGS` to construct the database url.</p> |
| `EMAIL_FROM_ADDRESS` | `-` | <p>The email address that transactional emails will be sent from. See [this doc](/docs/configuration/transactional-emails) for more info.</p> |
| `FORCE_ENABLE_ANONYMOUS_ACCESS` | `false` | <p>When enabled, [anonymous access](/docs/configuration/auth/access-settings#anonymous-access) to the organization will always be enabled</p>
| `REQUIRE_APPROVAL_NEW_MEMBERS` | - | <p>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.</p>
| `REDIS_DATA_DIR` | `$DATA_CACHE_DIR/redis` | <p>The data directory for the default Redis instance.</p> |
| `REDIS_URL` | `redis://localhost:6379` | <p>Connection string of your Redis instance. By default, a Redis database is automatically provisioned at startup within the container.</p> |
| `REDIS_REMOVE_ON_COMPLETE` | `0` | <p>Controls how many completed jobs are allowed to remain in Redis queues</p> |
Expand Down
1 change: 1 addition & 0 deletions packages/shared/src/env.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ export const env = createEnv({

// 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'),
Expand Down
31 changes: 28 additions & 3 deletions packages/web/src/app/components/memberApprovalRequiredToggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import { useToast } from "@/components/hooks/use-toast"
interface MemberApprovalRequiredToggleProps {
memberApprovalRequired: boolean
onToggleChange?: (checked: boolean) => void
isControlledByEnvVar: boolean
}

export function MemberApprovalRequiredToggle({ memberApprovalRequired, onToggleChange }: MemberApprovalRequiredToggleProps) {
export function MemberApprovalRequiredToggle({ memberApprovalRequired, onToggleChange, isControlledByEnvVar }: MemberApprovalRequiredToggleProps) {
const [enabled, setEnabled] = useState(memberApprovalRequired)
const [isLoading, setIsLoading] = useState(false)
const { toast } = useToast()
Expand Down Expand Up @@ -45,8 +46,10 @@ export function MemberApprovalRequiredToggle({ memberApprovalRequired, onToggleC
}
}

const isDisabled = isLoading || isControlledByEnvVar;

return (
<div className="p-4 rounded-lg border border-[var(--border)] bg-[var(--card)]">
<div className={`p-4 rounded-lg border border-[var(--border)] bg-[var(--card)] ${isControlledByEnvVar ? 'opacity-60' : ''}`}>
<div className="flex items-start justify-between gap-4">
<div className="flex-1 min-w-0">
<h3 className="font-medium text-[var(--foreground)] mb-2">
Expand All @@ -56,13 +59,35 @@ export function MemberApprovalRequiredToggle({ memberApprovalRequired, onToggleC
<p className="text-sm text-[var(--muted-foreground)] leading-relaxed">
When enabled, new users will need approval from an organization owner before they can access your deployment.
</p>
{isControlledByEnvVar && (
<div className="mt-3 p-3 rounded-md bg-[var(--muted)] border border-[var(--border)]">
<p className="text-sm text-[var(--foreground)] leading-relaxed flex items-center gap-2">
<svg
className="w-4 h-4 flex-shrink-0 text-[var(--muted-foreground)]"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span>
This setting is controlled by the <code className="bg-[var(--secondary)] px-1 py-0.5 rounded text-xs font-mono">REQUIRE_APPROVAL_NEW_MEMBERS</code> environment variable.
</span>
</p>
</div>
)}
</div>
</div>
<div className="flex-shrink-0">
<Switch
checked={enabled}
onCheckedChange={handleToggle}
disabled={isLoading}
disabled={isDisabled}
/>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export async function OrganizationAccessSettings() {
const hasAnonymousAccessEntitlement = hasEntitlement("anonymous-access");

const forceEnableAnonymousAccess = env.FORCE_ENABLE_ANONYMOUS_ACCESS === 'true';
const memberApprovalEnvVarSet = env.REQUIRE_APPROVAL_NEW_MEMBERS !== undefined;

return (
<div className="space-y-6">
Expand All @@ -34,6 +35,7 @@ export async function OrganizationAccessSettings() {
memberApprovalRequired={org.memberApprovalRequired}
inviteLinkEnabled={org.inviteLinkEnabled}
inviteLink={inviteLink}
memberApprovalEnvVarSet={memberApprovalEnvVarSet}
/>
</div>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ interface OrganizationAccessSettingsWrapperProps {
memberApprovalRequired: boolean
inviteLinkEnabled: boolean
inviteLink: string | null
memberApprovalEnvVarSet: boolean
}

export function OrganizationAccessSettingsWrapper({
memberApprovalRequired,
inviteLinkEnabled,
inviteLink
inviteLink,
memberApprovalEnvVarSet
}: OrganizationAccessSettingsWrapperProps) {
const [showInviteLink, setShowInviteLink] = useState(memberApprovalRequired)

Expand All @@ -27,6 +29,7 @@ export function OrganizationAccessSettingsWrapper({
<MemberApprovalRequiredToggle
memberApprovalRequired={memberApprovalRequired}
onToggleChange={handleMemberApprovalToggle}
isControlledByEnvVar={memberApprovalEnvVarSet}
/>
</div>

Expand Down
13 changes: 13 additions & 0 deletions packages/web/src/initialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,19 @@ const initSingleTenancy = async () => {
}
}
}

// Sync member approval setting from env var (only if explicitly set)
if (env.REQUIRE_APPROVAL_NEW_MEMBERS !== undefined) {
const requireApprovalNewMembers = env.REQUIRE_APPROVAL_NEW_MEMBERS === 'true';
const org = await getOrgFromDomain(SINGLE_TENANT_ORG_DOMAIN);
if (org && org.memberApprovalRequired !== requireApprovalNewMembers) {
await prisma.org.update({
where: { id: org.id },
data: { memberApprovalRequired: requireApprovalNewMembers },
});
logger.info(`Member approval requirement set to ${requireApprovalNewMembers} via REQUIRE_APPROVAL_NEW_MEMBERS environment variable`);
}
}
}

const initMultiTenancy = async () => {
Expand Down
Loading