diff --git a/server/db/sqlite/schema/privateSchema.ts b/server/db/sqlite/schema/privateSchema.ts index 265cee10..40f6d713 100644 --- a/server/db/sqlite/schema/privateSchema.ts +++ b/server/db/sqlite/schema/privateSchema.ts @@ -79,6 +79,7 @@ export const subscriptionItems = sqliteTable("subscriptionItems", { subscriptionItemId: integer("subscriptionItemId").primaryKey({ autoIncrement: true }), + stripeSubscriptionItemId: text("stripeSubscriptionItemId"), subscriptionId: text("subscriptionId") .notNull() .references(() => subscriptions.subscriptionId, { diff --git a/server/private/lib/config.ts b/server/private/lib/config.ts index a1baf5a6..2c3490ba 100644 --- a/server/private/lib/config.ts +++ b/server/private/lib/config.ts @@ -65,6 +65,11 @@ export class PrivateConfig { this.rawPrivateConfig.branding?.logo?.dark_path || undefined; } + if (this.rawPrivateConfig.app.identity_provider_mode) { + process.env.IDENTITY_PROVIDER_MODE = + this.rawPrivateConfig.app.identity_provider_mode; + } + process.env.BRANDING_LOGO_AUTH_WIDTH = this.rawPrivateConfig.branding ?.logo?.auth_page?.width ? this.rawPrivateConfig.branding?.logo?.auth_page?.width.toString() @@ -129,10 +134,8 @@ export class PrivateConfig { process.env.USE_PANGOLIN_DNS = this.rawPrivateConfig.flags.use_pangolin_dns.toString(); } - if (this.rawPrivateConfig.flags.use_org_only_idp) { - process.env.USE_ORG_ONLY_IDP = - this.rawPrivateConfig.flags.use_org_only_idp.toString(); - } + + console.log(this.rawPrivateConfig.app.identity_provider_mode); } public getRawPrivateConfig() { diff --git a/server/private/lib/readConfigFile.ts b/server/private/lib/readConfigFile.ts index ac528e73..e5efa498 100644 --- a/server/private/lib/readConfigFile.ts +++ b/server/private/lib/readConfigFile.ts @@ -25,7 +25,8 @@ export const privateConfigSchema = z.object({ app: z .object({ region: z.string().optional().default("default"), - base_domain: z.string().optional() + base_domain: z.string().optional(), + identity_provider_mode: z.enum(["global", "org"]).optional() }) .optional() .default({ @@ -95,7 +96,7 @@ export const privateConfigSchema = z.object({ .object({ enable_redis: z.boolean().optional().default(false), use_pangolin_dns: z.boolean().optional().default(false), - use_org_only_idp: z.boolean().optional().default(false), + use_org_only_idp: z.boolean().optional() }) .optional() .prefault({}), @@ -181,7 +182,29 @@ export const privateConfigSchema = z.object({ // localFilePath: z.string().optional() }) .optional() -}); +}) + .transform((data) => { + // this to maintain backwards compatibility with the old config file + const identityProviderMode = data.app?.identity_provider_mode; + const useOrgOnlyIdp = data.flags?.use_org_only_idp; + + if (identityProviderMode !== undefined) { + return data; + } + if (useOrgOnlyIdp === true) { + return { + ...data, + app: { ...data.app, identity_provider_mode: "org" as const } + }; + } + if (useOrgOnlyIdp === false) { + return { + ...data, + app: { ...data.app, identity_provider_mode: "global" as const } + }; + } + return data; + }); export function readPrivateConfigFile() { if (build == "oss") { diff --git a/server/private/routers/orgIdp/createOrgOidcIdp.ts b/server/private/routers/orgIdp/createOrgOidcIdp.ts index d1874033..77346fd9 100644 --- a/server/private/routers/orgIdp/createOrgOidcIdp.ts +++ b/server/private/routers/orgIdp/createOrgOidcIdp.ts @@ -27,6 +27,7 @@ import config from "@server/lib/config"; import { CreateOrgIdpResponse } from "@server/routers/orgIdp/types"; import { isSubscribed } from "#private/lib/isSubscribed"; import { tierMatrix } from "@server/lib/billing/tierMatrix"; +import privateConfig from "#private/lib/config"; const paramsSchema = z.strictObject({ orgId: z.string().nonempty() }); @@ -92,6 +93,18 @@ export async function createOrgOidcIdp( ); } + if ( + privateConfig.getRawPrivateConfig().app.identity_provider_mode !== + "org" + ) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Organization-specific IdP creation is not allowed in the current identity provider mode. Set app.identity_provider_mode to 'org' in the private configuration to enable this feature." + ) + ); + } + const { clientId, clientSecret, diff --git a/server/private/routers/orgIdp/deleteOrgIdp.ts b/server/private/routers/orgIdp/deleteOrgIdp.ts index 176f4238..2d6b0899 100644 --- a/server/private/routers/orgIdp/deleteOrgIdp.ts +++ b/server/private/routers/orgIdp/deleteOrgIdp.ts @@ -22,6 +22,7 @@ import { fromError } from "zod-validation-error"; import { idp, idpOidcConfig, idpOrg } from "@server/db"; import { eq } from "drizzle-orm"; import { OpenAPITags, registry } from "@server/openApi"; +import privateConfig from "#private/lib/config"; const paramsSchema = z .object({ @@ -59,6 +60,18 @@ export async function deleteOrgIdp( const { idpId } = parsedParams.data; + if ( + privateConfig.getRawPrivateConfig().app.identity_provider_mode !== + "org" + ) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Organization-specific IdP creation is not allowed in the current identity provider mode. Set app.identity_provider_mode to 'org' in the private configuration to enable this feature." + ) + ); + } + // Check if IDP exists const [existingIdp] = await db .select() diff --git a/server/private/routers/orgIdp/updateOrgOidcIdp.ts b/server/private/routers/orgIdp/updateOrgOidcIdp.ts index c5619460..804afbe6 100644 --- a/server/private/routers/orgIdp/updateOrgOidcIdp.ts +++ b/server/private/routers/orgIdp/updateOrgOidcIdp.ts @@ -26,6 +26,7 @@ import { encrypt } from "@server/lib/crypto"; import config from "@server/lib/config"; import { isSubscribed } from "#private/lib/isSubscribed"; import { tierMatrix } from "@server/lib/billing/tierMatrix"; +import privateConfig from "#private/lib/config"; const paramsSchema = z .object({ @@ -97,6 +98,18 @@ export async function updateOrgOidcIdp( ); } + if ( + privateConfig.getRawPrivateConfig().app.identity_provider_mode !== + "org" + ) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Organization-specific IdP creation is not allowed in the current identity provider mode. Set app.identity_provider_mode to 'org' in the private configuration to enable this feature." + ) + ); + } + const { idpId, orgId } = parsedParams.data; const { clientId, diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx index 121e7196..b00ce1ee 100644 --- a/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx @@ -132,7 +132,7 @@ export default function ResourceAuthenticationPage() { const { data: orgIdps = [], isLoading: isLoadingOrgIdps } = useQuery( orgQueries.identityProviders({ orgId: org.org.orgId, - useOrgOnlyIdp: env.flags.useOrgOnlyIdp + useOrgOnlyIdp: env.app.identityProviderMode === "org" }) ); diff --git a/src/app/auth/login/page.tsx b/src/app/auth/login/page.tsx index 2ba4d7f8..bfb552df 100644 --- a/src/app/auth/login/page.tsx +++ b/src/app/auth/login/page.tsx @@ -76,12 +76,13 @@ export default async function Page(props: { // Only use SmartLoginForm if NOT (OSS build OR org-only IdP enabled) const useSmartLogin = - build === "saas" || (build === "enterprise" && env.flags.useOrgOnlyIdp); + build === "saas" || + (build === "enterprise" && env.app.identityProviderMode === "org"); let loginIdps: LoginFormIDP[] = []; if (!useSmartLogin) { // Load IdPs for DashboardLoginForm (OSS or org-only IdP mode) - if (build === "oss" || !env.flags.useOrgOnlyIdp) { + if (build === "oss" || env.app.identityProviderMode !== "org") { const idpsRes = await cache( async () => await priv.get>("/idp") @@ -165,7 +166,8 @@ export default async function Page(props: { forceLogin={forceLogin} showOrgLogin={ !isInvite && - (build === "saas" || env.flags.useOrgOnlyIdp) + (build === "saas" || + env.app.identityProviderMode === "org") } searchParams={searchParams} defaultUser={defaultUser} @@ -188,7 +190,8 @@ export default async function Page(props: {

)} - {!isInvite && (build === "saas" || env.flags.useOrgOnlyIdp) ? ( + {!isInvite && + (build === "saas" || env.app.identityProviderMode === "org") ? ( diff --git a/src/app/navigation.tsx b/src/app/navigation.tsx index d74ef30b..cb95099e 100644 --- a/src/app/navigation.tsx +++ b/src/app/navigation.tsx @@ -124,7 +124,8 @@ export const orgNavSections = (env?: Env): SidebarNavSection[] => [ // PaidFeaturesAlert ...((build === "oss" && !env?.flags.disableEnterpriseFeatures) || build === "saas" || - env?.flags.useOrgOnlyIdp + env?.app.identityProviderMode === "org" || + env?.app.identityProviderMode === undefined ? [ { title: "sidebarIdentityProviders", @@ -251,7 +252,9 @@ export const adminNavSections = (env?: Env): SidebarNavSection[] => [ href: "/admin/api-keys", icon: }, - ...(build === "oss" || !env?.flags.useOrgOnlyIdp + ...(build === "oss" || + env?.app.identityProviderMode === "global" || + env?.app.identityProviderMode === undefined ? [ { title: "sidebarIdentityProviders", diff --git a/src/components/AuthPageBrandingForm.tsx b/src/components/AuthPageBrandingForm.tsx index daceb2fa..a1998062 100644 --- a/src/components/AuthPageBrandingForm.tsx +++ b/src/components/AuthPageBrandingForm.tsx @@ -322,7 +322,7 @@ export default function AuthPageBrandingForm({ {build === "saas" || - env.env.flags.useOrgOnlyIdp ? ( + env.env.app.identityProviderMode === "org" ? ( <>
diff --git a/src/components/PermissionsSelectBox.tsx b/src/components/PermissionsSelectBox.tsx index 73f8a212..b11c635a 100644 --- a/src/components/PermissionsSelectBox.tsx +++ b/src/components/PermissionsSelectBox.tsx @@ -118,7 +118,7 @@ function getActionsCategories(root: boolean) { } }; - if (root || build === "saas" || env.flags.useOrgOnlyIdp) { + if (root || build === "saas" || env.app.identityProviderMode === "org") { actionsByCategory["Identity Provider (IDP)"] = { [t("actionCreateIdp")]: "createIdp", [t("actionUpdateIdp")]: "updateIdp", diff --git a/src/components/SignupForm.tsx b/src/components/SignupForm.tsx index 9cb93757..a54b1c23 100644 --- a/src/components/SignupForm.tsx +++ b/src/components/SignupForm.tsx @@ -204,7 +204,9 @@ export default function SignupForm({ ? env.branding.logo?.authPage?.height || 44 : 44; - const showOrgBanner = fromSmartLogin && (build === "saas" || env.flags.useOrgOnlyIdp); + const showOrgBanner = + fromSmartLogin && + (build === "saas" || env.app.identityProviderMode === "org"); const orgBannerHref = redirect ? `/auth/org?redirect=${encodeURIComponent(redirect)}` : "/auth/org"; @@ -226,388 +228,398 @@ export default function SignupForm({ )} - -
- -
-
-

{getSubtitle()}

-
-
- -
- - ( - - {t("email")} - - - - - - )} - /> - ( - -
- {t("password")} - {passwordStrength.strength === - "strong" && ( - - )} -
- -
+ +
+ +
+
+

{getSubtitle()}

+
+
+ + + + ( + + {t("email")} + { - field.onChange(e); - setPasswordValue( - e.target.value - ); - }} - className={cn( - passwordStrength.strength === - "strong" && - "border-green-500 focus-visible:ring-green-500", - passwordStrength.strength === - "medium" && - "border-yellow-500 focus-visible:ring-yellow-500", - passwordStrength.strength === - "weak" && - passwordValue.length > - 0 && - "border-red-500 focus-visible:ring-red-500" - )} - autoComplete="new-password" + disabled={!!emailParam} /> -
-
- - {passwordValue.length > 0 && ( -
- {/* Password Strength Meter */} -
-
- - {t("passwordStrength")} - - - {t( - `passwordStrength${passwordStrength.strength.charAt(0).toUpperCase() + passwordStrength.strength.slice(1)}` - )} - -
- -
- - {/* Requirements Checklist */} -
-
- {t("passwordRequirements")} -
-
-
- {passwordStrength - .requirements - .length ? ( - - ) : ( - - )} - - {t( - "passwordRequirementLengthText" - )} - -
-
- {passwordStrength - .requirements - .uppercase ? ( - - ) : ( - - )} - - {t( - "passwordRequirementUppercaseText" - )} - -
-
- {passwordStrength - .requirements - .lowercase ? ( - - ) : ( - - )} - - {t( - "passwordRequirementLowercaseText" - )} - -
-
- {passwordStrength - .requirements - .number ? ( - - ) : ( - - )} - - {t( - "passwordRequirementNumberText" - )} - -
-
- {passwordStrength - .requirements - .special ? ( - - ) : ( - - )} - - {t( - "passwordRequirementSpecialText" - )} - -
-
-
-
- )} - - {/* Only show FormMessage when not showing our custom requirements */} - {passwordValue.length === 0 && ( + - )} -
- )} - /> - ( - -
- - {t("confirmPassword")} - - {doPasswordsMatch && ( - - )} -
- -
- { - field.onChange(e); - setConfirmPasswordValue( - e.target.value - ); - }} - className={cn( - doPasswordsMatch && - "border-green-500 focus-visible:ring-green-500", - confirmPasswordValue.length > - 0 && - !doPasswordsMatch && - "border-red-500 focus-visible:ring-red-500" - )} - autoComplete="new-password" - /> + + )} + /> + ( + +
+ + {t("password")} + + {passwordStrength.strength === + "strong" && ( + + )}
- - {confirmPasswordValue.length > 0 && - !doPasswordsMatch && ( -

- {t("passwordsDoNotMatch")} -

- )} - {/* Only show FormMessage when field is empty */} - {confirmPasswordValue.length === 0 && ( - - )} -
- )} - /> - {build === "saas" && ( - <> - ( - - - { - field.onChange(checked); - handleTermsChange( - checked as boolean + +
+ { + field.onChange(e); + setPasswordValue( + e.target.value ); }} - /> - -
- -
- {t( - "signUpTerms.IAgreeToThe" - )}{" "} - - {t( - "signUpTerms.termsOfService" - )}{" "} - - {t("signUpTerms.and")}{" "} - - {t( - "signUpTerms.privacyPolicy" - )} - -
-
- -
- - )} - /> - ( - - - - -
- - {t( - "signUpMarketing.keepMeInTheLoop" + className={cn( + passwordStrength.strength === + "strong" && + "border-green-500 focus-visible:ring-green-500", + passwordStrength.strength === + "medium" && + "border-yellow-500 focus-visible:ring-yellow-500", + passwordStrength.strength === + "weak" && + passwordValue.length > + 0 && + "border-red-500 focus-visible:ring-red-500" )} - - + autoComplete="new-password" + />
-
- )} - /> - - )} + - {error && ( - - {error} - - )} + {passwordValue.length > 0 && ( +
+ {/* Password Strength Meter */} +
+
+ + {t( + "passwordStrength" + )} + + + {t( + `passwordStrength${passwordStrength.strength.charAt(0).toUpperCase() + passwordStrength.strength.slice(1)}` + )} + +
+ +
- - - - - + {/* Requirements Checklist */} +
+
+ {t( + "passwordRequirements" + )} +
+
+
+ {passwordStrength + .requirements + .length ? ( + + ) : ( + + )} + + {t( + "passwordRequirementLengthText" + )} + +
+
+ {passwordStrength + .requirements + .uppercase ? ( + + ) : ( + + )} + + {t( + "passwordRequirementUppercaseText" + )} + +
+
+ {passwordStrength + .requirements + .lowercase ? ( + + ) : ( + + )} + + {t( + "passwordRequirementLowercaseText" + )} + +
+
+ {passwordStrength + .requirements + .number ? ( + + ) : ( + + )} + + {t( + "passwordRequirementNumberText" + )} + +
+
+ {passwordStrength + .requirements + .special ? ( + + ) : ( + + )} + + {t( + "passwordRequirementSpecialText" + )} + +
+
+
+
+ )} + + {/* Only show FormMessage when not showing our custom requirements */} + {passwordValue.length === 0 && ( + + )} + + )} + /> + ( + +
+ + {t("confirmPassword")} + + {doPasswordsMatch && ( + + )} +
+ +
+ { + field.onChange(e); + setConfirmPasswordValue( + e.target.value + ); + }} + className={cn( + doPasswordsMatch && + "border-green-500 focus-visible:ring-green-500", + confirmPasswordValue.length > + 0 && + !doPasswordsMatch && + "border-red-500 focus-visible:ring-red-500" + )} + autoComplete="new-password" + /> +
+
+ {confirmPasswordValue.length > 0 && + !doPasswordsMatch && ( +

+ {t("passwordsDoNotMatch")} +

+ )} + {/* Only show FormMessage when field is empty */} + {confirmPasswordValue.length === 0 && ( + + )} +
+ )} + /> + {build === "saas" && ( + <> + ( + + + { + field.onChange( + checked + ); + handleTermsChange( + checked as boolean + ); + }} + /> + +
+ +
+ {t( + "signUpTerms.IAgreeToThe" + )}{" "} + + {t( + "signUpTerms.termsOfService" + )}{" "} + + {t( + "signUpTerms.and" + )}{" "} + + {t( + "signUpTerms.privacyPolicy" + )} + +
+
+ +
+
+ )} + /> + ( + + + + +
+ + {t( + "signUpMarketing.keepMeInTheLoop" + )} + + +
+
+ )} + /> + + )} + + {error && ( + + {error} + + )} + + + + + + ); } diff --git a/src/lib/pullEnv.ts b/src/lib/pullEnv.ts index 298745b9..ddbd42c2 100644 --- a/src/lib/pullEnv.ts +++ b/src/lib/pullEnv.ts @@ -32,7 +32,11 @@ export function pullEnv(): Env { process.env.NEW_RELEASES_NOTIFICATION_ENABLED === "true" ? true : false - } + }, + identityProviderMode: process.env.IDENTITY_PROVIDER_MODE as + | "org" + | "global" + | undefined }, email: { emailEnabled: process.env.EMAIL_ENABLED === "true" ? true : false @@ -64,8 +68,6 @@ export function pullEnv(): Env { process.env.FLAGS_DISABLE_PRODUCT_HELP_BANNERS === "true" ? true : false, - useOrgOnlyIdp: - process.env.USE_ORG_ONLY_IDP === "true" ? true : false, disableEnterpriseFeatures: process.env.DISABLE_ENTERPRISE_FEATURES === "true" ? true diff --git a/src/lib/types/env.ts b/src/lib/types/env.ts index 925e4348..46513ae5 100644 --- a/src/lib/types/env.ts +++ b/src/lib/types/env.ts @@ -8,6 +8,7 @@ export type Env = { product_updates: boolean; new_releases: boolean; }; + identityProviderMode?: "global" | "org"; }; server: { externalPort: string; @@ -34,7 +35,6 @@ export type Env = { hideSupporterKey: boolean; usePangolinDns: boolean; disableProductHelpBanners: boolean; - useOrgOnlyIdp: boolean; disableEnterpriseFeatures: boolean; }; branding: {