Merge branch 'cross-org-idp' into dev

This commit is contained in:
miloschwartz
2026-04-17 17:27:34 -07:00
28 changed files with 1231 additions and 329 deletions

View File

@@ -97,7 +97,8 @@ export default function GeneralPage() {
emailPath: z.string().nullable().optional(),
namePath: z.string().nullable().optional(),
scopes: z.string().min(1, { message: t("idpScopeRequired") }),
autoProvision: z.boolean().default(false)
autoProvision: z.boolean().default(false),
orgMapping: z.string().optional()
});
// Google form schema (simplified)
@@ -109,7 +110,8 @@ export default function GeneralPage() {
.min(1, { message: t("idpClientSecretRequired") }),
roleMapping: z.string().nullable().optional(),
roleId: z.number().nullable().optional(),
autoProvision: z.boolean().default(false)
autoProvision: z.boolean().default(false),
orgMapping: z.string().optional()
});
// Azure form schema (simplified with tenant ID)
@@ -122,7 +124,8 @@ export default function GeneralPage() {
tenantId: z.string().min(1, { message: t("idpTenantIdRequired") }),
roleMapping: z.string().nullable().optional(),
roleId: z.number().nullable().optional(),
autoProvision: z.boolean().default(false)
autoProvision: z.boolean().default(false),
orgMapping: z.string().optional()
});
type OidcFormValues = z.infer<typeof OidcFormSchema>;
@@ -160,7 +163,8 @@ export default function GeneralPage() {
autoProvision: true,
roleMapping: null,
roleId: null,
tenantId: ""
tenantId: "",
orgMapping: ""
}
});
@@ -227,7 +231,8 @@ export default function GeneralPage() {
clientSecret: data.idpOidcConfig.clientSecret,
autoProvision: data.idp.autoProvision,
roleMapping: roleMapping || null,
roleId: null
roleId: null,
orgMapping: data.idpOrg?.orgMapping ?? ""
};
// Add variant-specific fields
@@ -344,12 +349,14 @@ export default function GeneralPage() {
}
// Build payload based on variant
const orgMappingTrimmed = data.orgMapping?.trim() ?? "";
let payload: any = {
name: data.name,
clientId: data.clientId,
clientSecret: data.clientSecret,
autoProvision: data.autoProvision,
roleMapping: roleMappingExpression
roleMapping: roleMappingExpression,
orgMapping: orgMappingTrimmed === "" ? null : orgMappingTrimmed
};
// Add variant-specific fields
@@ -532,6 +539,10 @@ export default function GeneralPage() {
}
rawExpression={rawRoleExpression}
onRawExpressionChange={setRawRoleExpression}
orgMappingField={{
control: form.control,
name: "orgMapping"
}}
/>
</form>
</Form>

View File

@@ -91,7 +91,8 @@ export default function Page() {
tenantId: z.string().optional(),
autoProvision: z.boolean().default(false),
roleMapping: z.string().nullable().optional(),
roleId: z.number().nullable().optional()
roleId: z.number().nullable().optional(),
orgMapping: z.string().optional()
});
type CreateIdpFormValues = z.infer<typeof createIdpFormSchema>;
@@ -112,7 +113,8 @@ export default function Page() {
tenantId: "",
autoProvision: false,
roleMapping: null,
roleId: null
roleId: null,
orgMapping: ""
}
});
@@ -177,7 +179,7 @@ export default function Page() {
return;
}
const payload = {
const payload: Record<string, unknown> = {
name: data.name,
clientId: data.clientId,
clientSecret: data.clientSecret,
@@ -191,6 +193,10 @@ export default function Page() {
scopes: data.scopes,
variant: data.type
};
const trimmedOrgMapping = data.orgMapping?.trim();
if (trimmedOrgMapping) {
payload.orgMapping = trimmedOrgMapping;
}
// Use the appropriate endpoint based on provider type
const endpoint = "oidc";
@@ -336,6 +342,10 @@ export default function Page() {
}
rawExpression={rawRoleExpression}
onRawExpressionChange={setRawRoleExpression}
orgMappingField={{
control: form.control,
name: "orgMapping"
}}
/>
</form>
</Form>

View File

@@ -46,7 +46,7 @@ import { Checkbox } from "@app/components/ui/checkbox";
import { ListIdpsResponse } from "@server/routers/idp";
import { useTranslations } from "next-intl";
import { build } from "@server/build";
import Image from "next/image";
import IdpTypeIcon from "@app/components/IdpTypeIcon";
import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
import OrgRolesTagField from "@app/components/OrgRolesTagField";
@@ -152,31 +152,8 @@ export default function Page() {
const getIdpIcon = (variant: string | null) => {
if (!variant) return null;
switch (variant.toLowerCase()) {
case "google":
return (
<Image
src="/idp/google.png"
alt={t("idpGoogleAlt")}
width={24}
height={24}
className="rounded"
/>
);
case "azure":
return (
<Image
src="/idp/azure.png"
alt={t("idpAzureAlt")}
width={24}
height={24}
className="rounded"
/>
);
default:
return null;
}
const type = variant.toLowerCase();
return <IdpTypeIcon type={type} size={24} />;
};
const validFor = [
@@ -340,15 +317,16 @@ export default function Page() {
const roleIds = values.roles.map((r) => parseInt(r.id, 10));
const res = await api.post<AxiosResponse<InviteUserResponse>>(
`/org/${orgId}/create-invite`,
{
email: values.email,
roleIds,
validHours: parseInt(values.validForHours),
sendEmail
}
)
const res = await api
.post<AxiosResponse<InviteUserResponse>>(
`/org/${orgId}/create-invite`,
{
email: values.email,
roleIds,
validHours: parseInt(values.validForHours),
sendEmail
}
)
.catch((e) => {
if (e.response?.status === 409) {
toast({

View File

@@ -20,7 +20,6 @@ import {
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
@@ -63,7 +62,7 @@ import {
SettingsSectionForm
} from "@app/components/Settings";
import { useTranslations } from "next-intl";
import RoleMappingConfigFields from "@app/components/RoleMappingConfigFields";
import AutoProvisionConfigWidget from "@app/components/AutoProvisionConfigWidget";
import {
compileRoleMappingExpression,
createMappingBuilderRule,
@@ -499,9 +498,17 @@ export default function PoliciesPage() {
id="policy-default-mappings-form"
className="space-y-6"
>
<RoleMappingConfigFields
fieldIdPrefix="admin-idp-default-role"
showFreeformRoleNamesHint={true}
<AutoProvisionConfigWidget
showAutoProvisionSwitch={false}
autoProvision={true}
onAutoProvisionChange={() => {}}
orgMappingField={{
control: defaultMappingsForm.control,
name: "defaultOrgMapping",
labelKey: "defaultMappingsOrg"
}}
roleMappingFieldIdPrefix="admin-idp-default-role"
showFreeformRoleNamesHint
roleMappingMode={defaultRoleMappingMode}
onRoleMappingModeChange={
setDefaultRoleMappingMode
@@ -528,27 +535,6 @@ export default function PoliciesPage() {
setDefaultRawRoleExpression
}
/>
<FormField
control={defaultMappingsForm.control}
name="defaultOrgMapping"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("defaultMappingsOrg")}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
{t(
"defaultMappingsOrgDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
<SettingsSectionFooter>
@@ -687,9 +673,15 @@ export default function PoliciesPage() {
)}
/>
<RoleMappingConfigFields
fieldIdPrefix="admin-idp-policy-role"
showFreeformRoleNamesHint={false}
<AutoProvisionConfigWidget
showAutoProvisionSwitch={false}
autoProvision={true}
onAutoProvisionChange={() => {}}
orgMappingField={{
control: form.control,
name: "orgMapping"
}}
roleMappingFieldIdPrefix="admin-idp-policy-role"
roleMappingMode={policyRoleMappingMode}
onRoleMappingModeChange={
setPolicyRoleMappingMode
@@ -716,27 +708,6 @@ export default function PoliciesPage() {
setPolicyRawRoleExpression
}
/>
<FormField
control={form.control}
name="orgMapping"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("orgMappingPathOptional")}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
{t(
"defaultMappingsOrgDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</CredenzaBody>

View File

@@ -24,7 +24,6 @@ import {
import HeaderTitle from "@app/components/SettingsSectionTitle";
import IdpAutoProvisionUsersDescription from "@app/components/IdpAutoProvisionUsersDescription";
import { SwitchInput } from "@app/components/SwitchInput";
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
import { Button } from "@app/components/ui/button";
import { Input } from "@app/components/ui/input";
import { usePaidStatus } from "@app/hooks/usePaidStatus";
@@ -34,7 +33,6 @@ import { createApiClient, formatAxiosError } from "@app/lib/api";
import { applyOidcIdpProviderType } from "@app/lib/idp/oidcIdpProviderDefaults";
import { zodResolver } from "@hookform/resolvers/zod";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
import { InfoIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import { useState } from "react";
@@ -220,23 +218,6 @@ export default function Page() {
)}
/>
<div className="flex items-start mb-0">
<SwitchInput
id="auto-provision-toggle"
label={t(
"idpAutoProvisionUsers"
)}
defaultChecked={form.getValues(
"autoProvision"
)}
onCheckedChange={(checked) => {
form.setValue(
"autoProvision",
checked
);
}}
/>
</div>
</form>
</Form>
</SettingsSectionForm>
@@ -244,6 +225,32 @@ export default function Page() {
</SettingsSectionBody>
</SettingsSection>
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t("idpAutoProvisionUsers")}
</SettingsSectionTitle>
<SettingsSectionDescription>
<IdpAutoProvisionUsersDescription />
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<div className="space-y-2">
<SwitchInput
id="auto-provision-toggle"
label={t("idpAutoProvisionUsers")}
defaultChecked={form.getValues("autoProvision")}
onCheckedChange={(checked) => {
form.setValue("autoProvision", checked);
}}
/>
<p className="text-sm text-muted-foreground">
{t("idpAutoProvisionConfigureAfterCreate")}
</p>
</div>
</SettingsSectionBody>
</SettingsSection>
<fieldset
disabled={templatesLocked}
className="min-w-0 border-0 p-0 m-0 disabled:pointer-events-none disabled:opacity-60"