Merge branch 'org-only-idp' into dev

This commit is contained in:
miloschwartz
2026-01-09 13:34:52 -08:00
16 changed files with 101 additions and 53 deletions

View File

@@ -139,6 +139,10 @@ 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 =
this.rawPrivateConfig.flags.use_org_only_idp.toString();
}
} }
public getRawPrivateConfig() { public getRawPrivateConfig() {

View File

@@ -83,7 +83,8 @@ export const privateConfigSchema = z.object({
flags: z flags: z
.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)
}) })
.optional() .optional()
.prefault({}), .prefault({}),

View File

@@ -28,6 +28,7 @@ import { eq, InferInsertModel } from "drizzle-orm";
import { getOrgTierData } from "#private/lib/billing"; import { getOrgTierData } from "#private/lib/billing";
import { TierId } from "@server/lib/billing/tiers"; import { TierId } from "@server/lib/billing/tiers";
import { build } from "@server/build"; import { build } from "@server/build";
import config from "@server/private/lib/config";
const paramsSchema = z.strictObject({ const paramsSchema = z.strictObject({
orgId: z.string() orgId: z.string()
@@ -94,8 +95,10 @@ export async function upsertLoginPageBranding(
typeof loginPageBranding typeof loginPageBranding
>; >;
if (build !== "saas") { if (
// org branding settings are only considered in the saas build build !== "saas" &&
!config.getRawPrivateConfig().flags.use_org_only_idp
) {
const { orgTitle, orgSubtitle, ...rest } = updateData; const { orgTitle, orgSubtitle, ...rest } = updateData;
updateData = rest; updateData = rest;
} }

View File

@@ -0,0 +1,18 @@
import { pullEnv } from "@app/lib/pullEnv";
import { build } from "@server/build";
import { redirect } from "next/navigation";
interface LayoutProps {
children: React.ReactNode;
params: Promise<{}>;
}
export default async function Layout(props: LayoutProps) {
const env = pullEnv();
if (build !== "saas" && !env.flags.useOrgOnlyIdp) {
redirect("/");
}
return props.children;
}

View File

@@ -82,7 +82,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
<Layout <Layout
orgId={params.orgId} orgId={params.orgId}
orgs={orgs} orgs={orgs}
navItems={orgNavSections()} navItems={orgNavSections(env)}
> >
{children} {children}
</Layout> </Layout>

View File

@@ -36,8 +36,8 @@ import {
import type { ResourceContextType } from "@app/contexts/resourceContext"; import type { ResourceContextType } from "@app/contexts/resourceContext";
import { useEnvContext } from "@app/hooks/useEnvContext"; import { useEnvContext } from "@app/hooks/useEnvContext";
import { useOrgContext } from "@app/hooks/useOrgContext"; import { useOrgContext } from "@app/hooks/useOrgContext";
import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { useResourceContext } from "@app/hooks/useResourceContext"; import { useResourceContext } from "@app/hooks/useResourceContext";
import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
import { toast } from "@app/hooks/useToast"; import { toast } from "@app/hooks/useToast";
import { createApiClient, formatAxiosError } from "@app/lib/api"; import { createApiClient, formatAxiosError } from "@app/lib/api";
import { orgQueries, resourceQueries } from "@app/lib/queries"; import { orgQueries, resourceQueries } from "@app/lib/queries";
@@ -95,7 +95,7 @@ export default function ResourceAuthenticationPage() {
const router = useRouter(); const router = useRouter();
const t = useTranslations(); const t = useTranslations();
const subscription = useSubscriptionStatusContext(); const { isPaidUser } = usePaidStatus();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { data: resourceRoles = [], isLoading: isLoadingResourceRoles } = const { data: resourceRoles = [], isLoading: isLoadingResourceRoles } =
@@ -129,7 +129,8 @@ 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
}) })
); );
@@ -159,7 +160,7 @@ export default function ResourceAuthenticationPage() {
const allIdps = useMemo(() => { const allIdps = useMemo(() => {
if (build === "saas") { if (build === "saas") {
if (subscription?.subscribed) { if (isPaidUser) {
return orgIdps.map((idp) => ({ return orgIdps.map((idp) => ({
id: idp.idpId, id: idp.idpId,
text: idp.name text: idp.name

View File

@@ -11,6 +11,7 @@ import { AxiosResponse } from "axios";
import { authCookieHeader } from "@app/lib/api/cookies"; import { authCookieHeader } from "@app/lib/api/cookies";
import { Layout } from "@app/components/Layout"; import { Layout } from "@app/components/Layout";
import { adminNavSections } from "../navigation"; import { adminNavSections } from "../navigation";
import { pullEnv } from "@app/lib/pullEnv";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
@@ -27,6 +28,8 @@ export default async function AdminLayout(props: LayoutProps) {
const getUser = cache(verifySession); const getUser = cache(verifySession);
const user = await getUser(); const user = await getUser();
const env = pullEnv();
if (!user || !user.serverAdmin) { if (!user || !user.serverAdmin) {
redirect(`/`); redirect(`/`);
} }
@@ -48,7 +51,7 @@ export default async function AdminLayout(props: LayoutProps) {
return ( return (
<UserProvider user={user}> <UserProvider user={user}>
<Layout orgs={orgs} navItems={adminNavSections}> <Layout orgs={orgs} navItems={adminNavSections(env)}>
{props.children} {props.children}
</Layout> </Layout>
</UserProvider> </UserProvider>

View File

@@ -70,7 +70,7 @@ export default async function Page(props: {
} }
let loginIdps: LoginFormIDP[] = []; let loginIdps: LoginFormIDP[] = [];
if (build !== "saas") { if (build === "oss" || !env.flags.useOrgOnlyIdp) {
const idpsRes = await cache( const idpsRes = await cache(
async () => await priv.get<AxiosResponse<ListIdpsResponse>>("/idp") async () => await priv.get<AxiosResponse<ListIdpsResponse>>("/idp")
)(); )();
@@ -121,7 +121,7 @@ export default async function Page(props: {
</p> </p>
)} )}
{!isInvite && build === "saas" ? ( {!isInvite && (build === "saas" || env.flags.useOrgOnlyIdp) ? (
<div className="text-center text-muted-foreground mt-12 flex flex-col items-center"> <div className="text-center text-muted-foreground mt-12 flex flex-col items-center">
<span>{t("needToSignInToOrg")}</span> <span>{t("needToSignInToOrg")}</span>
<Link <Link

View File

@@ -11,6 +11,7 @@ import {
} from "@server/routers/loginPage/types"; } from "@server/routers/loginPage/types";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import OrgLoginPage from "@app/components/OrgLoginPage"; import OrgLoginPage from "@app/components/OrgLoginPage";
import { pullEnv } from "@app/lib/pullEnv";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
@@ -21,7 +22,9 @@ export default async function OrgAuthPage(props: {
const searchParams = await props.searchParams; const searchParams = await props.searchParams;
const params = await props.params; const params = await props.params;
if (build !== "saas") { const env = pullEnv();
if (build !== "saas" && !env.flags.useOrgOnlyIdp) {
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}` : ""}`);
} }
@@ -50,29 +53,25 @@ export default async function OrgAuthPage(props: {
} catch (e) {} } catch (e) {}
let loginIdps: LoginFormIDP[] = []; let loginIdps: LoginFormIDP[] = [];
if (build === "saas") { const idpsRes = await priv.get<AxiosResponse<ListOrgIdpsResponse>>(
const idpsRes = await priv.get<AxiosResponse<ListOrgIdpsResponse>>( `/org/${orgId}/idp`
`/org/${orgId}/idp` );
);
loginIdps = idpsRes.data.data.idps.map((idp) => ({ loginIdps = idpsRes.data.data.idps.map((idp) => ({
idpId: idp.idpId, idpId: idp.idpId,
name: idp.name, name: idp.name,
variant: idp.variant variant: idp.variant
})) as LoginFormIDP[]; })) as LoginFormIDP[];
}
let branding: LoadLoginPageBrandingResponse | null = null; let branding: LoadLoginPageBrandingResponse | null = null;
if (build === "saas") { try {
try { const res = await priv.get<
const res = await priv.get< AxiosResponse<LoadLoginPageBrandingResponse>
AxiosResponse<LoadLoginPageBrandingResponse> >(`/login-page-branding?orgId=${orgId}`);
>(`/login-page-branding?orgId=${orgId}`); if (res.status === 200) {
if (res.status === 200) { branding = res.data.data;
branding = res.data.data; }
} } catch (error) {}
} catch (error) {}
}
return ( return (
<OrgLoginPage <OrgLoginPage

View File

@@ -33,12 +33,12 @@ export default async function OrgAuthPage(props: {
const forceLoginParam = searchParams.forceLogin; const forceLoginParam = searchParams.forceLogin;
const forceLogin = forceLoginParam === "true"; const forceLogin = forceLoginParam === "true";
if (build !== "saas") { const env = pullEnv();
if (build !== "saas" && !env.flags.useOrgOnlyIdp) {
redirect("/"); redirect("/");
} }
const env = pullEnv();
const authHeader = await authCookieHeader(); const authHeader = await authCookieHeader();
if (searchParams.token) { if (searchParams.token) {

View File

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

View File

@@ -1,4 +1,5 @@
import { SidebarNavItem } from "@app/components/SidebarNav"; import { SidebarNavItem } from "@app/components/SidebarNav";
import { Env } from "@app/lib/types/env";
import { build } from "@server/build"; import { build } from "@server/build";
import { import {
Settings, Settings,
@@ -39,7 +40,7 @@ export const orgLangingNavItems: SidebarNavItem[] = [
} }
]; ];
export const orgNavSections = (): SidebarNavSection[] => [ export const orgNavSections = (env?: Env): SidebarNavSection[] => [
{ {
heading: "sidebarGeneral", heading: "sidebarGeneral",
items: [ items: [
@@ -92,8 +93,7 @@ export const orgNavSections = (): SidebarNavSection[] => [
{ {
title: "sidebarRemoteExitNodes", title: "sidebarRemoteExitNodes",
href: "/{orgId}/settings/remote-exit-nodes", href: "/{orgId}/settings/remote-exit-nodes",
icon: <Server className="size-4 flex-none" />, icon: <Server className="size-4 flex-none" />
showEE: true
} }
] ]
: []) : [])
@@ -123,13 +123,12 @@ export const orgNavSections = (): SidebarNavSection[] => [
href: "/{orgId}/settings/access/roles", href: "/{orgId}/settings/access/roles",
icon: <Users className="size-4 flex-none" /> icon: <Users className="size-4 flex-none" />
}, },
...(build == "saas" ...(build == "saas" || env?.flags.useOrgOnlyIdp
? [ ? [
{ {
title: "sidebarIdentityProviders", title: "sidebarIdentityProviders",
href: "/{orgId}/settings/idp", href: "/{orgId}/settings/idp",
icon: <Fingerprint className="size-4 flex-none" />, icon: <Fingerprint className="size-4 flex-none" />
showEE: true
} }
] ]
: []), : []),
@@ -228,7 +227,7 @@ export const orgNavSections = (): SidebarNavSection[] => [
} }
]; ];
export const adminNavSections: SidebarNavSection[] = [ export const adminNavSections = (env?: Env): SidebarNavSection[] => [
{ {
heading: "sidebarAdmin", heading: "sidebarAdmin",
items: [ items: [
@@ -242,11 +241,15 @@ export const adminNavSections: 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
title: "sidebarIdentityProviders", ? [
href: "/admin/idp", {
icon: <Fingerprint className="size-4 flex-none" /> title: "sidebarIdentityProviders",
}, href: "/admin/idp",
icon: <Fingerprint className="size-4 flex-none" />
}
]
: []),
...(build == "enterprise" ...(build == "enterprise"
? [ ? [
{ {

View File

@@ -118,6 +118,7 @@ export default function AuthPageBrandingForm({
const brandingData = form.getValues(); const brandingData = form.getValues();
if (!isValid || !isPaidUser) return; if (!isValid || !isPaidUser) return;
try { try {
const updateRes = await api.put( const updateRes = await api.put(
`/org/${orgId}/login-page-branding`, `/org/${orgId}/login-page-branding`,
@@ -289,7 +290,8 @@ export default function AuthPageBrandingForm({
</div> </div>
</div> </div>
{build === "saas" && ( {build === "saas" ||
env.env.flags.useOrgOnlyIdp ? (
<> <>
<div className="mt-3 mb-6"> <div className="mt-3 mb-6">
<SettingsSectionTitle> <SettingsSectionTitle>
@@ -343,7 +345,7 @@ export default function AuthPageBrandingForm({
/> />
</div> </div>
</> </>
)} ) : null}
<div className="mt-3 mb-6"> <div className="mt-3 mb-6">
<SettingsSectionTitle> <SettingsSectionTitle>

View File

@@ -63,7 +63,9 @@ export function pullEnv(): Env {
disableProductHelpBanners: disableProductHelpBanners:
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
}, },
branding: { branding: {

View File

@@ -157,7 +157,13 @@ export const orgQueries = {
return res.data.data.domains; return res.data.data.domains;
} }
}), }),
identityProviders: ({ orgId }: { orgId: string }) => identityProviders: ({
orgId,
useOrgOnlyIdp
}: {
orgId: string;
useOrgOnlyIdp?: boolean;
}) =>
queryOptions({ queryOptions({
queryKey: ["ORG", orgId, "IDPS"] as const, queryKey: ["ORG", orgId, "IDPS"] as const,
queryFn: async ({ signal, meta }) => { queryFn: async ({ signal, meta }) => {
@@ -165,7 +171,12 @@ export const orgQueries = {
AxiosResponse<{ AxiosResponse<{
idps: { idpId: number; name: string }[]; idps: { idpId: number; name: string }[];
}> }>
>(build === "saas" ? `/org/${orgId}/idp` : "/idp", { signal }); >(
build === "saas" || useOrgOnlyIdp
? `/org/${orgId}/idp`
: "/idp",
{ signal }
);
return res.data.data.idps; return res.data.data.idps;
} }
}) })

View File

@@ -34,6 +34,7 @@ export type Env = {
hideSupporterKey: boolean; hideSupporterKey: boolean;
usePangolinDns: boolean; usePangolinDns: boolean;
disableProductHelpBanners: boolean; disableProductHelpBanners: boolean;
useOrgOnlyIdp: boolean;
}; };
branding: { branding: {
appName?: string; appName?: string;