diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx index 94e1ed6ff..3194e7343 100644 --- a/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx @@ -2,16 +2,12 @@ import ActionBanner from "@app/components/ActionBanner"; import { EditPolicyForm } from "@app/components/resource-policy/EditPolicyForm"; -import { RolesSelector } from "@app/components/roles-selector"; -import SetResourceHeaderAuthForm from "@app/components/SetResourceHeaderAuthForm"; -import SetResourcePincodeForm from "@app/components/SetResourcePincodeForm"; import { SettingsContainer, SettingsSection, SettingsSectionBody, SettingsSectionDescription, SettingsSectionFooter, - SettingsSectionForm, SettingsSectionHeader, SettingsSectionTitle } from "@app/components/Settings"; @@ -19,9 +15,6 @@ import { StrategySelect, type StrategyOption } from "@app/components/StrategySelect"; -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 { Command, @@ -31,30 +24,11 @@ import { CommandItem, CommandList } from "@app/components/ui/command"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage -} from "@app/components/ui/form"; -import { InfoPopup } from "@app/components/ui/info-popup"; import { Popover, PopoverContent, PopoverTrigger } from "@app/components/ui/popover"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue -} from "@app/components/ui/select"; -import { UsersSelector } from "@app/components/users-selector"; -import type { ResourceContextType } from "@app/contexts/resourceContext"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { useOrgContext } from "@app/hooks/useOrgContext"; import { usePaidStatus } from "@app/hooks/usePaidStatus"; @@ -62,36 +36,18 @@ import { useResourceContext } from "@app/hooks/useResourceContext"; import { toast } from "@app/hooks/useToast"; import { createApiClient, formatAxiosError } from "@app/lib/api"; import { cn } from "@app/lib/cn"; -import { getUserDisplayName } from "@app/lib/getUserDisplayName"; import { orgQueries, resourceQueries } from "@app/lib/queries"; import { ResourcePolicyProvider } from "@app/providers/ResourcePolicyProvider"; import { zodResolver } from "@hookform/resolvers/zod"; import { CaretSortIcon } from "@radix-ui/react-icons"; import { build } from "@server/build"; import { tierMatrix, TierFeature } from "@server/lib/billing/tierMatrix"; -import { UserType } from "@server/types/UserTypes"; import { useQuery, useQueryClient } from "@tanstack/react-query"; -import SetResourcePasswordForm from "components/SetResourcePasswordForm"; -import { - ArrowRightIcon, - Binary, - Bot, - CheckIcon, - InfoIcon, - Key, - ShieldAlertIcon -} from "lucide-react"; +import { ArrowRightIcon, CheckIcon, ShieldAlertIcon } from "lucide-react"; import { useTranslations } from "next-intl"; import Link from "next/link"; import { useRouter } from "next/navigation"; -import { - useActionState, - useEffect, - useMemo, - useRef, - useState, - useTransition -} from "react"; +import { useEffect, useState, useTransition } from "react"; import { useForm, useWatch } from "react-hook-form"; import { z } from "zod"; @@ -108,34 +64,9 @@ const resourceTypeSchema = z type ResourcePolicyType = StrategyOption<"inline" | "shared">; -const UsersRolesFormSchema = z.object({ - roles: z.array( - z.object({ - id: z.string(), - text: z.string() - }) - ), - users: z.array( - z.object({ - id: z.string(), - text: z.string() - }) - ) -}); - -const whitelistSchema = z.object({ - emails: z.array( - z.object({ - id: z.string(), - text: z.string() - }) - ) -}); - export default function ResourceAuthenticationPage() { const { org } = useOrgContext(); - const { resource, updateResource, authInfo, updateAuthInfo } = - useResourceContext(); + const { resource, updateResource } = useResourceContext(); const queryClient = useQueryClient(); const { env } = useEnvContext(); @@ -151,42 +82,6 @@ export default function ResourceAuthenticationPage() { }) ); - const { data: resourceRoles = [], isLoading: isLoadingResourceRoles } = - useQuery( - resourceQueries.resourceRoles({ - resourceId: resource.resourceId - }) - ); - const { data: resourceUsers = [], isLoading: isLoadingResourceUsers } = - useQuery( - resourceQueries.resourceUsers({ - resourceId: resource.resourceId - }) - ); - - const { data: whitelist = [], isLoading: isLoadingWhiteList } = useQuery( - resourceQueries.resourceWhitelist({ - resourceId: resource.resourceId - }) - ); - - 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(resourceTypeSchema), defaultValues: { @@ -231,58 +126,6 @@ export default function ResourceAuthenticationPage() { } ]; - 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 [ssoEnabled, setSsoEnabled] = useState(resource.sso ?? false); - - useEffect(() => { - setSsoEnabled(resource.sso ?? false); - }, [resource.sso]); - - const [selectedIdpId, setSelectedIdpId] = useState( - resource.skipToIdpId || null - ); - - const [loadingRemoveResourcePassword, setLoadingRemoveResourcePassword] = - useState(false); - const [loadingRemoveResourcePincode, setLoadingRemoveResourcePincode] = - useState(false); - const [ - loadingRemoveResourceHeaderAuth, - setLoadingRemoveResourceHeaderAuth - ] = useState(false); - - const [isSetPasswordOpen, setIsSetPasswordOpen] = useState(false); - const [isSetPincodeOpen, setIsSetPincodeOpen] = useState(false); - const [isSetHeaderAuthOpen, setIsSetHeaderAuthOpen] = useState(false); - - const usersRolesForm = useForm({ - resolver: zodResolver(UsersRolesFormSchema), - defaultValues: { roles: [], users: [] } - }); - - const whitelistForm = useForm({ - resolver: zodResolver(whitelistSchema), - defaultValues: { emails: [] } - }); - - const hasInitializedRef = useRef(false); - useEffect(() => { if (!isLoadingPolicies && policies?.sharedPolicy) { setSelectedPolicy({ @@ -292,55 +135,7 @@ export default function ResourceAuthenticationPage() { } }, [isLoadingPolicies, policies?.sharedPolicy]); - const pageLoading = - isLoadingPolicies || - !policies || - isLoadingOrgRoles || - isLoadingOrgUsers || - isLoadingResourceRoles || - isLoadingResourceUsers || - isLoadingWhiteList || - isLoadingOrgIdps; - - useEffect(() => { - if (pageLoading || hasInitializedRef.current) return; - - usersRolesForm.setValue( - "roles", - resourceRoles - .map((i) => ({ - id: i.roleId.toString(), - text: i.name - })) - .filter((role) => role.text !== "Admin") - ); - usersRolesForm.setValue( - "users", - resourceUsers.map((i) => ({ - id: i.userId.toString(), - text: `${getUserDisplayName({ - email: i.email, - username: i.username - })}${i.type !== UserType.Internal ? ` (${i.idpName})` : ""}` - })) - ); - - whitelistForm.setValue( - "emails", - whitelist.map((w) => ({ - id: w.email, - text: w.email - })) - ); - hasInitializedRef.current = true; - }, [pageLoading, resourceRoles, resourceUsers, whitelist, orgIdps]); - - const [, submitUserRolesForm, loadingSaveUsersRoles] = useActionState( - onSubmitUsersRoles, - null - ); - - const [isUpdatingResource, startTransitionPolicy] = useTransition(); + const [isUpdatingResource, startTransition] = useTransition(); async function handleSaveResourcePolicyType() { try { @@ -381,206 +176,18 @@ export default function ResourceAuthenticationPage() { } } - async function onSubmitUsersRoles() { - const isValid = usersRolesForm.trigger(); - if (!isValid) return; - - const data = usersRolesForm.getValues(); - - try { - const jobs = [ - api.post(`/resource/${resource.resourceId}/roles`, { - roleIds: data.roles.map((i) => parseInt(i.id)) - }), - api.post(`/resource/${resource.resourceId}/users`, { - userIds: data.users.map((i) => i.id) - }), - api.post(`/resource/${resource.resourceId}`, { - sso: ssoEnabled, - skipToIdpId: selectedIdpId - }) - ]; - - await Promise.all(jobs); - - updateResource({ - sso: ssoEnabled, - skipToIdpId: selectedIdpId - }); - - updateAuthInfo({ - sso: ssoEnabled - }); - - toast({ - title: t("resourceAuthSettingsSave"), - description: t("resourceAuthSettingsSaveDescription") - }); - await queryClient.invalidateQueries( - resourceQueries.resourceUsers({ - resourceId: resource.resourceId - }) - ); - await queryClient.invalidateQueries( - resourceQueries.resourceRoles({ - resourceId: resource.resourceId - }) - ); - - router.refresh(); - } catch (e) { - console.error(e); - toast({ - variant: "destructive", - title: t("resourceErrorUsersRolesSave"), - description: formatAxiosError( - e, - t("resourceErrorUsersRolesSaveDescription") - ) - }); - } - } - - function removeResourcePassword() { - setLoadingRemoveResourcePassword(true); - - api.post(`/resource/${resource.resourceId}/password`, { - password: null - }) - .then(() => { - toast({ - title: t("resourcePasswordRemove"), - description: t("resourcePasswordRemoveDescription") - }); - - updateAuthInfo({ - password: false - }); - router.refresh(); - }) - .catch((e) => { - toast({ - variant: "destructive", - title: t("resourceErrorPasswordRemove"), - description: formatAxiosError( - e, - t("resourceErrorPasswordRemoveDescription") - ) - }); - }) - .finally(() => setLoadingRemoveResourcePassword(false)); - } - - function removeResourcePincode() { - setLoadingRemoveResourcePincode(true); - - api.post(`/resource/${resource.resourceId}/pincode`, { - pincode: null - }) - .then(() => { - toast({ - title: t("resourcePincodeRemove"), - description: t("resourcePincodeRemoveDescription") - }); - - updateAuthInfo({ - pincode: false - }); - router.refresh(); - }) - .catch((e) => { - toast({ - variant: "destructive", - title: t("resourceErrorPincodeRemove"), - description: formatAxiosError( - e, - t("resourceErrorPincodeRemoveDescription") - ) - }); - }) - .finally(() => setLoadingRemoveResourcePincode(false)); - } - - function removeResourceHeaderAuth() { - setLoadingRemoveResourceHeaderAuth(true); - - api.post(`/resource/${resource.resourceId}/header-auth`, { - user: null, - password: null, - extendedCompatibility: null - }) - .then(() => { - toast({ - title: t("resourceHeaderAuthRemove"), - description: t("resourceHeaderAuthRemoveDescription") - }); - - updateAuthInfo({ - headerAuth: false - }); - router.refresh(); - }) - .catch((e) => { - toast({ - variant: "destructive", - title: t("resourceErrorHeaderAuthRemove"), - description: formatAxiosError( - e, - t("resourceErrorHeaderAuthRemoveDescription") - ) - }); - }) - .finally(() => setLoadingRemoveResourceHeaderAuth(false)); - } + const pageLoading = isLoadingPolicies || !policies; if (pageLoading) { return <>; } + console.log({ + shared: policies.sharedPolicy + }); + return ( <> - {isSetPasswordOpen && ( - { - setIsSetPasswordOpen(false); - updateAuthInfo({ - password: true - }); - }} - /> - )} - - {isSetPincodeOpen && ( - { - setIsSetPincodeOpen(false); - updateAuthInfo({ - pincode: true - }); - }} - /> - )} - - {isSetHeaderAuthOpen && ( - { - setIsSetHeaderAuthOpen(false); - updateAuthInfo({ - headerAuth: true - }); - }} - /> - )} - {build !== "oss" && isPaidUser(tierMatrix[TierFeature.ResourcePolicies]) && ( @@ -683,7 +290,7 @@ export default function ResourceAuthenticationPage() { - - - - - - - {t("resourceAuthMethods")} - - - {t("resourceAuthMethodsDescriptions")} - - - - - {/* Password Protection */} -
-
- - - {t("resourcePasswordProtection", { - status: authInfo.password - ? t("enabled") - : t("disabled") - })} - -
- -
- - {/* PIN Code Protection */} -
-
- - - {t("resourcePincodeProtection", { - status: authInfo.pincode - ? t("enabled") - : t("disabled") - })} - -
- -
- - {/* Header Authentication Protection */} -
-
- - - {authInfo.headerAuth - ? t( - "resourceHeaderAuthProtectionEnabled" - ) - : t( - "resourceHeaderAuthProtectionDisabled" - )} - -
- -
-
-
-
- - - {selectedResourceType === "inline" ? ( @@ -1020,216 +344,3 @@ export default function ResourceAuthenticationPage() { ); } - -type OneTimePasswordFormSectionProps = Pick< - ResourceContextType, - "resource" | "updateResource" -> & { - whitelist: Array<{ email: string }>; - isLoadingWhiteList: boolean; -}; - -function OneTimePasswordFormSection({ - resource, - updateResource, - whitelist, - isLoadingWhiteList -}: OneTimePasswordFormSectionProps) { - const { env } = useEnvContext(); - const [whitelistEnabled, setWhitelistEnabled] = useState( - resource.emailWhitelistEnabled ?? false - ); - - useEffect(() => { - setWhitelistEnabled(resource.emailWhitelistEnabled); - }, [resource.emailWhitelistEnabled]); - - const queryClient = useQueryClient(); - - const [loadingSaveWhitelist, startTransition] = useTransition(); - const whitelistForm = useForm({ - resolver: zodResolver(whitelistSchema), - defaultValues: { emails: [] } - }); - const api = createApiClient({ env }); - const router = useRouter(); - const t = useTranslations(); - - const [activeEmailTagIndex, setActiveEmailTagIndex] = useState< - number | null - >(null); - - useEffect(() => { - if (isLoadingWhiteList) return; - - whitelistForm.setValue( - "emails", - whitelist.map((w) => ({ - id: w.email, - text: w.email - })) - ); - }, [isLoadingWhiteList, whitelist, whitelistForm]); - - async function saveWhitelist() { - try { - await api.post(`/resource/${resource.resourceId}`, { - emailWhitelistEnabled: whitelistEnabled - }); - - if (whitelistEnabled) { - await api.post(`/resource/${resource.resourceId}/whitelist`, { - emails: whitelistForm.getValues().emails.map((i) => i.text) - }); - } - - updateResource({ - emailWhitelistEnabled: whitelistEnabled - }); - - toast({ - title: t("resourceWhitelistSave"), - description: t("resourceWhitelistSaveDescription") - }); - router.refresh(); - await queryClient.invalidateQueries( - resourceQueries.resourceWhitelist({ - resourceId: resource.resourceId - }) - ); - } catch (e) { - console.error(e); - toast({ - variant: "destructive", - title: t("resourceErrorWhitelistSave"), - description: formatAxiosError( - e, - t("resourceErrorWhitelistSaveDescription") - ) - }); - } - } - - return ( - - - - {t("otpEmailTitle")} - - - {t("otpEmailTitleDescription")} - - - - - {!env.email.emailEnabled && ( - - - - {t("otpEmailSmtpRequired")} - - - {t("otpEmailSmtpRequiredDescription")} - - - )} - - - {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={ - whitelistForm.getValues() - .emails - } - setTags={(newRoles) => { - whitelistForm.setValue( - "emails", - newRoles as [ - Tag, - ...Tag[] - ] - ); - }} - allowDuplicates={false} - sortTags={true} - /> - - - {t("otpEmailEnterDescription")} - - - )} - /> - - - )} -
-
- - - -
- ); -} diff --git a/src/components/resource-policy/EditPolicyForm.tsx b/src/components/resource-policy/EditPolicyForm.tsx index d0594752f..b26453c6e 100644 --- a/src/components/resource-policy/EditPolicyForm.tsx +++ b/src/components/resource-policy/EditPolicyForm.tsx @@ -6,11 +6,9 @@ import { useEnvContext } from "@app/hooks/useEnvContext"; import { useOrgContext } from "@app/hooks/useOrgContext"; import { usePaidStatus } from "@app/hooks/usePaidStatus"; -import { getUserDisplayName } from "@app/lib/getUserDisplayName"; import { orgQueries } from "@app/lib/queries"; 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 { useTranslations } from "next-intl"; @@ -51,12 +49,6 @@ export function EditPolicyForm({ env.server.maxmind_asn_path && env.server.maxmind_asn_path.length > 0 ); - 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, @@ -64,26 +56,6 @@ export function EditPolicyForm({ }) ); - const allRoles = useMemo( - () => - orgRoles - .map((role) => ({ - id: role.roleId.toString(), - text: role.name - })) - .filter((role) => role.text !== "Admin"), - [orgRoles] - ); - - const allUsers = useMemo( - () => - 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)) { @@ -98,17 +70,18 @@ export function EditPolicyForm({ return []; }, [orgIdps, isPaidUser]); - if (isLoadingOrgRoles || isLoadingOrgUsers || isLoadingOrgIdps) { + if (isLoadingOrgIdps) { return <>; } return ( - {!hidePolicyNameForm && } + {!hidePolicyNameForm && ( + + )} diff --git a/src/components/resource-policy/EditPolicyUserRolesSectionForm.tsx b/src/components/resource-policy/EditPolicyUserRolesSectionForm.tsx index 29fe486be..f535021e2 100644 --- a/src/components/resource-policy/EditPolicyUserRolesSectionForm.tsx +++ b/src/components/resource-policy/EditPolicyUserRolesSectionForm.tsx @@ -23,8 +23,9 @@ import type { AxiosResponse } from "axios"; import { useRouter } from "next/navigation"; import { createPolicySchema } from "."; +import { RolesSelector } from "@app/components/roles-selector"; +import { UsersSelector } from "@app/components/users-selector"; import { SwitchInput } from "@app/components/SwitchInput"; -import { Tag, TagInput } from "@app/components/tags/tag-input"; import { Button } from "@app/components/ui/button"; import { Form, @@ -50,15 +51,13 @@ import { useForm, useWatch } from "react-hook-form"; // ─── PolicyUsersRolesSection ────────────────────────────────────────────────── type PolicyUsersRolesSectionProps = { - allRoles: { id: string; text: string }[]; - allUsers: { id: string; text: string }[]; + orgId: string; allIdps: { id: number; text: string }[]; readonly?: boolean; }; export function EditPolicyUsersRolesSectionForm({ - allRoles, - allUsers, + orgId, allIdps, readonly }: PolicyUsersRolesSectionProps) { @@ -98,12 +97,6 @@ export function EditPolicyUsersRolesSectionForm({ control: form.control, name: "skipToIdpId" }); - const [activeRolesTagIndex, setActiveRolesTagIndex] = useState< - number | null - >(null); - const [activeUsersTagIndex, setActiveUsersTagIndex] = useState< - number | null - >(null); const [, formAction, isSubmitting] = useActionState(onSubmit, null); @@ -190,43 +183,21 @@ export function EditPolicyUsersRolesSectionForm({ {t("roles")} - { + onSelectRoles={( + roles + ) => form.setValue( "roles", - newRoles as [ - Tag, - ...Tag[] - ] - ); - }} - enableAutocomplete={ - true + roles + ) } - autocompleteOptions={ - allRoles - } - allowDuplicates={false} - restrictTagsToAutocompleteOptions={ - true - } - sortTags={true} disabled={readonly} + restrictAdminRole /> @@ -247,42 +218,19 @@ export function EditPolicyUsersRolesSectionForm({ {t("users")} - { + onSelectUsers={( + users + ) => form.setValue( "users", - newUsers as [ - Tag, - ...Tag[] - ] - ); - }} - enableAutocomplete={ - true + users + ) } - autocompleteOptions={ - allUsers - } - allowDuplicates={false} - restrictTagsToAutocompleteOptions={ - true - } - sortTags={true} disabled={readonly} /> diff --git a/src/components/users-selector.tsx b/src/components/users-selector.tsx index 5cbe90e89..b63e3d4bb 100644 --- a/src/components/users-selector.tsx +++ b/src/components/users-selector.tsx @@ -18,12 +18,14 @@ export type UsersSelectorProps = { orgId: string; selectedUsers?: SelectedUser[]; onSelectUsers: (users: SelectedUser[]) => void; + disabled?: boolean; }; export function UsersSelector({ orgId, selectedUsers = [], - onSelectUsers + onSelectUsers, + disabled }: UsersSelectorProps) { const t = useTranslations(); const [userSearchQuery, setUserSearchQuery] = useState(""); @@ -58,6 +60,7 @@ export function UsersSelector({ options={usersShown} value={selectedUsers} onChange={onSelectUsers} + disabled={disabled} /> ); }