mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-31 15:06:42 +00:00
add google and azure templates to global idp
This commit is contained in:
@@ -890,7 +890,7 @@
|
|||||||
"defaultMappingsRole": "Default Role Mapping",
|
"defaultMappingsRole": "Default Role Mapping",
|
||||||
"defaultMappingsRoleDescription": "The result of this expression must return the role name as defined in the organization as a string.",
|
"defaultMappingsRoleDescription": "The result of this expression must return the role name as defined in the organization as a string.",
|
||||||
"defaultMappingsOrg": "Default Organization Mapping",
|
"defaultMappingsOrg": "Default Organization Mapping",
|
||||||
"defaultMappingsOrgDescription": "This expression must return the org ID or true for the user to be allowed to access the organization.",
|
"defaultMappingsOrgDescription": "When set, this expression must return the organization ID or true for the user to access that organization. When unset, defining an organization policy for that org is enough: the user is allowed in as long as a valid role mapping can be resolved for them within the organization.",
|
||||||
"defaultMappingsSubmit": "Save Default Mappings",
|
"defaultMappingsSubmit": "Save Default Mappings",
|
||||||
"orgPoliciesEdit": "Edit Organization Policy",
|
"orgPoliciesEdit": "Edit Organization Policy",
|
||||||
"org": "Organization",
|
"org": "Organization",
|
||||||
@@ -1942,19 +1942,19 @@
|
|||||||
"invalidValue": "Invalid value",
|
"invalidValue": "Invalid value",
|
||||||
"idpTypeLabel": "Identity Provider Type",
|
"idpTypeLabel": "Identity Provider Type",
|
||||||
"roleMappingExpressionPlaceholder": "e.g., contains(groups, 'admin') && 'Admin' || 'Member'",
|
"roleMappingExpressionPlaceholder": "e.g., contains(groups, 'admin') && 'Admin' || 'Member'",
|
||||||
"roleMappingModeFixedRoles": "Fixed roles",
|
"roleMappingModeFixedRoles": "Fixed Roles",
|
||||||
"roleMappingModeMappingBuilder": "Mapping builder",
|
"roleMappingModeMappingBuilder": "Mapping Builder",
|
||||||
"roleMappingModeRawExpression": "Raw expression",
|
"roleMappingModeRawExpression": "Raw Expression",
|
||||||
"roleMappingFixedRolesPlaceholderSelect": "Select one or more roles",
|
"roleMappingFixedRolesPlaceholderSelect": "Select one or more roles",
|
||||||
"roleMappingFixedRolesPlaceholderFreeform": "Type role names (exact match per organization)",
|
"roleMappingFixedRolesPlaceholderFreeform": "Type role names (exact match per organization)",
|
||||||
"roleMappingFixedRolesDescriptionSameForAll": "Assign the same role set to every auto-provisioned user.",
|
"roleMappingFixedRolesDescriptionSameForAll": "Assign the same role set to every auto-provisioned user.",
|
||||||
"roleMappingFixedRolesDescriptionDefaultPolicy": "For default policies, type role names that exist in each organization where users are provisioned. Names must match exactly.",
|
"roleMappingFixedRolesDescriptionDefaultPolicy": "For default policies, type role names that exist in each organization where users are provisioned. Names must match exactly.",
|
||||||
"roleMappingClaimPath": "Claim path",
|
"roleMappingClaimPath": "Claim Path",
|
||||||
"roleMappingClaimPathPlaceholder": "groups",
|
"roleMappingClaimPathPlaceholder": "groups",
|
||||||
"roleMappingClaimPathDescription": "Path in the token payload that contains source values (for example, groups).",
|
"roleMappingClaimPathDescription": "Path in the token payload that contains source values (for example, groups).",
|
||||||
"roleMappingMatchValue": "Match value",
|
"roleMappingMatchValue": "Match Value",
|
||||||
"roleMappingAssignRoles": "Assign roles",
|
"roleMappingAssignRoles": "Assign Roles",
|
||||||
"roleMappingAddMappingRule": "Add mapping rule",
|
"roleMappingAddMappingRule": "Add Mapping Rule",
|
||||||
"roleMappingRawExpressionResultDescription": "Expression must evaluate to a string or string array.",
|
"roleMappingRawExpressionResultDescription": "Expression must evaluate to a string or string array.",
|
||||||
"roleMappingMatchValuePlaceholder": "Match value (for example: admin)",
|
"roleMappingMatchValuePlaceholder": "Match value (for example: admin)",
|
||||||
"roleMappingAssignRolesPlaceholderFreeform": "Type role names (exact per org)",
|
"roleMappingAssignRolesPlaceholderFreeform": "Type role names (exact per org)",
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ const bodySchema = z.strictObject({
|
|||||||
namePath: z.string().optional(),
|
namePath: z.string().optional(),
|
||||||
scopes: z.string().nonempty(),
|
scopes: z.string().nonempty(),
|
||||||
autoProvision: z.boolean().optional(),
|
autoProvision: z.boolean().optional(),
|
||||||
tags: z.string().optional()
|
tags: z.string().optional(),
|
||||||
|
variant: z.enum(["oidc", "google", "azure"]).optional().default("oidc")
|
||||||
});
|
});
|
||||||
|
|
||||||
export type CreateIdpResponse = {
|
export type CreateIdpResponse = {
|
||||||
@@ -77,7 +78,8 @@ export async function createOidcIdp(
|
|||||||
namePath,
|
namePath,
|
||||||
name,
|
name,
|
||||||
autoProvision,
|
autoProvision,
|
||||||
tags
|
tags,
|
||||||
|
variant
|
||||||
} = parsedBody.data;
|
} = parsedBody.data;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -121,7 +123,8 @@ export async function createOidcIdp(
|
|||||||
scopes,
|
scopes,
|
||||||
identifierPath,
|
identifierPath,
|
||||||
emailPath,
|
emailPath,
|
||||||
namePath
|
namePath,
|
||||||
|
variant
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ const bodySchema = z.strictObject({
|
|||||||
autoProvision: z.boolean().optional(),
|
autoProvision: z.boolean().optional(),
|
||||||
defaultRoleMapping: z.string().optional(),
|
defaultRoleMapping: z.string().optional(),
|
||||||
defaultOrgMapping: z.string().optional(),
|
defaultOrgMapping: z.string().optional(),
|
||||||
tags: z.string().optional()
|
tags: z.string().optional(),
|
||||||
|
variant: z.enum(["oidc", "google", "azure"]).optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type UpdateIdpResponse = {
|
export type UpdateIdpResponse = {
|
||||||
@@ -96,7 +97,8 @@ export async function updateOidcIdp(
|
|||||||
autoProvision,
|
autoProvision,
|
||||||
defaultRoleMapping,
|
defaultRoleMapping,
|
||||||
defaultOrgMapping,
|
defaultOrgMapping,
|
||||||
tags
|
tags,
|
||||||
|
variant
|
||||||
} = parsedBody.data;
|
} = parsedBody.data;
|
||||||
|
|
||||||
if (process.env.IDENTITY_PROVIDER_MODE === "org") {
|
if (process.env.IDENTITY_PROVIDER_MODE === "org") {
|
||||||
@@ -159,7 +161,8 @@ export async function updateOidcIdp(
|
|||||||
scopes,
|
scopes,
|
||||||
identifierPath,
|
identifierPath,
|
||||||
emailPath,
|
emailPath,
|
||||||
namePath
|
namePath,
|
||||||
|
variant
|
||||||
};
|
};
|
||||||
|
|
||||||
keysToUpdate = Object.keys(configData).filter(
|
keysToUpdate = Object.keys(configData).filter(
|
||||||
|
|||||||
@@ -448,16 +448,6 @@ export default function GeneralPage() {
|
|||||||
</InfoSection>
|
</InfoSection>
|
||||||
</InfoSections>
|
</InfoSections>
|
||||||
|
|
||||||
<Alert variant="neutral" className="">
|
|
||||||
<InfoIcon className="h-4 w-4" />
|
|
||||||
<AlertTitle className="font-semibold">
|
|
||||||
{t("redirectUrlAbout")}
|
|
||||||
</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
{t("redirectUrlAboutDescription")}
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
{/* IDP Type Indicator */}
|
{/* IDP Type Indicator */}
|
||||||
<div className="flex items-center space-x-2 mb-4">
|
<div className="flex items-center space-x-2 mb-4">
|
||||||
<span className="text-sm font-medium text-muted-foreground">
|
<span className="text-sm font-medium text-muted-foreground">
|
||||||
@@ -843,29 +833,6 @@ export default function GeneralPage() {
|
|||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
id="general-settings-form"
|
id="general-settings-form"
|
||||||
>
|
>
|
||||||
<Alert variant="neutral">
|
|
||||||
<InfoIcon className="h-4 w-4" />
|
|
||||||
<AlertTitle className="font-semibold">
|
|
||||||
{t("idpJmespathAbout")}
|
|
||||||
</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
{t(
|
|
||||||
"idpJmespathAboutDescription"
|
|
||||||
)}{" "}
|
|
||||||
<a
|
|
||||||
href="https://jmespath.org"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-primary hover:underline inline-flex items-center"
|
|
||||||
>
|
|
||||||
{t(
|
|
||||||
"idpJmespathAboutDescriptionLink"
|
|
||||||
)}{" "}
|
|
||||||
<ExternalLink className="ml-1 h-4 w-4" />
|
|
||||||
</a>
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="identifierPath"
|
name="identifierPath"
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
SettingsSectionTitle
|
SettingsSectionTitle
|
||||||
} from "@app/components/Settings";
|
} from "@app/components/Settings";
|
||||||
import HeaderTitle from "@app/components/SettingsSectionTitle";
|
import HeaderTitle from "@app/components/SettingsSectionTitle";
|
||||||
import { StrategySelect } from "@app/components/StrategySelect";
|
import { OidcIdpProviderTypeSelect } from "@app/components/idp/OidcIdpProviderTypeSelect";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
|
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
|
||||||
import { Button } from "@app/components/ui/button";
|
import { Button } from "@app/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -27,17 +27,16 @@ import {
|
|||||||
} from "@app/components/ui/form";
|
} from "@app/components/ui/form";
|
||||||
import { Input } from "@app/components/ui/input";
|
import { Input } from "@app/components/ui/input";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
|
||||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||||
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 { applyOidcIdpProviderType } from "@app/lib/idp/oidcIdpProviderDefaults";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||||
import { ListRolesResponse } from "@server/routers/role";
|
import { ListRolesResponse } from "@server/routers/role";
|
||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
import { InfoIcon } from "lucide-react";
|
import { InfoIcon } from "lucide-react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import Image from "next/image";
|
|
||||||
import { useParams, useRouter } from "next/navigation";
|
import { useParams, useRouter } from "next/navigation";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
@@ -96,49 +95,6 @@ export default function Page() {
|
|||||||
|
|
||||||
type CreateIdpFormValues = z.infer<typeof createIdpFormSchema>;
|
type CreateIdpFormValues = z.infer<typeof createIdpFormSchema>;
|
||||||
|
|
||||||
interface ProviderTypeOption {
|
|
||||||
id: "oidc" | "google" | "azure";
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
icon?: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
const providerTypes: ReadonlyArray<ProviderTypeOption> = [
|
|
||||||
{
|
|
||||||
id: "oidc",
|
|
||||||
title: "OAuth2/OIDC",
|
|
||||||
description: t("idpOidcDescription")
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "google",
|
|
||||||
title: t("idpGoogleTitle"),
|
|
||||||
description: t("idpGoogleDescription"),
|
|
||||||
icon: (
|
|
||||||
<Image
|
|
||||||
src="/idp/google.png"
|
|
||||||
alt={t("idpGoogleAlt")}
|
|
||||||
width={24}
|
|
||||||
height={24}
|
|
||||||
className="rounded"
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "azure",
|
|
||||||
title: t("idpAzureTitle"),
|
|
||||||
description: t("idpAzureDescription"),
|
|
||||||
icon: (
|
|
||||||
<Image
|
|
||||||
src="/idp/azure.png"
|
|
||||||
alt={t("idpAzureAlt")}
|
|
||||||
width={24}
|
|
||||||
height={24}
|
|
||||||
className="rounded"
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
resolver: zodResolver(createIdpFormSchema),
|
resolver: zodResolver(createIdpFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -186,47 +142,6 @@ export default function Page() {
|
|||||||
fetchRoles();
|
fetchRoles();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Handle provider type changes and set defaults
|
|
||||||
const handleProviderChange = (value: "oidc" | "google" | "azure") => {
|
|
||||||
form.setValue("type", value);
|
|
||||||
|
|
||||||
if (value === "google") {
|
|
||||||
// Set Google defaults
|
|
||||||
form.setValue(
|
|
||||||
"authUrl",
|
|
||||||
"https://accounts.google.com/o/oauth2/v2/auth"
|
|
||||||
);
|
|
||||||
form.setValue("tokenUrl", "https://oauth2.googleapis.com/token");
|
|
||||||
form.setValue("identifierPath", "email");
|
|
||||||
form.setValue("emailPath", "email");
|
|
||||||
form.setValue("namePath", "name");
|
|
||||||
form.setValue("scopes", "openid profile email");
|
|
||||||
} else if (value === "azure") {
|
|
||||||
// Set Azure Entra ID defaults (URLs will be constructed dynamically)
|
|
||||||
form.setValue(
|
|
||||||
"authUrl",
|
|
||||||
"https://login.microsoftonline.com/{{TENANT_ID}}/oauth2/v2.0/authorize"
|
|
||||||
);
|
|
||||||
form.setValue(
|
|
||||||
"tokenUrl",
|
|
||||||
"https://login.microsoftonline.com/{{TENANT_ID}}/oauth2/v2.0/token"
|
|
||||||
);
|
|
||||||
form.setValue("identifierPath", "email");
|
|
||||||
form.setValue("emailPath", "email");
|
|
||||||
form.setValue("namePath", "name");
|
|
||||||
form.setValue("scopes", "openid profile email");
|
|
||||||
form.setValue("tenantId", "");
|
|
||||||
} else {
|
|
||||||
// Reset to OIDC defaults
|
|
||||||
form.setValue("authUrl", "");
|
|
||||||
form.setValue("tokenUrl", "");
|
|
||||||
form.setValue("identifierPath", "sub");
|
|
||||||
form.setValue("namePath", "name");
|
|
||||||
form.setValue("emailPath", "email");
|
|
||||||
form.setValue("scopes", "openid profile email");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
async function onSubmit(data: CreateIdpFormValues) {
|
async function onSubmit(data: CreateIdpFormValues) {
|
||||||
setCreateLoading(true);
|
setCreateLoading(true);
|
||||||
|
|
||||||
@@ -304,6 +219,7 @@ export default function Page() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const disabled = !isPaidUser(tierMatrix.orgOidc);
|
const disabled = !isPaidUser(tierMatrix.orgOidc);
|
||||||
|
const templatesPaid = isPaidUser(tierMatrix.orgOidc);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -336,23 +252,13 @@ export default function Page() {
|
|||||||
</SettingsSectionDescription>
|
</SettingsSectionDescription>
|
||||||
</SettingsSectionHeader>
|
</SettingsSectionHeader>
|
||||||
<SettingsSectionBody>
|
<SettingsSectionBody>
|
||||||
<div>
|
<OidcIdpProviderTypeSelect
|
||||||
<div className="mb-2">
|
value={form.watch("type")}
|
||||||
<span className="text-sm font-medium">
|
templatesPaid={templatesPaid}
|
||||||
{t("idpType")}
|
onTypeChange={(next) => {
|
||||||
</span>
|
applyOidcIdpProviderType(form.setValue, next);
|
||||||
</div>
|
}}
|
||||||
<StrategySelect
|
/>
|
||||||
options={providerTypes}
|
|
||||||
defaultValue={form.getValues("type")}
|
|
||||||
onChange={(value) => {
|
|
||||||
handleProviderChange(
|
|
||||||
value as "oidc" | "google" | "azure"
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
cols={3}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<SettingsSectionForm>
|
<SettingsSectionForm>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
@@ -708,16 +614,6 @@ export default function Page() {
|
|||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
<Alert variant="neutral">
|
|
||||||
<InfoIcon className="h-4 w-4" />
|
|
||||||
<AlertTitle className="font-semibold">
|
|
||||||
{t("idpOidcConfigureAlert")}
|
|
||||||
</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
{t("idpOidcConfigureAlertDescription")}
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
</SettingsSectionBody>
|
</SettingsSectionBody>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import {
|
|||||||
SettingsSectionDescription,
|
SettingsSectionDescription,
|
||||||
SettingsSectionBody,
|
SettingsSectionBody,
|
||||||
SettingsSectionForm,
|
SettingsSectionForm,
|
||||||
SettingsSectionFooter,
|
|
||||||
SettingsSectionGrid
|
SettingsSectionGrid
|
||||||
} from "@app/components/Settings";
|
} from "@app/components/Settings";
|
||||||
import { formatAxiosError } from "@app/lib/api";
|
import { formatAxiosError } from "@app/lib/api";
|
||||||
@@ -33,8 +32,6 @@ import { createApiClient } from "@app/lib/api";
|
|||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { SwitchInput } from "@app/components/SwitchInput";
|
import { SwitchInput } from "@app/components/SwitchInput";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
|
|
||||||
import { InfoIcon, ExternalLink } from "lucide-react";
|
|
||||||
import {
|
import {
|
||||||
InfoSection,
|
InfoSection,
|
||||||
InfoSectionContent,
|
InfoSectionContent,
|
||||||
@@ -42,8 +39,7 @@ import {
|
|||||||
InfoSectionTitle
|
InfoSectionTitle
|
||||||
} from "@app/components/InfoSection";
|
} from "@app/components/InfoSection";
|
||||||
import CopyToClipboard from "@app/components/CopyToClipboard";
|
import CopyToClipboard from "@app/components/CopyToClipboard";
|
||||||
import { Badge } from "@app/components/ui/badge";
|
import IdpTypeBadge from "@app/components/IdpTypeBadge";
|
||||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
export default function GeneralPage() {
|
export default function GeneralPage() {
|
||||||
@@ -53,12 +49,12 @@ export default function GeneralPage() {
|
|||||||
const { idpId } = useParams();
|
const { idpId } = useParams();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [initialLoading, setInitialLoading] = useState(true);
|
const [initialLoading, setInitialLoading] = useState(true);
|
||||||
const { isUnlocked } = useLicenseStatusContext();
|
const [variant, setVariant] = useState<"oidc" | "google" | "azure">("oidc");
|
||||||
|
|
||||||
const redirectUrl = `${env.app.dashboardUrl}/auth/idp/${idpId}/oidc/callback`;
|
const redirectUrl = `${env.app.dashboardUrl}/auth/idp/${idpId}/oidc/callback`;
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
|
||||||
const GeneralFormSchema = z.object({
|
const OidcFormSchema = z.object({
|
||||||
name: z.string().min(2, { message: t("nameMin", { len: 2 }) }),
|
name: z.string().min(2, { message: t("nameMin", { len: 2 }) }),
|
||||||
clientId: z.string().min(1, { message: t("idpClientIdRequired") }),
|
clientId: z.string().min(1, { message: t("idpClientIdRequired") }),
|
||||||
clientSecret: z
|
clientSecret: z
|
||||||
@@ -73,10 +69,46 @@ export default function GeneralPage() {
|
|||||||
autoProvision: z.boolean().default(false)
|
autoProvision: z.boolean().default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
|
const GoogleFormSchema = z.object({
|
||||||
|
name: z.string().min(2, { message: t("nameMin", { len: 2 }) }),
|
||||||
|
clientId: z.string().min(1, { message: t("idpClientIdRequired") }),
|
||||||
|
clientSecret: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: t("idpClientSecretRequired") }),
|
||||||
|
autoProvision: z.boolean().default(false)
|
||||||
|
});
|
||||||
|
|
||||||
const form = useForm({
|
const AzureFormSchema = z.object({
|
||||||
resolver: zodResolver(GeneralFormSchema),
|
name: z.string().min(2, { message: t("nameMin", { len: 2 }) }),
|
||||||
|
clientId: z.string().min(1, { message: t("idpClientIdRequired") }),
|
||||||
|
clientSecret: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: t("idpClientSecretRequired") }),
|
||||||
|
tenantId: z.string().min(1, { message: t("idpTenantIdRequired") }),
|
||||||
|
autoProvision: z.boolean().default(false)
|
||||||
|
});
|
||||||
|
|
||||||
|
type OidcFormValues = z.infer<typeof OidcFormSchema>;
|
||||||
|
type GoogleFormValues = z.infer<typeof GoogleFormSchema>;
|
||||||
|
type AzureFormValues = z.infer<typeof AzureFormSchema>;
|
||||||
|
type GeneralFormValues =
|
||||||
|
| OidcFormValues
|
||||||
|
| GoogleFormValues
|
||||||
|
| AzureFormValues;
|
||||||
|
|
||||||
|
const getFormSchema = () => {
|
||||||
|
switch (variant) {
|
||||||
|
case "google":
|
||||||
|
return GoogleFormSchema;
|
||||||
|
case "azure":
|
||||||
|
return AzureFormSchema;
|
||||||
|
default:
|
||||||
|
return OidcFormSchema;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const form = useForm<GeneralFormValues>({
|
||||||
|
resolver: zodResolver(getFormSchema()) as never,
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: "",
|
name: "",
|
||||||
clientId: "",
|
clientId: "",
|
||||||
@@ -87,28 +119,60 @@ export default function GeneralPage() {
|
|||||||
emailPath: "email",
|
emailPath: "email",
|
||||||
namePath: "name",
|
namePath: "name",
|
||||||
scopes: "openid profile email",
|
scopes: "openid profile email",
|
||||||
autoProvision: true
|
autoProvision: true,
|
||||||
|
tenantId: ""
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
form.clearErrors();
|
||||||
|
}, [variant, form]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadIdp = async () => {
|
const loadIdp = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await api.get(`/idp/${idpId}`);
|
const res = await api.get(`/idp/${idpId}`);
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
const data = res.data.data;
|
const data = res.data.data;
|
||||||
form.reset({
|
const idpVariant =
|
||||||
|
(data.idpOidcConfig?.variant as
|
||||||
|
| "oidc"
|
||||||
|
| "google"
|
||||||
|
| "azure") || "oidc";
|
||||||
|
setVariant(idpVariant);
|
||||||
|
|
||||||
|
let tenantId = "";
|
||||||
|
if (idpVariant === "azure" && data.idpOidcConfig?.authUrl) {
|
||||||
|
const tenantMatch = data.idpOidcConfig.authUrl.match(
|
||||||
|
/login\.microsoftonline\.com\/([^/]+)\/oauth2/
|
||||||
|
);
|
||||||
|
if (tenantMatch) {
|
||||||
|
tenantId = tenantMatch[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData: Record<string, unknown> = {
|
||||||
name: data.idp.name,
|
name: data.idp.name,
|
||||||
clientId: data.idpOidcConfig.clientId,
|
clientId: data.idpOidcConfig.clientId,
|
||||||
clientSecret: data.idpOidcConfig.clientSecret,
|
clientSecret: data.idpOidcConfig.clientSecret,
|
||||||
authUrl: data.idpOidcConfig.authUrl,
|
|
||||||
tokenUrl: data.idpOidcConfig.tokenUrl,
|
|
||||||
identifierPath: data.idpOidcConfig.identifierPath,
|
|
||||||
emailPath: data.idpOidcConfig.emailPath,
|
|
||||||
namePath: data.idpOidcConfig.namePath,
|
|
||||||
scopes: data.idpOidcConfig.scopes,
|
|
||||||
autoProvision: data.idp.autoProvision
|
autoProvision: data.idp.autoProvision
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (idpVariant === "oidc") {
|
||||||
|
formData.authUrl = data.idpOidcConfig.authUrl;
|
||||||
|
formData.tokenUrl = data.idpOidcConfig.tokenUrl;
|
||||||
|
formData.identifierPath =
|
||||||
|
data.idpOidcConfig.identifierPath;
|
||||||
|
formData.emailPath =
|
||||||
|
data.idpOidcConfig.emailPath ?? undefined;
|
||||||
|
formData.namePath =
|
||||||
|
data.idpOidcConfig.namePath ?? undefined;
|
||||||
|
formData.scopes = data.idpOidcConfig.scopes;
|
||||||
|
} else if (idpVariant === "azure") {
|
||||||
|
formData.tenantId = tenantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
form.reset(formData as GeneralFormValues);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast({
|
toast({
|
||||||
@@ -123,25 +187,76 @@ export default function GeneralPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
loadIdp();
|
loadIdp();
|
||||||
}, [idpId, api, form, router]);
|
}, [idpId]);
|
||||||
|
|
||||||
async function onSubmit(data: GeneralFormValues) {
|
async function onSubmit(data: GeneralFormValues) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const payload = {
|
const schema = getFormSchema();
|
||||||
|
const validationResult = schema.safeParse(data);
|
||||||
|
|
||||||
|
if (!validationResult.success) {
|
||||||
|
const errors = validationResult.error.flatten().fieldErrors;
|
||||||
|
Object.keys(errors).forEach((key) => {
|
||||||
|
const fieldName = key as keyof GeneralFormValues;
|
||||||
|
const errorMessage =
|
||||||
|
(errors as Record<string, string[] | undefined>)[
|
||||||
|
key
|
||||||
|
]?.[0] || t("invalidValue");
|
||||||
|
form.setError(fieldName, {
|
||||||
|
type: "manual",
|
||||||
|
message: errorMessage
|
||||||
|
});
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let payload: Record<string, unknown> = {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
clientId: data.clientId,
|
clientId: data.clientId,
|
||||||
clientSecret: data.clientSecret,
|
clientSecret: data.clientSecret,
|
||||||
authUrl: data.authUrl,
|
|
||||||
tokenUrl: data.tokenUrl,
|
|
||||||
identifierPath: data.identifierPath,
|
|
||||||
emailPath: data.emailPath,
|
|
||||||
namePath: data.namePath,
|
|
||||||
autoProvision: data.autoProvision,
|
autoProvision: data.autoProvision,
|
||||||
scopes: data.scopes
|
variant
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (variant === "oidc") {
|
||||||
|
const oidcData = data as OidcFormValues;
|
||||||
|
payload = {
|
||||||
|
...payload,
|
||||||
|
authUrl: oidcData.authUrl,
|
||||||
|
tokenUrl: oidcData.tokenUrl,
|
||||||
|
identifierPath: oidcData.identifierPath,
|
||||||
|
emailPath: oidcData.emailPath ?? "",
|
||||||
|
namePath: oidcData.namePath ?? "",
|
||||||
|
scopes: oidcData.scopes
|
||||||
|
};
|
||||||
|
} else if (variant === "azure") {
|
||||||
|
const azureData = data as AzureFormValues;
|
||||||
|
const authUrl = `https://login.microsoftonline.com/${azureData.tenantId}/oauth2/v2.0/authorize`;
|
||||||
|
const tokenUrl = `https://login.microsoftonline.com/${azureData.tenantId}/oauth2/v2.0/token`;
|
||||||
|
payload = {
|
||||||
|
...payload,
|
||||||
|
authUrl,
|
||||||
|
tokenUrl,
|
||||||
|
identifierPath: "email",
|
||||||
|
emailPath: "email",
|
||||||
|
namePath: "name",
|
||||||
|
scopes: "openid profile email"
|
||||||
|
};
|
||||||
|
} else if (variant === "google") {
|
||||||
|
payload = {
|
||||||
|
...payload,
|
||||||
|
authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
||||||
|
tokenUrl: "https://oauth2.googleapis.com/token",
|
||||||
|
identifierPath: "email",
|
||||||
|
emailPath: "email",
|
||||||
|
namePath: "name",
|
||||||
|
scopes: "openid profile email"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const res = await api.post(`/idp/${idpId}/oidc`, payload);
|
const res = await api.post(`/idp/${idpId}/oidc`, payload);
|
||||||
|
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
@@ -190,6 +305,13 @@ export default function GeneralPage() {
|
|||||||
</InfoSection>
|
</InfoSection>
|
||||||
</InfoSections>
|
</InfoSections>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2 mb-4">
|
||||||
|
<span className="text-sm font-medium text-muted-foreground">
|
||||||
|
{t("idpTypeLabel")}:
|
||||||
|
</span>
|
||||||
|
<IdpTypeBadge type={variant} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<SettingsSectionForm>
|
<SettingsSectionForm>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
@@ -215,62 +337,80 @@ export default function GeneralPage() {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<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>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
{t(
|
|
||||||
"idpAutoProvisionUsersDescription"
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
{form.watch("autoProvision") && (
|
|
||||||
<FormDescription>
|
|
||||||
{t.rich(
|
|
||||||
"idpAdminAutoProvisionPoliciesTabHint",
|
|
||||||
{
|
|
||||||
policiesTabLink: (
|
|
||||||
chunks
|
|
||||||
) => (
|
|
||||||
<Link
|
|
||||||
href={`/admin/idp/${idpId}/policies`}
|
|
||||||
className="text-primary hover:underline inline-flex items-center gap-1"
|
|
||||||
>
|
|
||||||
{chunks}
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</FormDescription>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</SettingsSectionForm>
|
</SettingsSectionForm>
|
||||||
</SettingsSectionBody>
|
</SettingsSectionBody>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
|
||||||
<SettingsSectionGrid cols={2}>
|
<SettingsSection>
|
||||||
|
<SettingsSectionHeader>
|
||||||
|
<SettingsSectionTitle>
|
||||||
|
{t("idpAutoProvisionUsers")}
|
||||||
|
</SettingsSectionTitle>
|
||||||
|
<SettingsSectionDescription>
|
||||||
|
{t("idpAutoProvisionUsersDescription")}
|
||||||
|
</SettingsSectionDescription>
|
||||||
|
</SettingsSectionHeader>
|
||||||
|
<SettingsSectionBody>
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="space-y-4"
|
||||||
|
id="general-settings-form"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{t("idpAutoProvisionUsersDescription")}
|
||||||
|
</span>
|
||||||
|
{form.watch("autoProvision") && (
|
||||||
|
<FormDescription>
|
||||||
|
{t.rich(
|
||||||
|
"idpAdminAutoProvisionPoliciesTabHint",
|
||||||
|
{
|
||||||
|
policiesTabLink: (
|
||||||
|
chunks
|
||||||
|
) => (
|
||||||
|
<Link
|
||||||
|
href={`/admin/idp/${idpId}/policies`}
|
||||||
|
className="text-primary hover:underline inline-flex items-center gap-1"
|
||||||
|
>
|
||||||
|
{chunks}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</SettingsSectionBody>
|
||||||
|
</SettingsSection>
|
||||||
|
|
||||||
|
{variant === "google" && (
|
||||||
<SettingsSection>
|
<SettingsSection>
|
||||||
<SettingsSectionHeader>
|
<SettingsSectionHeader>
|
||||||
<SettingsSectionTitle>
|
<SettingsSectionTitle>
|
||||||
{t("idpOidcConfigure")}
|
{t("idpGoogleConfiguration")}
|
||||||
</SettingsSectionTitle>
|
</SettingsSectionTitle>
|
||||||
<SettingsSectionDescription>
|
<SettingsSectionDescription>
|
||||||
{t("idpOidcConfigureDescription")}
|
{t("idpGoogleConfigurationDescription")}
|
||||||
</SettingsSectionDescription>
|
</SettingsSectionDescription>
|
||||||
</SettingsSectionHeader>
|
</SettingsSectionHeader>
|
||||||
<SettingsSectionBody>
|
<SettingsSectionBody>
|
||||||
@@ -294,7 +434,7 @@ export default function GeneralPage() {
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
{t(
|
{t(
|
||||||
"idpClientIdDescription"
|
"idpGoogleClientIdDescription"
|
||||||
)}
|
)}
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -318,49 +458,7 @@ export default function GeneralPage() {
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
{t(
|
{t(
|
||||||
"idpClientSecretDescription"
|
"idpGoogleClientSecretDescription"
|
||||||
)}
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="authUrl"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
{t("idpAuthUrl")}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
{t(
|
|
||||||
"idpAuthUrlDescription"
|
|
||||||
)}
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="tokenUrl"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
{t("idpTokenUrl")}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
{t(
|
|
||||||
"idpTokenUrlDescription"
|
|
||||||
)}
|
)}
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -372,14 +470,16 @@ export default function GeneralPage() {
|
|||||||
</SettingsSectionForm>
|
</SettingsSectionForm>
|
||||||
</SettingsSectionBody>
|
</SettingsSectionBody>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{variant === "azure" && (
|
||||||
<SettingsSection>
|
<SettingsSection>
|
||||||
<SettingsSectionHeader>
|
<SettingsSectionHeader>
|
||||||
<SettingsSectionTitle>
|
<SettingsSectionTitle>
|
||||||
{t("idpToken")}
|
{t("idpAzureConfiguration")}
|
||||||
</SettingsSectionTitle>
|
</SettingsSectionTitle>
|
||||||
<SettingsSectionDescription>
|
<SettingsSectionDescription>
|
||||||
{t("idpTokenDescription")}
|
{t("idpAzureConfigurationDescription")}
|
||||||
</SettingsSectionDescription>
|
</SettingsSectionDescription>
|
||||||
</SettingsSectionHeader>
|
</SettingsSectionHeader>
|
||||||
<SettingsSectionBody>
|
<SettingsSectionBody>
|
||||||
@@ -392,18 +492,18 @@ export default function GeneralPage() {
|
|||||||
>
|
>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="identifierPath"
|
name="tenantId"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{t("idpJmespathLabel")}
|
{t("idpTenantId")}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} />
|
<Input {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
{t(
|
{t(
|
||||||
"idpJmespathLabelDescription"
|
"idpAzureTenantIdDescription"
|
||||||
)}
|
)}
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -413,20 +513,18 @@ export default function GeneralPage() {
|
|||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="emailPath"
|
name="clientId"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{t(
|
{t("idpClientId")}
|
||||||
"idpJmespathEmailPathOptional"
|
|
||||||
)}
|
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} />
|
<Input {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
{t(
|
{t(
|
||||||
"idpJmespathEmailPathOptionalDescription"
|
"idpAzureClientIdDescription"
|
||||||
)}
|
)}
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -436,43 +534,21 @@ export default function GeneralPage() {
|
|||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="namePath"
|
name="clientSecret"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{t(
|
{t("idpClientSecret")}
|
||||||
"idpJmespathNamePathOptional"
|
|
||||||
)}
|
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} />
|
<Input
|
||||||
|
type="password"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
{t(
|
{t(
|
||||||
"idpJmespathNamePathOptionalDescription"
|
"idpAzureClientSecretDescription"
|
||||||
)}
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="scopes"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
{t(
|
|
||||||
"idpOidcConfigureScopes"
|
|
||||||
)}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
{t(
|
|
||||||
"idpOidcConfigureScopesDescription"
|
|
||||||
)}
|
)}
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -484,15 +560,263 @@ export default function GeneralPage() {
|
|||||||
</SettingsSectionForm>
|
</SettingsSectionForm>
|
||||||
</SettingsSectionBody>
|
</SettingsSectionBody>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
</SettingsSectionGrid>
|
)}
|
||||||
|
|
||||||
|
{variant === "oidc" && (
|
||||||
|
<SettingsSectionGrid cols={2}>
|
||||||
|
<SettingsSection>
|
||||||
|
<SettingsSectionHeader>
|
||||||
|
<SettingsSectionTitle>
|
||||||
|
{t("idpOidcConfigure")}
|
||||||
|
</SettingsSectionTitle>
|
||||||
|
<SettingsSectionDescription>
|
||||||
|
{t("idpOidcConfigureDescription")}
|
||||||
|
</SettingsSectionDescription>
|
||||||
|
</SettingsSectionHeader>
|
||||||
|
<SettingsSectionBody>
|
||||||
|
<SettingsSectionForm>
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(
|
||||||
|
onSubmit
|
||||||
|
)}
|
||||||
|
className="space-y-4"
|
||||||
|
id="general-settings-form"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="clientId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("idpClientId")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{t(
|
||||||
|
"idpClientIdDescription"
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="clientSecret"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t(
|
||||||
|
"idpClientSecret"
|
||||||
|
)}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{t(
|
||||||
|
"idpClientSecretDescription"
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="authUrl"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("idpAuthUrl")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{t(
|
||||||
|
"idpAuthUrlDescription"
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="tokenUrl"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("idpTokenUrl")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{t(
|
||||||
|
"idpTokenUrlDescription"
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</SettingsSectionForm>
|
||||||
|
</SettingsSectionBody>
|
||||||
|
</SettingsSection>
|
||||||
|
|
||||||
|
<SettingsSection>
|
||||||
|
<SettingsSectionHeader>
|
||||||
|
<SettingsSectionTitle>
|
||||||
|
{t("idpToken")}
|
||||||
|
</SettingsSectionTitle>
|
||||||
|
<SettingsSectionDescription>
|
||||||
|
{t("idpTokenDescription")}
|
||||||
|
</SettingsSectionDescription>
|
||||||
|
</SettingsSectionHeader>
|
||||||
|
<SettingsSectionBody>
|
||||||
|
<SettingsSectionForm>
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(
|
||||||
|
onSubmit
|
||||||
|
)}
|
||||||
|
className="space-y-4"
|
||||||
|
id="general-settings-form"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="identifierPath"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t(
|
||||||
|
"idpJmespathLabel"
|
||||||
|
)}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{t(
|
||||||
|
"idpJmespathLabelDescription"
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="emailPath"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t(
|
||||||
|
"idpJmespathEmailPathOptional"
|
||||||
|
)}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={
|
||||||
|
field.value ||
|
||||||
|
""
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{t(
|
||||||
|
"idpJmespathEmailPathOptionalDescription"
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="namePath"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t(
|
||||||
|
"idpJmespathNamePathOptional"
|
||||||
|
)}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
value={
|
||||||
|
field.value ||
|
||||||
|
""
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{t(
|
||||||
|
"idpJmespathNamePathOptionalDescription"
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="scopes"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t(
|
||||||
|
"idpOidcConfigureScopes"
|
||||||
|
)}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{t(
|
||||||
|
"idpOidcConfigureScopesDescription"
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</SettingsSectionForm>
|
||||||
|
</SettingsSectionBody>
|
||||||
|
</SettingsSection>
|
||||||
|
</SettingsSectionGrid>
|
||||||
|
)}
|
||||||
</SettingsContainer>
|
</SettingsContainer>
|
||||||
|
|
||||||
<div className="flex justify-end mt-8">
|
<div className="flex justify-end mt-8">
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="button"
|
||||||
form="general-settings-form"
|
form="general-settings-form"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
|
onClick={() => {
|
||||||
|
form.handleSubmit(onSubmit)();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{t("saveGeneralSettings")}
|
{t("saveGeneralSettings")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -575,7 +575,7 @@ export default function PoliciesPage() {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CredenzaContent className="max-w-4xl w-[calc(100vw-2rem)] sm:w-full">
|
<CredenzaContent className="max-w-4xl sm:w-full">
|
||||||
<CredenzaHeader>
|
<CredenzaHeader>
|
||||||
<CredenzaTitle>
|
<CredenzaTitle>
|
||||||
{editingPolicy
|
{editingPolicy
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { OidcIdpProviderTypeSelect } from "@app/components/idp/OidcIdpProviderTypeSelect";
|
||||||
|
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
||||||
import {
|
import {
|
||||||
SettingsContainer,
|
SettingsContainer,
|
||||||
SettingsSection,
|
SettingsSection,
|
||||||
@@ -20,70 +22,63 @@ import {
|
|||||||
FormMessage
|
FormMessage
|
||||||
} from "@app/components/ui/form";
|
} from "@app/components/ui/form";
|
||||||
import HeaderTitle from "@app/components/SettingsSectionTitle";
|
import HeaderTitle from "@app/components/SettingsSectionTitle";
|
||||||
import { z } from "zod";
|
import { SwitchInput } from "@app/components/SwitchInput";
|
||||||
import { createElement, useEffect, useState } from "react";
|
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { Input } from "@app/components/ui/input";
|
|
||||||
import { Button } from "@app/components/ui/button";
|
import { Button } from "@app/components/ui/button";
|
||||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
import { Input } from "@app/components/ui/input";
|
||||||
|
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
import { useRouter } from "next/navigation";
|
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||||
import { Checkbox } from "@app/components/ui/checkbox";
|
import { applyOidcIdpProviderType } from "@app/lib/idp/oidcIdpProviderDefaults";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { InfoIcon, ExternalLink } from "lucide-react";
|
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||||
import { StrategySelect } from "@app/components/StrategySelect";
|
import { InfoIcon } from "lucide-react";
|
||||||
import { SwitchInput } from "@app/components/SwitchInput";
|
|
||||||
import { Badge } from "@app/components/ui/badge";
|
|
||||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const { env } = useEnvContext();
|
const { env } = useEnvContext();
|
||||||
const api = createApiClient({ env });
|
const api = createApiClient({ env });
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [createLoading, setCreateLoading] = useState(false);
|
const [createLoading, setCreateLoading] = useState(false);
|
||||||
const { isUnlocked } = useLicenseStatusContext();
|
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
const { isPaidUser } = usePaidStatus();
|
||||||
|
const templatesPaid = isPaidUser(tierMatrix.orgOidc);
|
||||||
|
|
||||||
const createIdpFormSchema = z.object({
|
const createIdpFormSchema = z.object({
|
||||||
name: z.string().min(2, { message: t("nameMin", { len: 2 }) }),
|
name: z.string().min(2, { message: t("nameMin", { len: 2 }) }),
|
||||||
type: z.enum(["oidc"]),
|
type: z.enum(["oidc", "google", "azure"]),
|
||||||
clientId: z.string().min(1, { message: t("idpClientIdRequired") }),
|
clientId: z.string().min(1, { message: t("idpClientIdRequired") }),
|
||||||
clientSecret: z
|
clientSecret: z
|
||||||
.string()
|
.string()
|
||||||
.min(1, { message: t("idpClientSecretRequired") }),
|
.min(1, { message: t("idpClientSecretRequired") }),
|
||||||
authUrl: z.url({ message: t("idpErrorAuthUrlInvalid") }),
|
authUrl: z.url({ message: t("idpErrorAuthUrlInvalid") }).optional(),
|
||||||
tokenUrl: z.url({ message: t("idpErrorTokenUrlInvalid") }),
|
tokenUrl: z.url({ message: t("idpErrorTokenUrlInvalid") }).optional(),
|
||||||
identifierPath: z.string().min(1, { message: t("idpPathRequired") }),
|
identifierPath: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: t("idpPathRequired") })
|
||||||
|
.optional(),
|
||||||
emailPath: z.string().optional(),
|
emailPath: z.string().optional(),
|
||||||
namePath: z.string().optional(),
|
namePath: z.string().optional(),
|
||||||
scopes: z.string().min(1, { message: t("idpScopeRequired") }),
|
scopes: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: t("idpScopeRequired") })
|
||||||
|
.optional(),
|
||||||
|
tenantId: z.string().optional(),
|
||||||
autoProvision: z.boolean().default(false)
|
autoProvision: z.boolean().default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
type CreateIdpFormValues = z.infer<typeof createIdpFormSchema>;
|
type CreateIdpFormValues = z.infer<typeof createIdpFormSchema>;
|
||||||
|
|
||||||
interface ProviderTypeOption {
|
|
||||||
id: "oidc";
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const providerTypes: ReadonlyArray<ProviderTypeOption> = [
|
|
||||||
{
|
|
||||||
id: "oidc",
|
|
||||||
title: "OAuth2/OIDC",
|
|
||||||
description: t("idpOidcDescription")
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
resolver: zodResolver(createIdpFormSchema),
|
resolver: zodResolver(createIdpFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: "",
|
name: "",
|
||||||
type: "oidc",
|
type: "oidc" as const,
|
||||||
clientId: "",
|
clientId: "",
|
||||||
clientSecret: "",
|
clientSecret: "",
|
||||||
authUrl: "",
|
authUrl: "",
|
||||||
@@ -92,25 +87,46 @@ export default function Page() {
|
|||||||
namePath: "name",
|
namePath: "name",
|
||||||
emailPath: "email",
|
emailPath: "email",
|
||||||
scopes: "openid profile email",
|
scopes: "openid profile email",
|
||||||
|
tenantId: "",
|
||||||
autoProvision: false
|
autoProvision: false
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const watchedType = form.watch("type");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
!templatesPaid &&
|
||||||
|
(watchedType === "google" || watchedType === "azure")
|
||||||
|
) {
|
||||||
|
applyOidcIdpProviderType(form.setValue, "oidc");
|
||||||
|
}
|
||||||
|
}, [templatesPaid, watchedType, form.setValue]);
|
||||||
|
|
||||||
async function onSubmit(data: CreateIdpFormValues) {
|
async function onSubmit(data: CreateIdpFormValues) {
|
||||||
setCreateLoading(true);
|
setCreateLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
let authUrl = data.authUrl;
|
||||||
|
let tokenUrl = data.tokenUrl;
|
||||||
|
|
||||||
|
if (data.type === "azure" && data.tenantId) {
|
||||||
|
authUrl = authUrl?.replace("{{TENANT_ID}}", data.tenantId);
|
||||||
|
tokenUrl = tokenUrl?.replace("{{TENANT_ID}}", data.tenantId);
|
||||||
|
}
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
clientId: data.clientId,
|
clientId: data.clientId,
|
||||||
clientSecret: data.clientSecret,
|
clientSecret: data.clientSecret,
|
||||||
authUrl: data.authUrl,
|
authUrl: authUrl,
|
||||||
tokenUrl: data.tokenUrl,
|
tokenUrl: tokenUrl,
|
||||||
identifierPath: data.identifierPath,
|
identifierPath: data.identifierPath,
|
||||||
emailPath: data.emailPath,
|
emailPath: data.emailPath,
|
||||||
namePath: data.namePath,
|
namePath: data.namePath,
|
||||||
autoProvision: data.autoProvision,
|
autoProvision: data.autoProvision,
|
||||||
scopes: data.scopes
|
scopes: data.scopes,
|
||||||
|
variant: data.type
|
||||||
};
|
};
|
||||||
|
|
||||||
const res = await api.put("/idp/oidc", payload);
|
const res = await api.put("/idp/oidc", payload);
|
||||||
@@ -150,6 +166,10 @@ export default function Page() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{!templatesPaid ? (
|
||||||
|
<PaidFeaturesAlert tiers={tierMatrix.orgOidc} />
|
||||||
|
) : null}
|
||||||
|
|
||||||
<SettingsContainer>
|
<SettingsContainer>
|
||||||
<SettingsSection>
|
<SettingsSection>
|
||||||
<SettingsSectionHeader>
|
<SettingsSectionHeader>
|
||||||
@@ -161,6 +181,14 @@ export default function Page() {
|
|||||||
</SettingsSectionDescription>
|
</SettingsSectionDescription>
|
||||||
</SettingsSectionHeader>
|
</SettingsSectionHeader>
|
||||||
<SettingsSectionBody>
|
<SettingsSectionBody>
|
||||||
|
<OidcIdpProviderTypeSelect
|
||||||
|
value={watchedType}
|
||||||
|
templatesPaid={templatesPaid}
|
||||||
|
onTypeChange={(next) => {
|
||||||
|
applyOidcIdpProviderType(form.setValue, next);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<SettingsSectionForm>
|
<SettingsSectionForm>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
@@ -208,27 +236,169 @@ export default function Page() {
|
|||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</SettingsSectionForm>
|
</SettingsSectionForm>
|
||||||
|
|
||||||
{/* <div> */}
|
|
||||||
{/* <div className="mb-2"> */}
|
|
||||||
{/* <span className="text-sm font-medium"> */}
|
|
||||||
{/* {t("idpType")} */}
|
|
||||||
{/* </span> */}
|
|
||||||
{/* </div> */}
|
|
||||||
{/* */}
|
|
||||||
{/* <StrategySelect */}
|
|
||||||
{/* options={providerTypes} */}
|
|
||||||
{/* defaultValue={form.getValues("type")} */}
|
|
||||||
{/* onChange={(value) => { */}
|
|
||||||
{/* form.setValue("type", value as "oidc"); */}
|
|
||||||
{/* }} */}
|
|
||||||
{/* cols={3} */}
|
|
||||||
{/* /> */}
|
|
||||||
{/* </div> */}
|
|
||||||
</SettingsSectionBody>
|
</SettingsSectionBody>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
|
||||||
{form.watch("type") === "oidc" && (
|
{watchedType === "google" && (
|
||||||
|
<SettingsSection>
|
||||||
|
<SettingsSectionHeader>
|
||||||
|
<SettingsSectionTitle>
|
||||||
|
{t("idpGoogleConfigurationTitle")}
|
||||||
|
</SettingsSectionTitle>
|
||||||
|
<SettingsSectionDescription>
|
||||||
|
{t("idpGoogleConfigurationDescription")}
|
||||||
|
</SettingsSectionDescription>
|
||||||
|
</SettingsSectionHeader>
|
||||||
|
<SettingsSectionBody>
|
||||||
|
<SettingsSectionForm>
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
className="space-y-4"
|
||||||
|
id="create-idp-form"
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="clientId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("idpClientId")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{t(
|
||||||
|
"idpGoogleClientIdDescription"
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="clientSecret"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("idpClientSecret")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{t(
|
||||||
|
"idpGoogleClientSecretDescription"
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</SettingsSectionForm>
|
||||||
|
</SettingsSectionBody>
|
||||||
|
</SettingsSection>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{watchedType === "azure" && (
|
||||||
|
<SettingsSection>
|
||||||
|
<SettingsSectionHeader>
|
||||||
|
<SettingsSectionTitle>
|
||||||
|
{t("idpAzureConfigurationTitle")}
|
||||||
|
</SettingsSectionTitle>
|
||||||
|
<SettingsSectionDescription>
|
||||||
|
{t("idpAzureConfigurationDescription")}
|
||||||
|
</SettingsSectionDescription>
|
||||||
|
</SettingsSectionHeader>
|
||||||
|
<SettingsSectionBody>
|
||||||
|
<SettingsSectionForm>
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
className="space-y-4"
|
||||||
|
id="create-idp-form"
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="tenantId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("idpTenantIdLabel")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{t(
|
||||||
|
"idpAzureTenantIdDescription"
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="clientId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("idpClientId")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{t(
|
||||||
|
"idpAzureClientIdDescription2"
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="clientSecret"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("idpClientSecret")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{t(
|
||||||
|
"idpAzureClientSecretDescription2"
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</SettingsSectionForm>
|
||||||
|
</SettingsSectionBody>
|
||||||
|
</SettingsSection>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{watchedType === "oidc" && (
|
||||||
<SettingsSectionGrid cols={2}>
|
<SettingsSectionGrid cols={2}>
|
||||||
<SettingsSection>
|
<SettingsSection>
|
||||||
<SettingsSectionHeader>
|
<SettingsSectionHeader>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ 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";
|
import { pullEnv } from "@app/lib/pullEnv";
|
||||||
|
import SubscriptionStatusProvider from "@app/providers/SubscriptionStatusProvider";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
@@ -51,9 +52,15 @@ export default async function AdminLayout(props: LayoutProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<UserProvider user={user}>
|
<UserProvider user={user}>
|
||||||
<Layout orgs={orgs} navItems={adminNavSections(env)}>
|
<SubscriptionStatusProvider
|
||||||
{props.children}
|
subscriptionStatus={null}
|
||||||
</Layout>
|
env={env.app.environment}
|
||||||
|
sandbox_mode={env.app.sandbox_mode}
|
||||||
|
>
|
||||||
|
<Layout orgs={orgs} navItems={adminNavSections(env)}>
|
||||||
|
{props.children}
|
||||||
|
</Layout>
|
||||||
|
</SubscriptionStatusProvider>
|
||||||
</UserProvider>
|
</UserProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ export default function RoleMappingConfigFields({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{roleMappingMode === "mappingBuilder" && (
|
{roleMappingMode === "mappingBuilder" && (
|
||||||
<div className="space-y-4 rounded-md border p-3 min-w-0 max-w-full">
|
<div className="space-y-4 min-w-0 max-w-full">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<FormLabel>{t("roleMappingClaimPath")}</FormLabel>
|
<FormLabel>{t("roleMappingClaimPath")}</FormLabel>
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
75
src/components/idp/OidcIdpProviderTypeSelect.tsx
Normal file
75
src/components/idp/OidcIdpProviderTypeSelect.tsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
StrategySelect,
|
||||||
|
type StrategyOption
|
||||||
|
} from "@app/components/StrategySelect";
|
||||||
|
import type { IdpOidcProviderType } from "@app/lib/idp/oidcIdpProviderDefaults";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
value: IdpOidcProviderType;
|
||||||
|
onTypeChange: (type: IdpOidcProviderType) => void;
|
||||||
|
templatesPaid: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function OidcIdpProviderTypeSelect({
|
||||||
|
value,
|
||||||
|
onTypeChange,
|
||||||
|
templatesPaid
|
||||||
|
}: Props) {
|
||||||
|
const t = useTranslations();
|
||||||
|
|
||||||
|
const options: ReadonlyArray<StrategyOption<IdpOidcProviderType>> = [
|
||||||
|
{
|
||||||
|
id: "oidc",
|
||||||
|
title: "OAuth2/OIDC",
|
||||||
|
description: t("idpOidcDescription")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "google",
|
||||||
|
title: t("idpGoogleTitle"),
|
||||||
|
description: t("idpGoogleDescription"),
|
||||||
|
disabled: !templatesPaid,
|
||||||
|
icon: (
|
||||||
|
<Image
|
||||||
|
src="/idp/google.png"
|
||||||
|
alt={t("idpGoogleAlt")}
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
className="rounded"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "azure",
|
||||||
|
title: t("idpAzureTitle"),
|
||||||
|
description: t("idpAzureDescription"),
|
||||||
|
disabled: !templatesPaid,
|
||||||
|
icon: (
|
||||||
|
<Image
|
||||||
|
src="/idp/azure.png"
|
||||||
|
alt={t("idpAzureAlt")}
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
className="rounded"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="mb-2">
|
||||||
|
<span className="text-sm font-medium">{t("idpType")}</span>
|
||||||
|
</div>
|
||||||
|
<StrategySelect
|
||||||
|
value={value}
|
||||||
|
options={options}
|
||||||
|
onChange={onTypeChange}
|
||||||
|
cols={3}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
46
src/lib/idp/oidcIdpProviderDefaults.ts
Normal file
46
src/lib/idp/oidcIdpProviderDefaults.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import type { FieldValues, UseFormSetValue } from "react-hook-form";
|
||||||
|
|
||||||
|
export type IdpOidcProviderType = "oidc" | "google" | "azure";
|
||||||
|
|
||||||
|
export function applyOidcIdpProviderType<T extends FieldValues>(
|
||||||
|
setValue: UseFormSetValue<T>,
|
||||||
|
provider: IdpOidcProviderType
|
||||||
|
): void {
|
||||||
|
setValue("type" as never, provider as never);
|
||||||
|
|
||||||
|
if (provider === "google") {
|
||||||
|
setValue(
|
||||||
|
"authUrl" as never,
|
||||||
|
"https://accounts.google.com/o/oauth2/v2/auth" as never
|
||||||
|
);
|
||||||
|
setValue(
|
||||||
|
"tokenUrl" as never,
|
||||||
|
"https://oauth2.googleapis.com/token" as never
|
||||||
|
);
|
||||||
|
setValue("identifierPath" as never, "email" as never);
|
||||||
|
setValue("emailPath" as never, "email" as never);
|
||||||
|
setValue("namePath" as never, "name" as never);
|
||||||
|
setValue("scopes" as never, "openid profile email" as never);
|
||||||
|
} else if (provider === "azure") {
|
||||||
|
setValue(
|
||||||
|
"authUrl" as never,
|
||||||
|
"https://login.microsoftonline.com/{{TENANT_ID}}/oauth2/v2.0/authorize" as never
|
||||||
|
);
|
||||||
|
setValue(
|
||||||
|
"tokenUrl" as never,
|
||||||
|
"https://login.microsoftonline.com/{{TENANT_ID}}/oauth2/v2.0/token" as never
|
||||||
|
);
|
||||||
|
setValue("identifierPath" as never, "email" as never);
|
||||||
|
setValue("emailPath" as never, "email" as never);
|
||||||
|
setValue("namePath" as never, "name" as never);
|
||||||
|
setValue("scopes" as never, "openid profile email" as never);
|
||||||
|
setValue("tenantId" as never, "" as never);
|
||||||
|
} else {
|
||||||
|
setValue("authUrl" as never, "" as never);
|
||||||
|
setValue("tokenUrl" as never, "" as never);
|
||||||
|
setValue("identifierPath" as never, "sub" as never);
|
||||||
|
setValue("namePath" as never, "name" as never);
|
||||||
|
setValue("emailPath" as never, "email" as never);
|
||||||
|
setValue("scopes" as never, "openid profile email" as never);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user