mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-12 12:25:22 +00:00
[member approval] configurable via environment
This commit is contained in:
parent
66c9ec044e
commit
e401810235
8 changed files with 69 additions and 3 deletions
|
|
@ -23,6 +23,9 @@ AUTH_URL="http://localhost:3000"
|
||||||
# AUTH_EE_GOOGLE_CLIENT_ID=""
|
# AUTH_EE_GOOGLE_CLIENT_ID=""
|
||||||
# AUTH_EE_GOOGLE_CLIENT_SECRET=""
|
# 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)
|
DATA_CACHE_DIR=${PWD}/.sourcebot # Path to the sourcebot cache dir (ex. ~/sourcebot/.sourcebot)
|
||||||
SOURCEBOT_PUBLIC_KEY_PATH=${PWD}/public.pem
|
SOURCEBOT_PUBLIC_KEY_PATH=${PWD}/public.pem
|
||||||
# CONFIG_PATH=${PWD}/config.json # Path to the sourcebot config file (if one exists)
|
# CONFIG_PATH=${PWD}/config.json # Path to the sourcebot config file (if one exists)
|
||||||
|
|
|
||||||
|
|
@ -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).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
- Enable configuration of member approval via Env Var. [#542](https://github.com/sourcebot-dev/sourcebot/pull/542)
|
||||||
|
|
||||||
## [4.7.2] - 2025-09-22
|
## [4.7.2] - 2025-09-22
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,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> |
|
| `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> |
|
||||||
| `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> |
|
| `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>
|
| `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>
|
||||||
|
| `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>
|
||||||
| `REDIS_DATA_DIR` | `$DATA_CACHE_DIR/redis` | <p>The data directory for the default Redis instance.</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_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> |
|
| `REDIS_REMOVE_ON_COMPLETE` | `0` | <p>Controls how many completed jobs are allowed to remain in Redis queues</p> |
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,10 @@ import { useToast } from "@/components/hooks/use-toast"
|
||||||
interface MemberApprovalRequiredToggleProps {
|
interface MemberApprovalRequiredToggleProps {
|
||||||
memberApprovalRequired: boolean
|
memberApprovalRequired: boolean
|
||||||
onToggleChange?: (checked: boolean) => void
|
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 [enabled, setEnabled] = useState(memberApprovalRequired)
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const { toast } = useToast()
|
const { toast } = useToast()
|
||||||
|
|
@ -45,6 +46,9 @@ export function MemberApprovalRequiredToggle({ memberApprovalRequired, onToggleC
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isDisabled = isLoading || forceMemberApprovalRequired !== undefined;
|
||||||
|
const showForceMessage = forceMemberApprovalRequired !== undefined;
|
||||||
|
|
||||||
return (
|
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)]">
|
||||||
<div className="flex items-start justify-between gap-4">
|
<div className="flex items-start justify-between gap-4">
|
||||||
|
|
@ -56,13 +60,35 @@ export function MemberApprovalRequiredToggle({ memberApprovalRequired, onToggleC
|
||||||
<p className="text-sm text-[var(--muted-foreground)] leading-relaxed">
|
<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.
|
When enabled, new users will need approval from an organization owner before they can access your deployment.
|
||||||
</p>
|
</p>
|
||||||
|
{showForceMessage && (
|
||||||
|
<div className="mt-3">
|
||||||
|
<p className="flex items-start gap-2 text-sm text-[var(--muted-foreground)] p-3 rounded-md bg-[var(--muted)] border border-[var(--border)]">
|
||||||
|
<svg
|
||||||
|
className="w-4 h-4 mt-0.5 flex-shrink-0"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
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.
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<Switch
|
<Switch
|
||||||
checked={enabled}
|
checked={enabled}
|
||||||
onCheckedChange={handleToggle}
|
onCheckedChange={handleToggle}
|
||||||
disabled={isLoading}
|
disabled={isDisabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ export async function OrganizationAccessSettings() {
|
||||||
const hasAnonymousAccessEntitlement = hasEntitlement("anonymous-access");
|
const hasAnonymousAccessEntitlement = hasEntitlement("anonymous-access");
|
||||||
|
|
||||||
const forceEnableAnonymousAccess = env.FORCE_ENABLE_ANONYMOUS_ACCESS === 'true';
|
const forceEnableAnonymousAccess = env.FORCE_ENABLE_ANONYMOUS_ACCESS === 'true';
|
||||||
|
const forceMemberApprovalRequired = env.FORCE_MEMBER_APPROVAL_REQUIRED;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
|
@ -37,6 +38,7 @@ export async function OrganizationAccessSettings() {
|
||||||
memberApprovalRequired={org.memberApprovalRequired}
|
memberApprovalRequired={org.memberApprovalRequired}
|
||||||
inviteLinkEnabled={org.inviteLinkEnabled}
|
inviteLinkEnabled={org.inviteLinkEnabled}
|
||||||
inviteLink={inviteLink}
|
inviteLink={inviteLink}
|
||||||
|
forceMemberApprovalRequired={forceMemberApprovalRequired}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,14 @@ interface OrganizationAccessSettingsWrapperProps {
|
||||||
memberApprovalRequired: boolean
|
memberApprovalRequired: boolean
|
||||||
inviteLinkEnabled: boolean
|
inviteLinkEnabled: boolean
|
||||||
inviteLink: string | null
|
inviteLink: string | null
|
||||||
|
forceMemberApprovalRequired?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function OrganizationAccessSettingsWrapper({
|
export function OrganizationAccessSettingsWrapper({
|
||||||
memberApprovalRequired,
|
memberApprovalRequired,
|
||||||
inviteLinkEnabled,
|
inviteLinkEnabled,
|
||||||
inviteLink
|
inviteLink,
|
||||||
|
forceMemberApprovalRequired
|
||||||
}: OrganizationAccessSettingsWrapperProps) {
|
}: OrganizationAccessSettingsWrapperProps) {
|
||||||
const [showInviteLink, setShowInviteLink] = useState(memberApprovalRequired)
|
const [showInviteLink, setShowInviteLink] = useState(memberApprovalRequired)
|
||||||
|
|
||||||
|
|
@ -27,6 +29,7 @@ export function OrganizationAccessSettingsWrapper({
|
||||||
<MemberApprovalRequiredToggle
|
<MemberApprovalRequiredToggle
|
||||||
memberApprovalRequired={memberApprovalRequired}
|
memberApprovalRequired={memberApprovalRequired}
|
||||||
onToggleChange={handleMemberApprovalToggle}
|
onToggleChange={handleMemberApprovalToggle}
|
||||||
|
forceMemberApprovalRequired={forceMemberApprovalRequired}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ export const env = createEnv({
|
||||||
|
|
||||||
// Auth
|
// Auth
|
||||||
FORCE_ENABLE_ANONYMOUS_ACCESS: booleanSchema.default('false'),
|
FORCE_ENABLE_ANONYMOUS_ACCESS: booleanSchema.default('false'),
|
||||||
|
FORCE_MEMBER_APPROVAL_REQUIRED: booleanSchema.optional(),
|
||||||
|
|
||||||
AUTH_SECRET: z.string(),
|
AUTH_SECRET: z.string(),
|
||||||
AUTH_URL: z.string().url(),
|
AUTH_URL: z.string().url(),
|
||||||
|
|
|
||||||
|
|
@ -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 syncConnections(config.connections);
|
||||||
await syncSearchContexts({
|
await syncSearchContexts({
|
||||||
contexts: config.contexts,
|
contexts: config.contexts,
|
||||||
|
|
@ -180,6 +193,9 @@ const initSingleTenancy = async () => {
|
||||||
name: SINGLE_TENANT_ORG_NAME,
|
name: SINGLE_TENANT_ORG_NAME,
|
||||||
domain: SINGLE_TENANT_ORG_DOMAIN,
|
domain: SINGLE_TENANT_ORG_DOMAIN,
|
||||||
inviteLinkId: crypto.randomUUID(),
|
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) {
|
} 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.
|
// Load any connections defined declaratively in the config file.
|
||||||
const configPath = env.CONFIG_PATH;
|
const configPath = env.CONFIG_PATH;
|
||||||
if (configPath) {
|
if (configPath) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue