add identity provider mode setting

This commit is contained in:
miloschwartz
2026-02-11 18:04:56 -08:00
parent 937f6fdae8
commit 143acbae48
17 changed files with 481 additions and 395 deletions

View File

@@ -79,6 +79,7 @@ export const subscriptionItems = sqliteTable("subscriptionItems", {
subscriptionItemId: integer("subscriptionItemId").primaryKey({ subscriptionItemId: integer("subscriptionItemId").primaryKey({
autoIncrement: true autoIncrement: true
}), }),
stripeSubscriptionItemId: text("stripeSubscriptionItemId"),
subscriptionId: text("subscriptionId") subscriptionId: text("subscriptionId")
.notNull() .notNull()
.references(() => subscriptions.subscriptionId, { .references(() => subscriptions.subscriptionId, {

View File

@@ -65,6 +65,11 @@ export class PrivateConfig {
this.rawPrivateConfig.branding?.logo?.dark_path || undefined; 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 process.env.BRANDING_LOGO_AUTH_WIDTH = this.rawPrivateConfig.branding
?.logo?.auth_page?.width ?.logo?.auth_page?.width
? this.rawPrivateConfig.branding?.logo?.auth_page?.width.toString() ? this.rawPrivateConfig.branding?.logo?.auth_page?.width.toString()
@@ -129,10 +134,8 @@ export class PrivateConfig {
process.env.USE_PANGOLIN_DNS = process.env.USE_PANGOLIN_DNS =
this.rawPrivateConfig.flags.use_pangolin_dns.toString(); this.rawPrivateConfig.flags.use_pangolin_dns.toString();
} }
if (this.rawPrivateConfig.flags.use_org_only_idp) {
process.env.USE_ORG_ONLY_IDP = console.log(this.rawPrivateConfig.app.identity_provider_mode);
this.rawPrivateConfig.flags.use_org_only_idp.toString();
}
} }
public getRawPrivateConfig() { public getRawPrivateConfig() {

View File

@@ -25,7 +25,8 @@ export const privateConfigSchema = z.object({
app: z app: z
.object({ .object({
region: z.string().optional().default("default"), 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() .optional()
.default({ .default({
@@ -95,7 +96,7 @@ export const privateConfigSchema = z.object({
.object({ .object({
enable_redis: z.boolean().optional().default(false), enable_redis: z.boolean().optional().default(false),
use_pangolin_dns: 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() .optional()
.prefault({}), .prefault({}),
@@ -181,7 +182,29 @@ export const privateConfigSchema = z.object({
// localFilePath: z.string().optional() // localFilePath: z.string().optional()
}) })
.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() { export function readPrivateConfigFile() {
if (build == "oss") { if (build == "oss") {

View File

@@ -27,6 +27,7 @@ import config from "@server/lib/config";
import { CreateOrgIdpResponse } from "@server/routers/orgIdp/types"; import { CreateOrgIdpResponse } from "@server/routers/orgIdp/types";
import { isSubscribed } from "#private/lib/isSubscribed"; import { isSubscribed } from "#private/lib/isSubscribed";
import { tierMatrix } from "@server/lib/billing/tierMatrix"; import { tierMatrix } from "@server/lib/billing/tierMatrix";
import privateConfig from "#private/lib/config";
const paramsSchema = z.strictObject({ orgId: z.string().nonempty() }); 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 { const {
clientId, clientId,
clientSecret, clientSecret,

View File

@@ -22,6 +22,7 @@ import { fromError } from "zod-validation-error";
import { idp, idpOidcConfig, idpOrg } from "@server/db"; import { idp, idpOidcConfig, idpOrg } from "@server/db";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
import privateConfig from "#private/lib/config";
const paramsSchema = z const paramsSchema = z
.object({ .object({
@@ -59,6 +60,18 @@ export async function deleteOrgIdp(
const { idpId } = parsedParams.data; 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 // Check if IDP exists
const [existingIdp] = await db const [existingIdp] = await db
.select() .select()

View File

@@ -26,6 +26,7 @@ import { encrypt } from "@server/lib/crypto";
import config from "@server/lib/config"; import config from "@server/lib/config";
import { isSubscribed } from "#private/lib/isSubscribed"; import { isSubscribed } from "#private/lib/isSubscribed";
import { tierMatrix } from "@server/lib/billing/tierMatrix"; import { tierMatrix } from "@server/lib/billing/tierMatrix";
import privateConfig from "#private/lib/config";
const paramsSchema = z const paramsSchema = z
.object({ .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 { idpId, orgId } = parsedParams.data;
const { const {
clientId, clientId,

View File

@@ -132,7 +132,7 @@ export default function ResourceAuthenticationPage() {
const { data: orgIdps = [], isLoading: isLoadingOrgIdps } = useQuery( const { data: orgIdps = [], isLoading: isLoadingOrgIdps } = useQuery(
orgQueries.identityProviders({ orgQueries.identityProviders({
orgId: org.org.orgId, orgId: org.org.orgId,
useOrgOnlyIdp: env.flags.useOrgOnlyIdp useOrgOnlyIdp: env.app.identityProviderMode === "org"
}) })
); );

View File

@@ -76,12 +76,13 @@ export default async function Page(props: {
// Only use SmartLoginForm if NOT (OSS build OR org-only IdP enabled) // Only use SmartLoginForm if NOT (OSS build OR org-only IdP enabled)
const useSmartLogin = const useSmartLogin =
build === "saas" || (build === "enterprise" && env.flags.useOrgOnlyIdp); build === "saas" ||
(build === "enterprise" && env.app.identityProviderMode === "org");
let loginIdps: LoginFormIDP[] = []; let loginIdps: LoginFormIDP[] = [];
if (!useSmartLogin) { if (!useSmartLogin) {
// Load IdPs for DashboardLoginForm (OSS or org-only IdP mode) // 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( const idpsRes = await cache(
async () => async () =>
await priv.get<AxiosResponse<ListIdpsResponse>>("/idp") await priv.get<AxiosResponse<ListIdpsResponse>>("/idp")
@@ -165,7 +166,8 @@ export default async function Page(props: {
forceLogin={forceLogin} forceLogin={forceLogin}
showOrgLogin={ showOrgLogin={
!isInvite && !isInvite &&
(build === "saas" || env.flags.useOrgOnlyIdp) (build === "saas" ||
env.app.identityProviderMode === "org")
} }
searchParams={searchParams} searchParams={searchParams}
defaultUser={defaultUser} defaultUser={defaultUser}
@@ -188,7 +190,8 @@ export default async function Page(props: {
</p> </p>
)} )}
{!isInvite && (build === "saas" || env.flags.useOrgOnlyIdp) ? ( {!isInvite &&
(build === "saas" || env.app.identityProviderMode === "org") ? (
<OrgSignInLink <OrgSignInLink
href={`/auth/org${buildQueryString(searchParams)}`} href={`/auth/org${buildQueryString(searchParams)}`}
linkText={t("orgAuthSignInToOrg")} linkText={t("orgAuthSignInToOrg")}

View File

@@ -24,7 +24,7 @@ export default async function OrgAuthPage(props: {
const env = pullEnv(); const env = pullEnv();
if (build !== "saas" && !env.flags.useOrgOnlyIdp) { if (build !== "saas" && env.app.identityProviderMode !== "org") {
const queryString = new URLSearchParams(searchParams as any).toString(); const queryString = new URLSearchParams(searchParams as any).toString();
redirect(`/auth/login${queryString ? `?${queryString}` : ""}`); redirect(`/auth/login${queryString ? `?${queryString}` : ""}`);
} }

View File

@@ -35,7 +35,7 @@ export default async function OrgAuthPage(props: {
const env = pullEnv(); const env = pullEnv();
if (build !== "saas" && !env.flags.useOrgOnlyIdp) { if (build !== "saas" && env.app.identityProviderMode !== "org") {
redirect("/"); redirect("/");
} }

View File

@@ -202,7 +202,7 @@ export default async function ResourceAuthPage(props: {
} }
let loginIdps: LoginFormIDP[] = []; let loginIdps: LoginFormIDP[] = [];
if (build === "saas" || env.flags.useOrgOnlyIdp) { if (build === "saas" || env.app.identityProviderMode === "org") {
if (subscribed) { if (subscribed) {
const idpsRes = await cache( const idpsRes = await cache(
async () => async () =>

View File

@@ -124,7 +124,8 @@ export const orgNavSections = (env?: Env): SidebarNavSection[] => [
// PaidFeaturesAlert // PaidFeaturesAlert
...((build === "oss" && !env?.flags.disableEnterpriseFeatures) || ...((build === "oss" && !env?.flags.disableEnterpriseFeatures) ||
build === "saas" || build === "saas" ||
env?.flags.useOrgOnlyIdp env?.app.identityProviderMode === "org" ||
env?.app.identityProviderMode === undefined
? [ ? [
{ {
title: "sidebarIdentityProviders", title: "sidebarIdentityProviders",
@@ -251,7 +252,9 @@ export const adminNavSections = (env?: Env): SidebarNavSection[] => [
href: "/admin/api-keys", href: "/admin/api-keys",
icon: <KeyRound className="size-4 flex-none" /> icon: <KeyRound className="size-4 flex-none" />
}, },
...(build === "oss" || !env?.flags.useOrgOnlyIdp ...(build === "oss" ||
env?.app.identityProviderMode === "global" ||
env?.app.identityProviderMode === undefined
? [ ? [
{ {
title: "sidebarIdentityProviders", title: "sidebarIdentityProviders",

View File

@@ -322,7 +322,7 @@ export default function AuthPageBrandingForm({
</div> </div>
{build === "saas" || {build === "saas" ||
env.env.flags.useOrgOnlyIdp ? ( env.env.app.identityProviderMode === "org" ? (
<> <>
<div className="mt-3 mb-6"> <div className="mt-3 mb-6">
<SettingsSectionTitle> <SettingsSectionTitle>

View File

@@ -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)"] = { actionsByCategory["Identity Provider (IDP)"] = {
[t("actionCreateIdp")]: "createIdp", [t("actionCreateIdp")]: "createIdp",
[t("actionUpdateIdp")]: "updateIdp", [t("actionUpdateIdp")]: "updateIdp",

View File

@@ -204,7 +204,9 @@ export default function SignupForm({
? env.branding.logo?.authPage?.height || 44 ? env.branding.logo?.authPage?.height || 44
: 44; : 44;
const showOrgBanner = fromSmartLogin && (build === "saas" || env.flags.useOrgOnlyIdp); const showOrgBanner =
fromSmartLogin &&
(build === "saas" || env.app.identityProviderMode === "org");
const orgBannerHref = redirect const orgBannerHref = redirect
? `/auth/org?redirect=${encodeURIComponent(redirect)}` ? `/auth/org?redirect=${encodeURIComponent(redirect)}`
: "/auth/org"; : "/auth/org";
@@ -226,388 +228,398 @@ export default function SignupForm({
</Alert> </Alert>
)} )}
<Card className="w-full max-w-md"> <Card className="w-full max-w-md">
<CardHeader className="border-b"> <CardHeader className="border-b">
<div className="flex flex-row items-center justify-center"> <div className="flex flex-row items-center justify-center">
<BrandingLogo height={logoHeight} width={logoWidth} /> <BrandingLogo height={logoHeight} width={logoWidth} />
</div> </div>
<div className="text-center space-y-1 pt-3"> <div className="text-center space-y-1 pt-3">
<p className="text-muted-foreground">{getSubtitle()}</p> <p className="text-muted-foreground">{getSubtitle()}</p>
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="pt-6"> <CardContent className="pt-6">
<Form {...form}> <Form {...form}>
<form <form
onSubmit={form.handleSubmit(onSubmit)} onSubmit={form.handleSubmit(onSubmit)}
className="space-y-4" className="space-y-4"
> >
<FormField <FormField
control={form.control} control={form.control}
name="email" name="email"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t("email")}</FormLabel> <FormLabel>{t("email")}</FormLabel>
<FormControl> <FormControl>
<Input
{...field}
disabled={!!emailParam}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<div className="flex items-center gap-2">
<FormLabel>{t("password")}</FormLabel>
{passwordStrength.strength ===
"strong" && (
<Check className="h-4 w-4 text-green-500" />
)}
</div>
<FormControl>
<div className="relative">
<Input <Input
type="password"
{...field} {...field}
onChange={(e) => { disabled={!!emailParam}
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"
/> />
</div> </FormControl>
</FormControl>
{passwordValue.length > 0 && (
<div className="space-y-3 mt-2">
{/* Password Strength Meter */}
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm font-medium text-foreground">
{t("passwordStrength")}
</span>
<span
className={cn(
"text-sm font-semibold",
passwordStrength.strength ===
"strong" &&
"text-green-600 dark:text-green-400",
passwordStrength.strength ===
"medium" &&
"text-yellow-600 dark:text-yellow-400",
passwordStrength.strength ===
"weak" &&
"text-red-600 dark:text-red-400"
)}
>
{t(
`passwordStrength${passwordStrength.strength.charAt(0).toUpperCase() + passwordStrength.strength.slice(1)}`
)}
</span>
</div>
<Progress
value={
passwordStrength.percentage
}
className="h-2"
/>
</div>
{/* Requirements Checklist */}
<div className="bg-muted rounded-lg p-3 space-y-2">
<div className="text-sm font-medium text-foreground mb-2">
{t("passwordRequirements")}
</div>
<div className="grid grid-cols-1 gap-1.5">
<div className="flex items-center gap-2">
{passwordStrength
.requirements
.length ? (
<Check className="h-3.5 w-3.5 text-green-500 flex-shrink-0" />
) : (
<X className="h-3.5 w-3.5 text-muted-foreground flex-shrink-0" />
)}
<span
className={cn(
"text-sm",
passwordStrength
.requirements
.length
? "text-green-600 dark:text-green-400"
: "text-muted-foreground"
)}
>
{t(
"passwordRequirementLengthText"
)}
</span>
</div>
<div className="flex items-center gap-2">
{passwordStrength
.requirements
.uppercase ? (
<Check className="h-3.5 w-3.5 text-green-500 flex-shrink-0" />
) : (
<X className="h-3.5 w-3.5 text-muted-foreground flex-shrink-0" />
)}
<span
className={cn(
"text-sm",
passwordStrength
.requirements
.uppercase
? "text-green-600 dark:text-green-400"
: "text-muted-foreground"
)}
>
{t(
"passwordRequirementUppercaseText"
)}
</span>
</div>
<div className="flex items-center gap-2">
{passwordStrength
.requirements
.lowercase ? (
<Check className="h-3.5 w-3.5 text-green-500 flex-shrink-0" />
) : (
<X className="h-3.5 w-3.5 text-muted-foreground flex-shrink-0" />
)}
<span
className={cn(
"text-sm",
passwordStrength
.requirements
.lowercase
? "text-green-600 dark:text-green-400"
: "text-muted-foreground"
)}
>
{t(
"passwordRequirementLowercaseText"
)}
</span>
</div>
<div className="flex items-center gap-2">
{passwordStrength
.requirements
.number ? (
<Check className="h-3.5 w-3.5 text-green-500 flex-shrink-0" />
) : (
<X className="h-3.5 w-3.5 text-muted-foreground flex-shrink-0" />
)}
<span
className={cn(
"text-sm",
passwordStrength
.requirements
.number
? "text-green-600 dark:text-green-400"
: "text-muted-foreground"
)}
>
{t(
"passwordRequirementNumberText"
)}
</span>
</div>
<div className="flex items-center gap-2">
{passwordStrength
.requirements
.special ? (
<Check className="h-3.5 w-3.5 text-green-500 flex-shrink-0" />
) : (
<X className="h-3.5 w-3.5 text-muted-foreground flex-shrink-0" />
)}
<span
className={cn(
"text-sm",
passwordStrength
.requirements
.special
? "text-green-600 dark:text-green-400"
: "text-muted-foreground"
)}
>
{t(
"passwordRequirementSpecialText"
)}
</span>
</div>
</div>
</div>
</div>
)}
{/* Only show FormMessage when not showing our custom requirements */}
{passwordValue.length === 0 && (
<FormMessage /> <FormMessage />
)} </FormItem>
</FormItem> )}
)} />
/> <FormField
<FormField control={form.control}
control={form.control} name="password"
name="confirmPassword" render={({ field }) => (
render={({ field }) => ( <FormItem>
<FormItem> <div className="flex items-center gap-2">
<div className="flex items-center gap-2"> <FormLabel>
<FormLabel> {t("password")}
{t("confirmPassword")} </FormLabel>
</FormLabel> {passwordStrength.strength ===
{doPasswordsMatch && ( "strong" && (
<Check className="h-4 w-4 text-green-500" /> <Check className="h-4 w-4 text-green-500" />
)} )}
</div>
<FormControl>
<div className="relative">
<Input
type="password"
{...field}
onChange={(e) => {
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"
/>
</div> </div>
</FormControl> <FormControl>
{confirmPasswordValue.length > 0 && <div className="relative">
!doPasswordsMatch && ( <Input
<p className="text-sm text-red-600 mt-1"> type="password"
{t("passwordsDoNotMatch")} {...field}
</p> onChange={(e) => {
)} field.onChange(e);
{/* Only show FormMessage when field is empty */} setPasswordValue(
{confirmPasswordValue.length === 0 && ( e.target.value
<FormMessage />
)}
</FormItem>
)}
/>
{build === "saas" && (
<>
<FormField
control={form.control}
name="agreeToTerms"
render={({ field }) => (
<FormItem className="flex flex-row items-center">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={(
checked
) => {
field.onChange(checked);
handleTermsChange(
checked as boolean
); );
}} }}
/> className={cn(
</FormControl> passwordStrength.strength ===
<div className="leading-none"> "strong" &&
<FormLabel className="text-sm font-normal"> "border-green-500 focus-visible:ring-green-500",
<div> passwordStrength.strength ===
{t( "medium" &&
"signUpTerms.IAgreeToThe" "border-yellow-500 focus-visible:ring-yellow-500",
)}{" "} passwordStrength.strength ===
<a "weak" &&
href="https://pangolin.net/terms-of-service.html" passwordValue.length >
target="_blank" 0 &&
rel="noopener noreferrer" "border-red-500 focus-visible:ring-red-500"
className="text-primary hover:underline"
>
{t(
"signUpTerms.termsOfService"
)}{" "}
</a>
{t("signUpTerms.and")}{" "}
<a
href="https://pangolin.net/privacy-policy.html"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
{t(
"signUpTerms.privacyPolicy"
)}
</a>
</div>
</FormLabel>
<FormMessage />
</div>
</FormItem>
)}
/>
<FormField
control={form.control}
name="marketingEmailConsent"
render={({ field }) => (
<FormItem className="flex flex-row items-start">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={
field.onChange
}
/>
</FormControl>
<div className="leading-none">
<FormLabel className="text-sm font-normal">
{t(
"signUpMarketing.keepMeInTheLoop"
)} )}
</FormLabel> autoComplete="new-password"
<FormMessage /> />
</div> </div>
</FormItem> </FormControl>
)}
/>
</>
)}
{error && ( {passwordValue.length > 0 && (
<Alert variant="destructive"> <div className="space-y-3 mt-2">
<AlertDescription>{error}</AlertDescription> {/* Password Strength Meter */}
</Alert> <div className="space-y-2">
)} <div className="flex justify-between items-center">
<span className="text-sm font-medium text-foreground">
{t(
"passwordStrength"
)}
</span>
<span
className={cn(
"text-sm font-semibold",
passwordStrength.strength ===
"strong" &&
"text-green-600 dark:text-green-400",
passwordStrength.strength ===
"medium" &&
"text-yellow-600 dark:text-yellow-400",
passwordStrength.strength ===
"weak" &&
"text-red-600 dark:text-red-400"
)}
>
{t(
`passwordStrength${passwordStrength.strength.charAt(0).toUpperCase() + passwordStrength.strength.slice(1)}`
)}
</span>
</div>
<Progress
value={
passwordStrength.percentage
}
className="h-2"
/>
</div>
<Button type="submit" className="w-full"> {/* Requirements Checklist */}
{t("createAccount")} <div className="bg-muted rounded-lg p-3 space-y-2">
</Button> <div className="text-sm font-medium text-foreground mb-2">
</form> {t(
</Form> "passwordRequirements"
</CardContent> )}
</Card> </div>
<div className="grid grid-cols-1 gap-1.5">
<div className="flex items-center gap-2">
{passwordStrength
.requirements
.length ? (
<Check className="h-3.5 w-3.5 text-green-500 flex-shrink-0" />
) : (
<X className="h-3.5 w-3.5 text-muted-foreground flex-shrink-0" />
)}
<span
className={cn(
"text-sm",
passwordStrength
.requirements
.length
? "text-green-600 dark:text-green-400"
: "text-muted-foreground"
)}
>
{t(
"passwordRequirementLengthText"
)}
</span>
</div>
<div className="flex items-center gap-2">
{passwordStrength
.requirements
.uppercase ? (
<Check className="h-3.5 w-3.5 text-green-500 flex-shrink-0" />
) : (
<X className="h-3.5 w-3.5 text-muted-foreground flex-shrink-0" />
)}
<span
className={cn(
"text-sm",
passwordStrength
.requirements
.uppercase
? "text-green-600 dark:text-green-400"
: "text-muted-foreground"
)}
>
{t(
"passwordRequirementUppercaseText"
)}
</span>
</div>
<div className="flex items-center gap-2">
{passwordStrength
.requirements
.lowercase ? (
<Check className="h-3.5 w-3.5 text-green-500 flex-shrink-0" />
) : (
<X className="h-3.5 w-3.5 text-muted-foreground flex-shrink-0" />
)}
<span
className={cn(
"text-sm",
passwordStrength
.requirements
.lowercase
? "text-green-600 dark:text-green-400"
: "text-muted-foreground"
)}
>
{t(
"passwordRequirementLowercaseText"
)}
</span>
</div>
<div className="flex items-center gap-2">
{passwordStrength
.requirements
.number ? (
<Check className="h-3.5 w-3.5 text-green-500 flex-shrink-0" />
) : (
<X className="h-3.5 w-3.5 text-muted-foreground flex-shrink-0" />
)}
<span
className={cn(
"text-sm",
passwordStrength
.requirements
.number
? "text-green-600 dark:text-green-400"
: "text-muted-foreground"
)}
>
{t(
"passwordRequirementNumberText"
)}
</span>
</div>
<div className="flex items-center gap-2">
{passwordStrength
.requirements
.special ? (
<Check className="h-3.5 w-3.5 text-green-500 flex-shrink-0" />
) : (
<X className="h-3.5 w-3.5 text-muted-foreground flex-shrink-0" />
)}
<span
className={cn(
"text-sm",
passwordStrength
.requirements
.special
? "text-green-600 dark:text-green-400"
: "text-muted-foreground"
)}
>
{t(
"passwordRequirementSpecialText"
)}
</span>
</div>
</div>
</div>
</div>
)}
{/* Only show FormMessage when not showing our custom requirements */}
{passwordValue.length === 0 && (
<FormMessage />
)}
</FormItem>
)}
/>
<FormField
control={form.control}
name="confirmPassword"
render={({ field }) => (
<FormItem>
<div className="flex items-center gap-2">
<FormLabel>
{t("confirmPassword")}
</FormLabel>
{doPasswordsMatch && (
<Check className="h-4 w-4 text-green-500" />
)}
</div>
<FormControl>
<div className="relative">
<Input
type="password"
{...field}
onChange={(e) => {
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"
/>
</div>
</FormControl>
{confirmPasswordValue.length > 0 &&
!doPasswordsMatch && (
<p className="text-sm text-red-600 mt-1">
{t("passwordsDoNotMatch")}
</p>
)}
{/* Only show FormMessage when field is empty */}
{confirmPasswordValue.length === 0 && (
<FormMessage />
)}
</FormItem>
)}
/>
{build === "saas" && (
<>
<FormField
control={form.control}
name="agreeToTerms"
render={({ field }) => (
<FormItem className="flex flex-row items-center">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={(
checked
) => {
field.onChange(
checked
);
handleTermsChange(
checked as boolean
);
}}
/>
</FormControl>
<div className="leading-none">
<FormLabel className="text-sm font-normal">
<div>
{t(
"signUpTerms.IAgreeToThe"
)}{" "}
<a
href="https://pangolin.net/terms-of-service.html"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
{t(
"signUpTerms.termsOfService"
)}{" "}
</a>
{t(
"signUpTerms.and"
)}{" "}
<a
href="https://pangolin.net/privacy-policy.html"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
{t(
"signUpTerms.privacyPolicy"
)}
</a>
</div>
</FormLabel>
<FormMessage />
</div>
</FormItem>
)}
/>
<FormField
control={form.control}
name="marketingEmailConsent"
render={({ field }) => (
<FormItem className="flex flex-row items-start">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={
field.onChange
}
/>
</FormControl>
<div className="leading-none">
<FormLabel className="text-sm font-normal">
{t(
"signUpMarketing.keepMeInTheLoop"
)}
</FormLabel>
<FormMessage />
</div>
</FormItem>
)}
/>
</>
)}
{error && (
<Alert variant="destructive">
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<Button type="submit" className="w-full">
{t("createAccount")}
</Button>
</form>
</Form>
</CardContent>
</Card>
</> </>
); );
} }

View File

@@ -32,7 +32,11 @@ export function pullEnv(): Env {
process.env.NEW_RELEASES_NOTIFICATION_ENABLED === "true" process.env.NEW_RELEASES_NOTIFICATION_ENABLED === "true"
? true ? true
: false : false
} },
identityProviderMode: process.env.IDENTITY_PROVIDER_MODE as
| "org"
| "global"
| undefined
}, },
email: { email: {
emailEnabled: process.env.EMAIL_ENABLED === "true" ? true : false emailEnabled: process.env.EMAIL_ENABLED === "true" ? true : false
@@ -64,8 +68,6 @@ export function pullEnv(): Env {
process.env.FLAGS_DISABLE_PRODUCT_HELP_BANNERS === "true" process.env.FLAGS_DISABLE_PRODUCT_HELP_BANNERS === "true"
? true ? true
: false, : false,
useOrgOnlyIdp:
process.env.USE_ORG_ONLY_IDP === "true" ? true : false,
disableEnterpriseFeatures: disableEnterpriseFeatures:
process.env.DISABLE_ENTERPRISE_FEATURES === "true" process.env.DISABLE_ENTERPRISE_FEATURES === "true"
? true ? true

View File

@@ -8,6 +8,7 @@ export type Env = {
product_updates: boolean; product_updates: boolean;
new_releases: boolean; new_releases: boolean;
}; };
identityProviderMode?: "global" | "org";
}; };
server: { server: {
externalPort: string; externalPort: string;
@@ -34,7 +35,6 @@ export type Env = {
hideSupporterKey: boolean; hideSupporterKey: boolean;
usePangolinDns: boolean; usePangolinDns: boolean;
disableProductHelpBanners: boolean; disableProductHelpBanners: boolean;
useOrgOnlyIdp: boolean;
disableEnterpriseFeatures: boolean; disableEnterpriseFeatures: boolean;
}; };
branding: { branding: {