-
Notifications
You must be signed in to change notification settings - Fork 254
[member approval] configurable via environment #542
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. similar to |
||
| const showForceMessage = forceMemberApprovalRequired !== undefined; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. similar to the |
||
|
|
||
| return ( | ||
| <div className="p-4 rounded-lg border border-[var(--border)] bg-[var(--card)]"> | ||
| <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"> | ||
| When enabled, new users will need approval from an organization owner before they can access your deployment. | ||
| </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 className="flex-shrink-0"> | ||
| <Switch | ||
| checked={enabled} | ||
| onCheckedChange={handleToggle} | ||
| disabled={isLoading} | ||
| disabled={isDisabled} | ||
| /> | ||
| </div> | ||
| </div> | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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; | ||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major 🧩 Analysis chainNormalize Keeps semantics clear (enforced true/false vs. not enforced) and aligns with - const forceMemberApprovalRequired = env.FORCE_MEMBER_APPROVAL_REQUIRED;
+ const forceMemberApprovalRequired = (() => {
+ const v = env.FORCE_MEMBER_APPROVAL_REQUIRED?.toLowerCase();
+ if (v === "true") return true;
+ if (v === "false") return false;
+ return undefined;
+ })();Run to confirm there are no other string-typed usages that would conflict: Also applies to: 41-41 🏁 Script executed: #!/bin/bash
# Verify all references and ensure downstream types can accept boolean|undefined
rg -n --pretty --no-ignore 'FORCE_MEMBER_APPROVAL_REQUIRED'
rg -n --type=ts -C2 'forceMemberApprovalRequired\??:\s*string'Length of output: 3006 Normalize
- const forceMemberApprovalRequired = env.FORCE_MEMBER_APPROVAL_REQUIRED;
+ const forceMemberApprovalRequired = (() => {
+ const v = env.FORCE_MEMBER_APPROVAL_REQUIRED?.toLowerCase();
+ if (v === 'true') return true;
+ if (v === 'false') return false;
+ return undefined;
+ })();
- forceMemberApprovalRequired?: string
+ forceMemberApprovalRequired?: boolean📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. coderabbit is probably right here although its suggestion is verbose.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain@msukkari You're absolutely right! The simpler approach is much cleaner and follows the same pattern as const forceMemberApprovalRequired = env.FORCE_MEMBER_APPROVAL_REQUIRED === 'true';This maintains consistency with the existing 🏁 Script executed: Length of output: 2353 Perfect! Now I can see the complete picture. Here are the changes needed across all three components to implement the simpler boolean approach: 1. In const forceMemberApprovalRequired = env.FORCE_MEMBER_APPROVAL_REQUIRED === 'true';2. In interface OrganizationAccessSettingsWrapperProps {
memberApprovalRequired: boolean
inviteLinkEnabled: boolean
inviteLink: string | null
forceMemberApprovalRequired?: boolean // Changed from string to boolean
}3. In interface MemberApprovalRequiredToggleProps {
memberApprovalRequired: boolean
onToggleChange?: (checked: boolean) => void
forceMemberApprovalRequired?: boolean // Changed from string to boolean
}This approach is much cleaner, follows the established pattern with |
||||||||||||||||||
|
|
||||||||||||||||||
| return ( | ||||||||||||||||||
| <div className="space-y-6"> | ||||||||||||||||||
|
|
@@ -37,6 +38,7 @@ export async function OrganizationAccessSettings() { | |||||||||||||||||
| memberApprovalRequired={org.memberApprovalRequired} | ||||||||||||||||||
| inviteLinkEnabled={org.inviteLinkEnabled} | ||||||||||||||||||
| inviteLink={inviteLink} | ||||||||||||||||||
| forceMemberApprovalRequired={forceMemberApprovalRequired} | ||||||||||||||||||
| /> | ||||||||||||||||||
| </div> | ||||||||||||||||||
| ) | ||||||||||||||||||
|
|
||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,6 +21,7 @@ export const env = createEnv({ | |
|
|
||
| // Auth | ||
| FORCE_ENABLE_ANONYMOUS_ACCESS: booleanSchema.default('false'), | ||
| FORCE_MEMBER_APPROVAL_REQUIRED: booleanSchema.optional(), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. following the rest of the changes to only force UI stuff when this is enabled, we can also default this to false to match the other env var |
||
|
|
||
| AUTH_SECRET: z.string(), | ||
| AUTH_URL: z.string().url(), | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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`); | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
Comment on lines
+134
to
+145
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chainForce must be enforced server‑side, not only at init Currently this sets the value on startup/sync, but an API call could still flip Run to locate and patch the update path(s): Suggested server check (illustrative): // In the handler that updates org access settings:
import { env } from '@/env.mjs';
if (env.FORCE_MEMBER_APPROVAL_REQUIRED !== undefined) {
return new Response(JSON.stringify({ error: 'memberApprovalRequired is forced by env' }), { status: 409 });
}🏁 Script executed: #!/bin/bash
# Find all updates to memberApprovalRequired and the access settings API.
rg -nP -C3 '\bmemberApprovalRequired\s*:' --type=ts
rg -nP -C3 '(access|org).*settings|memberApproval' --type=ts -g '!**/node_modules/**'Length of output: 4482 Enforce FORCE_MEMBER_APPROVAL_REQUIRED in org update endpoint (actions.ts:1816–1822)
🤖 Prompt for AI Agents
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not super needed since the only way we set this is in the UI and we're properly disabling it when the env var is enabeld
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain@msukkari I understand your point about the UI being disabled, but I'd like to verify the API surface to ensure there aren't other ways to bypass this protection. 🏁 Script executed: Length of output: 3596 🏁 Script executed: Length of output: 1422 @msukkari I found the server action that can bypass the UI protection. Looking at the code, there's a // From actions.ts lines 1814-1824
export const setMemberApprovalRequired = async (domain: string, required: boolean): Promise<{ success: boolean } | ServiceError> => sew(async () =>
withAuth(async (userId) =>
withOrgMembership(userId, domain, async ({ org }) => {
await prisma.org.update({
where: { id: org.id },
data: { memberApprovalRequired: required },
});
return {
success: true,
};This server action can be called directly via form actions or fetch requests, completely bypassing the disabled UI toggle. While the UI protection is good UX, relying solely on client-side controls for enforcement isn't secure. A simple server-side check in this function would ensure the environment variable setting is truly enforced: if (env.FORCE_MEMBER_APPROVAL_REQUIRED !== undefined) {
return { statusCode: 409, errorCode: "SETTING_FORCED_BY_ENV", message: "Member approval setting is controlled by environment variable" };
}This would provide defense in depth and ensure the configuration can't be circumvented. |
||||||||||||||
|
|
||||||||||||||
| 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 | ||||||||||||||
|
Comment on lines
+196
to
+198
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
default values for data in the db are set by the schema so its best to not have them set in the code. We only want to set this if the env var is true anyways |
||||||||||||||
| } | ||||||||||||||
| }); | ||||||||||||||
| } 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) { | ||||||||||||||
|
|
||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.