Skip to content

Commit e401810

Browse files
committed
[member approval] configurable via environment
1 parent 66c9ec0 commit e401810

File tree

8 files changed

+69
-3
lines changed

8 files changed

+69
-3
lines changed

.env.development

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ AUTH_URL="http://localhost:3000"
2323
# AUTH_EE_GOOGLE_CLIENT_ID=""
2424
# AUTH_EE_GOOGLE_CLIENT_SECRET=""
2525

26+
# FORCE_ENABLE_ANONYMOUS_ACCESS="false"
27+
# FORCE_MEMBER_APPROVAL_REQUIRED="false"
28+
2629
DATA_CACHE_DIR=${PWD}/.sourcebot # Path to the sourcebot cache dir (ex. ~/sourcebot/.sourcebot)
2730
SOURCEBOT_PUBLIC_KEY_PATH=${PWD}/public.pem
2831
# CONFIG_PATH=${PWD}/config.json # Path to the sourcebot config file (if one exists)

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

88
## [Unreleased]
9+
- Enable configuration of member approval via Env Var. [#542](https://github.com/sourcebot-dev/sourcebot/pull/542)
910

1011
## [4.7.2] - 2025-09-22
1112

docs/docs/configuration/environment-variables.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ The following environment variables allow you to configure your Sourcebot deploy
2222
| `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> |
2323
| `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> |
2424
| `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>
25+
| `FORCE_MEMBER_APPROVAL_REQUIRED` | `-` | <p>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</p>
2526
| `REDIS_DATA_DIR` | `$DATA_CACHE_DIR/redis` | <p>The data directory for the default Redis instance.</p> |
2627
| `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> |
2728
| `REDIS_REMOVE_ON_COMPLETE` | `0` | <p>Controls how many completed jobs are allowed to remain in Redis queues</p> |

packages/web/src/app/components/memberApprovalRequiredToggle.tsx

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ import { useToast } from "@/components/hooks/use-toast"
1010
interface MemberApprovalRequiredToggleProps {
1111
memberApprovalRequired: boolean
1212
onToggleChange?: (checked: boolean) => void
13+
forceMemberApprovalRequired?: string
1314
}
1415

15-
export function MemberApprovalRequiredToggle({ memberApprovalRequired, onToggleChange }: MemberApprovalRequiredToggleProps) {
16+
export function MemberApprovalRequiredToggle({ memberApprovalRequired, onToggleChange, forceMemberApprovalRequired }: MemberApprovalRequiredToggleProps) {
1617
const [enabled, setEnabled] = useState(memberApprovalRequired)
1718
const [isLoading, setIsLoading] = useState(false)
1819
const { toast } = useToast()
@@ -45,6 +46,9 @@ export function MemberApprovalRequiredToggle({ memberApprovalRequired, onToggleC
4546
}
4647
}
4748

49+
const isDisabled = isLoading || forceMemberApprovalRequired !== undefined;
50+
const showForceMessage = forceMemberApprovalRequired !== undefined;
51+
4852
return (
4953
<div className="p-4 rounded-lg border border-[var(--border)] bg-[var(--card)]">
5054
<div className="flex items-start justify-between gap-4">
@@ -56,13 +60,35 @@ export function MemberApprovalRequiredToggle({ memberApprovalRequired, onToggleC
5660
<p className="text-sm text-[var(--muted-foreground)] leading-relaxed">
5761
When enabled, new users will need approval from an organization owner before they can access your deployment.
5862
</p>
63+
{showForceMessage && (
64+
<div className="mt-3">
65+
<p className="flex items-start gap-2 text-sm text-[var(--muted-foreground)] p-3 rounded-md bg-[var(--muted)] border border-[var(--border)]">
66+
<svg
67+
className="w-4 h-4 mt-0.5 flex-shrink-0"
68+
fill="none"
69+
stroke="currentColor"
70+
viewBox="0 0 24 24"
71+
>
72+
<path
73+
strokeLinecap="round"
74+
strokeLinejoin="round"
75+
strokeWidth={2}
76+
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
77+
/>
78+
</svg>
79+
<span>
80+
The <code className="bg-[var(--secondary)] px-1 py-0.5 rounded text-xs font-mono">FORCE_MEMBER_APPROVAL_REQUIRED</code> environment variable is set, so this cannot be changed from the UI.
81+
</span>
82+
</p>
83+
</div>
84+
)}
5985
</div>
6086
</div>
6187
<div className="flex-shrink-0">
6288
<Switch
6389
checked={enabled}
6490
onCheckedChange={handleToggle}
65-
disabled={isLoading}
91+
disabled={isDisabled}
6692
/>
6793
</div>
6894
</div>

packages/web/src/app/components/organizationAccessSettings.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export async function OrganizationAccessSettings() {
2424
const hasAnonymousAccessEntitlement = hasEntitlement("anonymous-access");
2525

2626
const forceEnableAnonymousAccess = env.FORCE_ENABLE_ANONYMOUS_ACCESS === 'true';
27+
const forceMemberApprovalRequired = env.FORCE_MEMBER_APPROVAL_REQUIRED;
2728

2829
return (
2930
<div className="space-y-6">
@@ -37,6 +38,7 @@ export async function OrganizationAccessSettings() {
3738
memberApprovalRequired={org.memberApprovalRequired}
3839
inviteLinkEnabled={org.inviteLinkEnabled}
3940
inviteLink={inviteLink}
41+
forceMemberApprovalRequired={forceMemberApprovalRequired}
4042
/>
4143
</div>
4244
)

packages/web/src/app/components/organizationAccessSettingsWrapper.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@ interface OrganizationAccessSettingsWrapperProps {
88
memberApprovalRequired: boolean
99
inviteLinkEnabled: boolean
1010
inviteLink: string | null
11+
forceMemberApprovalRequired?: string
1112
}
1213

1314
export function OrganizationAccessSettingsWrapper({
1415
memberApprovalRequired,
1516
inviteLinkEnabled,
16-
inviteLink
17+
inviteLink,
18+
forceMemberApprovalRequired
1719
}: OrganizationAccessSettingsWrapperProps) {
1820
const [showInviteLink, setShowInviteLink] = useState(memberApprovalRequired)
1921

@@ -27,6 +29,7 @@ export function OrganizationAccessSettingsWrapper({
2729
<MemberApprovalRequiredToggle
2830
memberApprovalRequired={memberApprovalRequired}
2931
onToggleChange={handleMemberApprovalToggle}
32+
forceMemberApprovalRequired={forceMemberApprovalRequired}
3033
/>
3134
</div>
3235

packages/web/src/env.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export const env = createEnv({
2121

2222
// Auth
2323
FORCE_ENABLE_ANONYMOUS_ACCESS: booleanSchema.default('false'),
24+
FORCE_MEMBER_APPROVAL_REQUIRED: booleanSchema.optional(),
2425

2526
AUTH_SECRET: z.string(),
2627
AUTH_URL: z.string().url(),

packages/web/src/initialize.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,19 @@ const syncDeclarativeConfig = async (configPath: string) => {
131131
}
132132
}
133133

134+
// Apply FORCE_MEMBER_APPROVAL_REQUIRED environment variable setting
135+
if (env.FORCE_MEMBER_APPROVAL_REQUIRED !== undefined) {
136+
const forceMemberApprovalRequired = env.FORCE_MEMBER_APPROVAL_REQUIRED === 'true';
137+
const org = await getOrgFromDomain(SINGLE_TENANT_ORG_DOMAIN);
138+
if (org) {
139+
await prisma.org.update({
140+
where: { id: org.id },
141+
data: { memberApprovalRequired: forceMemberApprovalRequired },
142+
});
143+
logger.info(`Member approval required set to ${forceMemberApprovalRequired} via FORCE_MEMBER_APPROVAL_REQUIRED environment variable`);
144+
}
145+
}
146+
134147
await syncConnections(config.connections);
135148
await syncSearchContexts({
136149
contexts: config.contexts,
@@ -180,6 +193,9 @@ const initSingleTenancy = async () => {
180193
name: SINGLE_TENANT_ORG_NAME,
181194
domain: SINGLE_TENANT_ORG_DOMAIN,
182195
inviteLinkId: crypto.randomUUID(),
196+
memberApprovalRequired: env.FORCE_MEMBER_APPROVAL_REQUIRED === 'true' ? true :
197+
env.FORCE_MEMBER_APPROVAL_REQUIRED === 'false' ? false :
198+
true, // default to true if FORCE_MEMBER_APPROVAL_REQUIRED is not set
183199
}
184200
});
185201
} else if (!org.inviteLinkId) {
@@ -220,6 +236,19 @@ const initSingleTenancy = async () => {
220236
}
221237
}
222238

239+
// Apply FORCE_MEMBER_APPROVAL_REQUIRED environment variable setting
240+
if (env.FORCE_MEMBER_APPROVAL_REQUIRED !== undefined) {
241+
const forceMemberApprovalRequired = env.FORCE_MEMBER_APPROVAL_REQUIRED === 'true';
242+
const org = await getOrgFromDomain(SINGLE_TENANT_ORG_DOMAIN);
243+
if (org) {
244+
await prisma.org.update({
245+
where: { id: org.id },
246+
data: { memberApprovalRequired: forceMemberApprovalRequired },
247+
});
248+
logger.info(`Member approval required set to ${forceMemberApprovalRequired} via FORCE_MEMBER_APPROVAL_REQUIRED environment variable`);
249+
}
250+
}
251+
223252
// Load any connections defined declaratively in the config file.
224253
const configPath = env.CONFIG_PATH;
225254
if (configPath) {

0 commit comments

Comments
 (0)