diff --git a/messages/en-US.json b/messages/en-US.json index ffd28a518..58a772967 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -173,6 +173,10 @@ "resourcePoliciesDefaultBadgeText": "Default policy", "resourcePoliciesCreate": "Create Resource Policy", "resourcePoliciesCreateDescription": "Follow the steps below to create a new policy", + "resourcePolicyName": "Policy Name", + "resourcePolicyNameDescription": "Give this policy a name to identify it across your resources", + "resourcePolicyNamePlaceholder": "e.g. Internal Access Policy", + "policiesSeeAll": "See All Policies", "authentication": "Authentication", "protected": "Protected", "notProtected": "Not Protected", diff --git a/src/app/[orgId]/settings/(private)/resources/policies/create/page.tsx b/src/app/[orgId]/settings/(private)/resources/policies/create/page.tsx index 02afa06cd..e43ef39ee 100644 --- a/src/app/[orgId]/settings/(private)/resources/policies/create/page.tsx +++ b/src/app/[orgId]/settings/(private)/resources/policies/create/page.tsx @@ -1,7 +1,11 @@ +import { CreatePolicyForm } from "@app/components/CreatePolicyForm"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; +import { Button } from "@app/components/ui/button"; import { getCachedOrg } from "@app/lib/api/getCachedOrg"; +import OrgProvider from "@app/providers/OrgProvider"; import type { GetOrgResponse } from "@server/routers/org"; import { getTranslations } from "next-intl/server"; +import Link from "next/link"; import { redirect } from "next/navigation"; export interface CreateResourcePolicyPageProps { @@ -23,10 +27,22 @@ export default async function CreateResourcePolicyPage( } return ( <> - +
+ + + +
+ + + + ); } diff --git a/src/components/CreatePolicyForm.tsx b/src/components/CreatePolicyForm.tsx new file mode 100644 index 000000000..b1cb49a01 --- /dev/null +++ b/src/components/CreatePolicyForm.tsx @@ -0,0 +1,620 @@ +"use client"; + +import { + SettingsContainer, + SettingsSection, + SettingsSectionBody, + SettingsSectionDescription, + SettingsSectionFooter, + SettingsSectionForm, + SettingsSectionHeader, + SettingsSectionTitle +} from "@app/components/Settings"; +import { SwitchInput } from "@app/components/SwitchInput"; +import { Tag, TagInput } from "@app/components/tags/tag-input"; +import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; +import { Button } from "@app/components/ui/button"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage +} from "@app/components/ui/form"; +import { Input } from "@app/components/ui/input"; +import { InfoPopup } from "@app/components/ui/info-popup"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from "@app/components/ui/select"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { useOrgContext } from "@app/hooks/useOrgContext"; +import { usePaidStatus } from "@app/hooks/usePaidStatus"; +import { createApiClient } from "@app/lib/api"; +import { getUserDisplayName } from "@app/lib/getUserDisplayName"; +import { orgQueries } from "@app/lib/queries"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { build } from "@server/build"; +import { tierMatrix } from "@server/lib/billing/tierMatrix"; +import { UserType } from "@server/types/UserTypes"; +import { useQuery } from "@tanstack/react-query"; +import { Binary, Bot, InfoIcon, Key } from "lucide-react"; +import { useTranslations } from "next-intl"; +import { useRouter } from "next/navigation"; +import { useActionState, useMemo, useState } from "react"; +import { useForm } from "react-hook-form"; +import z from "zod"; + +const createPolicySchema = z.object({ + name: z.string().min(1).max(255), + sso: z.boolean().default(true), + skipToIdpId: z.number().nullable().optional(), + emailWhitelistEnabled: z.boolean().default(false), + roles: z.array( + z.object({ + id: z.string(), + text: z.string() + }) + ), + users: z.array( + z.object({ + id: z.string(), + text: z.string() + }) + ), + emails: z.array( + z.object({ + id: z.string(), + text: z.string() + }) + ) +}); + +export type CreatePolicyFormProps = {}; + +export function CreatePolicyForm({}: CreatePolicyFormProps) { + const { org } = useOrgContext(); + const t = useTranslations(); + const { env } = useEnvContext(); + const api = createApiClient({ env }); + const [, formAction, isSubmitting] = useActionState(onSubmit, null); + const router = useRouter(); + const { isPaidUser } = usePaidStatus(); + + const { data: orgRoles = [], isLoading: isLoadingOrgRoles } = useQuery( + orgQueries.roles({ + orgId: org.org.orgId + }) + ); + const { data: orgUsers = [], isLoading: isLoadingOrgUsers } = useQuery( + orgQueries.users({ + orgId: org.org.orgId + }) + ); + const { data: orgIdps = [], isLoading: isLoadingOrgIdps } = useQuery( + orgQueries.identityProviders({ + orgId: org.org.orgId, + useOrgOnlyIdp: env.app.identityProviderMode === "org" + }) + ); + + const form = useForm({ + resolver: zodResolver(createPolicySchema), + defaultValues: { + name: "", + sso: true, + skipToIdpId: null, + emailWhitelistEnabled: false, + roles: [], + users: [], + emails: [] + } + }); + + const [ssoEnabled, setSsoEnabled] = useState(true); + const [whitelistEnabled, setWhitelistEnabled] = useState(false); + const [selectedIdpId, setSelectedIdpId] = useState(null); + const [activeRolesTagIndex, setActiveRolesTagIndex] = useState< + number | null + >(null); + const [activeUsersTagIndex, setActiveUsersTagIndex] = useState< + number | null + >(null); + const [activeEmailTagIndex, setActiveEmailTagIndex] = useState< + number | null + >(null); + + async function onSubmit() { + // ... + } + + const allRoles = useMemo(() => { + return orgRoles + .map((role) => ({ + id: role.roleId.toString(), + text: role.name + })) + .filter((role) => role.text !== "Admin"); + }, [orgRoles]); + + const allUsers = useMemo(() => { + return orgUsers.map((user) => ({ + id: user.id.toString(), + text: `${getUserDisplayName({ + email: user.email, + username: user.username + })}${user.type !== UserType.Internal ? ` (${user.idpName})` : ""}` + })); + }, [orgUsers]); + + const allIdps = useMemo(() => { + if (build === "saas") { + if (isPaidUser(tierMatrix.orgOidc)) { + return orgIdps.map((idp) => ({ + id: idp.idpId, + text: idp.name + })); + } + } else { + return orgIdps.map((idp) => ({ + id: idp.idpId, + text: idp.name + })); + } + return []; + }, [orgIdps]); + + const pageLoading = + isLoadingOrgRoles || isLoadingOrgUsers || isLoadingOrgIdps; + + if (pageLoading) { + return <>; + } + + return ( +
+ + + {/* Name */} + + + + {t("resourcePolicyName")} + + + {t("resourcePolicyNameDescription")} + + + + + ( + + {t("name")} + + + + + + )} + /> + + + + + {/* Users & Roles */} + + + + {t("resourceUsersRoles")} + + + {t("resourceUsersRolesDescription")} + + + + + { + setSsoEnabled(val); + form.setValue("sso", val); + }} + /> + + {ssoEnabled && ( + <> + ( + + + {t("roles")} + + + { + form.setValue( + "roles", + newRoles as [ + Tag, + ...Tag[] + ] + ); + }} + enableAutocomplete={ + true + } + autocompleteOptions={ + allRoles + } + allowDuplicates={ + false + } + restrictTagsToAutocompleteOptions={ + true + } + sortTags={true} + /> + + + + {t( + "resourceRoleDescription" + )} + + + )} + /> + ( + + + {t("users")} + + + { + form.setValue( + "users", + newUsers as [ + Tag, + ...Tag[] + ] + ); + }} + enableAutocomplete={ + true + } + autocompleteOptions={ + allUsers + } + allowDuplicates={ + false + } + restrictTagsToAutocompleteOptions={ + true + } + sortTags={true} + /> + + + + )} + /> + + )} + + {ssoEnabled && allIdps.length > 0 && ( +
+ + +

+ {t( + "defaultIdentityProviderDescription" + )} +

+
+ )} +
+
+
+ + {/* Auth Methods */} + + + + {t("resourceAuthMethods")} + + + {t("resourceAuthMethodsDescriptions")} + + + + +
+
+ + + {t("resourcePasswordProtection", { + status: t("disabled") + })} + +
+ +
+ +
+
+ + + {t("resourcePincodeProtection", { + status: t("disabled") + })} + +
+ +
+ +
+
+ + + {t( + "resourceHeaderAuthProtectionDisabled" + )} + +
+ +
+
+
+
+ + {/* OTP Email */} + + + + {t("otpEmailTitle")} + + + {t("otpEmailTitleDescription")} + + + + + {!env.email.emailEnabled && ( + + + + {t("otpEmailSmtpRequired")} + + + {t( + "otpEmailSmtpRequiredDescription" + )} + + + )} + { + setWhitelistEnabled(val); + form.setValue( + "emailWhitelistEnabled", + val + ); + }} + disabled={!env.email.emailEnabled} + /> + + {whitelistEnabled && env.email.emailEnabled && ( + ( + + + + + + {/* @ts-ignore */} + { + return z + .email() + .or( + z + .string() + .regex( + /^\*@[\w.-]+\.[a-zA-Z]{2,}$/, + { + message: + t( + "otpEmailErrorInvalid" + ) + } + ) + ) + .safeParse(tag) + .success; + }} + setActiveTagIndex={ + setActiveEmailTagIndex + } + placeholder={t( + "otpEmailEnter" + )} + tags={ + form.getValues() + .emails + } + setTags={( + newEmails + ) => { + form.setValue( + "emails", + newEmails as [ + Tag, + ...Tag[] + ] + ); + }} + allowDuplicates={false} + sortTags={true} + /> + + + {t( + "otpEmailEnterDescription" + )} + + + )} + /> + )} + + + + + + +
+
+ + ); +}