mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-07 00:39:53 +00:00
🚧 wip
This commit is contained in:
@@ -147,7 +147,7 @@ export enum ActionsEnum {
|
||||
setResourcePolicyHeaderAuth = "setResourcePolicyHeaderAuth",
|
||||
setResourcePolicyWhitelist = "setResourcePolicyWhitelist",
|
||||
setResourcePolicyRules = "setResourcePolicyRules",
|
||||
getResourcePolicies = "getResourcePolicies"
|
||||
|
||||
}
|
||||
|
||||
export async function checkUserActionPermission(
|
||||
|
||||
@@ -637,10 +637,10 @@ authenticated.get(
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/resource/:resourceId/policies",
|
||||
"/resource/:resourceId/default-policy",
|
||||
verifyResourceAccess,
|
||||
verifyUserHasAction(ActionsEnum.getResourcePolicies),
|
||||
resource.getResourcePolicies
|
||||
verifyUserHasAction(ActionsEnum.getResourcePolicy),
|
||||
resource.getDefaultResourcePolicy
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
|
||||
@@ -454,10 +454,10 @@ authenticated.get(
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/resource/:resourceId/policies",
|
||||
"/resource/:resourceId/default-policy",
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.getResourcePolicies),
|
||||
resource.getResourcePolicies
|
||||
verifyApiKeyHasAction(ActionsEnum.getResourcePolicy),
|
||||
resource.getDefaultResourcePolicy
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
|
||||
@@ -17,13 +17,11 @@ const getResourcePoliciesParamsSchema = z.strictObject({
|
||||
resourceId: z.string().transform(Number).pipe(z.int().positive())
|
||||
});
|
||||
|
||||
export type GetResourcePoliciesResponse = {
|
||||
defaultPolicy: GetResourcePolicyResponse | null;
|
||||
};
|
||||
export type GetDefaultResourcePolicyResponse = GetResourcePolicyResponse;
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/resource/{resourceId}/policies",
|
||||
path: "/resource/{resourceId}/default-policy",
|
||||
description: "Get the default policy for a resource.",
|
||||
tags: [OpenAPITags.PublicResource, OpenAPITags.Policy],
|
||||
request: {
|
||||
@@ -32,7 +30,7 @@ registry.registerPath({
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function getResourcePolicies(
|
||||
export async function getDefaultResourcePolicy(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
@@ -66,14 +64,20 @@ export async function getResourcePolicies(
|
||||
);
|
||||
}
|
||||
|
||||
const defaultPolicy = resource.defaultResourcePolicyId
|
||||
? await queryResourcePolicy({
|
||||
resourcePolicyId: resource.defaultResourcePolicyId
|
||||
})
|
||||
: null;
|
||||
if (!resource.defaultResourcePolicyId) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
"Resource has no default policy"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return response<GetResourcePoliciesResponse>(res, {
|
||||
data: { defaultPolicy },
|
||||
const defaultPolicy = await queryResourcePolicy({
|
||||
resourcePolicyId: resource.defaultResourcePolicyId
|
||||
});
|
||||
return response<GetDefaultResourcePolicyResponse>(res, {
|
||||
data: defaultPolicy,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Resource policies retrieved successfully",
|
||||
@@ -31,4 +31,4 @@ export * from "./addUserToResource";
|
||||
export * from "./removeUserFromResource";
|
||||
export * from "./listAllResourceNames";
|
||||
export * from "./removeEmailFromResourceWhitelist";
|
||||
export * from "./getResourcePolicies";
|
||||
export * from "./getDefaultResourcePolicy";
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { EditPolicyForm } from "@app/components/resource-policy/EditPolicyForm";
|
||||
import SetResourceHeaderAuthForm from "@app/components/SetResourceHeaderAuthForm";
|
||||
import SetResourcePincodeForm from "@app/components/SetResourcePincodeForm";
|
||||
import {
|
||||
@@ -46,7 +47,10 @@ import { toast } from "@app/hooks/useToast";
|
||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||
import { getUserDisplayName } from "@app/lib/getUserDisplayName";
|
||||
import { orgQueries, resourceQueries } from "@app/lib/queries";
|
||||
import { ResourcePolicyContext } from "@app/providers/ResourcePolicyProvider";
|
||||
import {
|
||||
ResourcePolicyContext,
|
||||
ResourcePolicyProvider
|
||||
} from "@app/providers/ResourcePolicyProvider";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { build } from "@server/build";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
@@ -64,32 +68,19 @@ import {
|
||||
useState,
|
||||
useTransition
|
||||
} from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useForm, useWatch } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
const UsersRolesFormSchema = z.object({
|
||||
roles: z.array(
|
||||
const resourceTypeSchema = z
|
||||
.object({
|
||||
type: z.literal("inline")
|
||||
})
|
||||
.or(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
text: z.string()
|
||||
type: z.literal("shared"),
|
||||
resourcePolicyId: z.number()
|
||||
})
|
||||
),
|
||||
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()
|
||||
})
|
||||
)
|
||||
});
|
||||
);
|
||||
|
||||
type ResourcePolicyType = StrategyOption<"inline" | "shared">;
|
||||
|
||||
@@ -106,114 +97,14 @@ export default function ResourceAuthenticationPage() {
|
||||
|
||||
const { isPaidUser } = usePaidStatus();
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
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: policies, isLoading: isLoadingPolicies } = useQuery(
|
||||
resourceQueries.policies({
|
||||
const { data: defaultPolicy, isLoading: isLoadingPolicies } = useQuery(
|
||||
resourceQueries.defaultPolicy({
|
||||
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 pageLoading = isLoadingPolicies || !defaultPolicy;
|
||||
|
||||
const pageLoading =
|
||||
isLoadingOrgRoles ||
|
||||
isLoadingOrgUsers ||
|
||||
isLoadingResourceRoles ||
|
||||
isLoadingResourceUsers ||
|
||||
isLoadingWhiteList ||
|
||||
isLoadingOrgIdps ||
|
||||
isLoadingPolicies;
|
||||
|
||||
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 [activeRolesTagIndex, setActiveRolesTagIndex] = useState<
|
||||
number | null
|
||||
>(null);
|
||||
const [activeUsersTagIndex, setActiveUsersTagIndex] = useState<
|
||||
number | null
|
||||
>(null);
|
||||
|
||||
const [ssoEnabled, setSsoEnabled] = useState(resource.sso ?? false);
|
||||
|
||||
useEffect(() => {
|
||||
setSsoEnabled(resource.sso ?? false);
|
||||
}, [resource.sso]);
|
||||
|
||||
const [selectedIdpId, setSelectedIdpId] = useState<number | null>(
|
||||
resource.skipToIdpId || null
|
||||
);
|
||||
|
||||
const [loadingRemoveResourcePassword, setLoadingRemoveResourcePassword] =
|
||||
useState(false);
|
||||
const [loadingRemoveResourcePincode, setLoadingRemoveResourcePincode] =
|
||||
useState(false);
|
||||
const [
|
||||
loadingRemoveResourceHeaderAuth,
|
||||
setLoadingRemoveResourceHeaderAuth
|
||||
@@ -223,209 +114,6 @@ export default function ResourceAuthenticationPage() {
|
||||
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 (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
|
||||
);
|
||||
|
||||
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")
|
||||
});
|
||||
// invalidate resource queries
|
||||
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 resourcePolicyTypes: Array<ResourcePolicyType> = [
|
||||
{
|
||||
id: "inline",
|
||||
@@ -439,8 +127,17 @@ export default function ResourceAuthenticationPage() {
|
||||
}
|
||||
];
|
||||
|
||||
const [selectedResourceType, setSelectedResourceType] =
|
||||
useState<ResourcePolicyType["id"]>("inline");
|
||||
const form = useForm({
|
||||
resolver: zodResolver(resourceTypeSchema),
|
||||
defaultValues: {
|
||||
type: "inline"
|
||||
}
|
||||
});
|
||||
|
||||
const selectedResourceType = useWatch({
|
||||
control: form.control,
|
||||
name: "type"
|
||||
});
|
||||
|
||||
if (pageLoading) {
|
||||
return <></>;
|
||||
@@ -503,13 +200,9 @@ export default function ResourceAuthenticationPage() {
|
||||
<SettingsSectionBody>
|
||||
<StrategySelect
|
||||
options={resourcePolicyTypes}
|
||||
defaultValue="inline"
|
||||
value={selectedResourceType}
|
||||
onChange={(value) => {
|
||||
// baseForm.setValue(
|
||||
// "http",
|
||||
// value === "http"
|
||||
// );
|
||||
// // Update method default when switching resource type
|
||||
form.setValue("type", value);
|
||||
}}
|
||||
cols={2}
|
||||
/>
|
||||
@@ -524,223 +217,10 @@ export default function ResourceAuthenticationPage() {
|
||||
</Button>
|
||||
</SettingsSectionFooter>
|
||||
</SettingsSection>
|
||||
{/* <ResourcePolicyContext value={policies?.defaultPolicy}>
|
||||
|
||||
</ResourcePolicyContext> */}
|
||||
<ResourcePolicyProvider policy={defaultPolicy}>
|
||||
<EditPolicyForm hidePolicyNameForm />
|
||||
</ResourcePolicyProvider>
|
||||
</SettingsContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("otpEmailTitle")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t("otpEmailTitleDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<SettingsSectionForm>
|
||||
{!env.email.emailEnabled && (
|
||||
<Alert variant="neutral" className="mb-4">
|
||||
<InfoIcon className="h-4 w-4" />
|
||||
<AlertTitle className="font-semibold">
|
||||
{t("otpEmailSmtpRequired")}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
{t("otpEmailSmtpRequiredDescription")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
<SwitchInput
|
||||
id="whitelist-toggle"
|
||||
label={t("otpEmailWhitelist")}
|
||||
checked={whitelistEnabled}
|
||||
onCheckedChange={setWhitelistEnabled}
|
||||
disabled={!env.email.emailEnabled}
|
||||
/>
|
||||
|
||||
{whitelistEnabled && env.email.emailEnabled && (
|
||||
<Form {...whitelistForm}>
|
||||
<form id="whitelist-form">
|
||||
<FormField
|
||||
control={whitelistForm.control}
|
||||
name="emails"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<InfoPopup
|
||||
text={t(
|
||||
"otpEmailWhitelistList"
|
||||
)}
|
||||
info={t(
|
||||
"otpEmailWhitelistListDescription"
|
||||
)}
|
||||
/>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
{/* @ts-ignore */}
|
||||
<TagInput
|
||||
{...field}
|
||||
activeTagIndex={
|
||||
activeEmailTagIndex
|
||||
}
|
||||
size={"sm"}
|
||||
validateTag={(tag) => {
|
||||
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}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t("otpEmailEnterDescription")}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
)}
|
||||
</SettingsSectionForm>
|
||||
</SettingsSectionBody>
|
||||
<SettingsSectionFooter>
|
||||
<Button
|
||||
onClick={() => startTransition(saveWhitelist)}
|
||||
form="whitelist-form"
|
||||
loading={loadingSaveWhitelist}
|
||||
disabled={loadingSaveWhitelist}
|
||||
>
|
||||
{t("otpEmailWhitelistSave")}
|
||||
</Button>
|
||||
</SettingsSectionFooter>
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -73,7 +73,11 @@ const setHeaderAuthSchema = z.object({
|
||||
extendedCompatibility: z.boolean()
|
||||
});
|
||||
|
||||
export function EditPolicyAuthMethodsSectionForm() {
|
||||
export function EditPolicyAuthMethodsSectionForm({
|
||||
readonly
|
||||
}: {
|
||||
readonly?: boolean;
|
||||
}) {
|
||||
const { policy } = useResourcePolicyContext();
|
||||
const router = useRouter();
|
||||
|
||||
@@ -132,6 +136,7 @@ export function EditPolicyAuthMethodsSectionForm() {
|
||||
const [, formAction, isSubmitting] = useActionState(onSubmit, null);
|
||||
|
||||
async function onSubmit() {
|
||||
if (readonly) return;
|
||||
const isValid = await form.trigger();
|
||||
|
||||
if (!isValid) return;
|
||||
@@ -237,14 +242,16 @@ export function EditPolicyAuthMethodsSectionForm() {
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setIsExpanded(true)}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
{t("resourcePolicyAuthMethodAdd")}
|
||||
</Button>
|
||||
{!readonly && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setIsExpanded(true)}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
{t("resourcePolicyAuthMethodAdd")}
|
||||
</Button>
|
||||
)}
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
);
|
||||
@@ -541,6 +548,7 @@ export function EditPolicyAuthMethodsSectionForm() {
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
disabled={readonly}
|
||||
onClick={
|
||||
hasPassword
|
||||
? () =>
|
||||
@@ -579,6 +587,7 @@ export function EditPolicyAuthMethodsSectionForm() {
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
disabled={readonly}
|
||||
onClick={
|
||||
hasPincode
|
||||
? () =>
|
||||
@@ -619,6 +628,7 @@ export function EditPolicyAuthMethodsSectionForm() {
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
disabled={readonly}
|
||||
onClick={
|
||||
hasHeaderAuth
|
||||
? () =>
|
||||
@@ -644,7 +654,7 @@ export function EditPolicyAuthMethodsSectionForm() {
|
||||
<Button
|
||||
type="submit"
|
||||
loading={isSubmitting}
|
||||
disabled={isSubmitting}
|
||||
disabled={readonly || isSubmitting}
|
||||
>
|
||||
{t("authMethodsSave")}
|
||||
</Button>
|
||||
|
||||
@@ -28,9 +28,13 @@ import { EditPolicyRulesSectionForm } from "./EditPolicyRulesSectionForm";
|
||||
|
||||
export type EditPolicyFormProps = {
|
||||
hidePolicyNameForm?: boolean;
|
||||
readonly?: boolean;
|
||||
};
|
||||
|
||||
export function EditPolicyForm({ hidePolicyNameForm }: EditPolicyFormProps) {
|
||||
export function EditPolicyForm({
|
||||
hidePolicyNameForm,
|
||||
readonly
|
||||
}: EditPolicyFormProps) {
|
||||
const { org } = useOrgContext();
|
||||
const t = useTranslations();
|
||||
const { env } = useEnvContext();
|
||||
@@ -100,23 +104,26 @@ export function EditPolicyForm({ hidePolicyNameForm }: EditPolicyFormProps) {
|
||||
|
||||
return (
|
||||
<SettingsContainer>
|
||||
{!hidePolicyNameForm && <EditPolicyNameSectionForm />}
|
||||
{!hidePolicyNameForm && <EditPolicyNameSectionForm readonly={readonly} />}
|
||||
|
||||
<EditPolicyUsersRolesSectionForm
|
||||
allRoles={allRoles}
|
||||
allUsers={allUsers}
|
||||
allIdps={allIdps}
|
||||
readonly={readonly}
|
||||
/>
|
||||
|
||||
<EditPolicyAuthMethodsSectionForm />
|
||||
<EditPolicyAuthMethodsSectionForm readonly={readonly} />
|
||||
|
||||
<EditPolicyOtpEmailSectionForm
|
||||
emailEnabled={env.email.emailEnabled}
|
||||
readonly={readonly}
|
||||
/>
|
||||
|
||||
<EditPolicyRulesSectionForm
|
||||
isMaxmindAvailable={isMaxmindAvailable}
|
||||
isMaxmindAsnAvailable={isMaxmindASNAvailable}
|
||||
readonly={readonly}
|
||||
/>
|
||||
</SettingsContainer>
|
||||
);
|
||||
|
||||
@@ -40,7 +40,7 @@ import { useForm } from "react-hook-form";
|
||||
|
||||
// ─── PolicyNameSection ──────────────────────────────────────────────────
|
||||
|
||||
export function EditPolicyNameSectionForm() {
|
||||
export function EditPolicyNameSectionForm({ readonly }: { readonly?: boolean }) {
|
||||
const t = useTranslations();
|
||||
const api = createApiClient(useEnvContext());
|
||||
const router = useRouter();
|
||||
@@ -61,6 +61,7 @@ export function EditPolicyNameSectionForm() {
|
||||
const [, formAction, isSubmitting] = useActionState(onSubmit, null);
|
||||
|
||||
async function onSubmit() {
|
||||
if (readonly) return;
|
||||
const isValid = await form.trigger();
|
||||
|
||||
if (!isValid) return;
|
||||
@@ -125,6 +126,7 @@ export function EditPolicyNameSectionForm() {
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
disabled={readonly}
|
||||
placeholder={t(
|
||||
"resourcePolicyNamePlaceholder"
|
||||
)}
|
||||
@@ -141,7 +143,7 @@ export function EditPolicyNameSectionForm() {
|
||||
<Button
|
||||
type="submit"
|
||||
loading={isSubmitting}
|
||||
disabled={isSubmitting}
|
||||
disabled={readonly || isSubmitting}
|
||||
>
|
||||
{t("saveSettings")}
|
||||
</Button>
|
||||
|
||||
@@ -46,10 +46,12 @@ import { useResourcePolicyContext } from "@app/providers/ResourcePolicyProvider"
|
||||
|
||||
type PolicyOtpEmailSectionProps = {
|
||||
emailEnabled: boolean;
|
||||
readonly?: boolean;
|
||||
};
|
||||
|
||||
export function EditPolicyOtpEmailSectionForm({
|
||||
emailEnabled
|
||||
emailEnabled,
|
||||
readonly
|
||||
}: PolicyOtpEmailSectionProps) {
|
||||
const t = useTranslations();
|
||||
|
||||
@@ -87,6 +89,7 @@ export function EditPolicyOtpEmailSectionForm({
|
||||
const [, formAction, isSubmitting] = useActionState(onSubmit, null);
|
||||
|
||||
async function onSubmit() {
|
||||
if (readonly) return;
|
||||
const isValid = await form.trigger();
|
||||
|
||||
if (!isValid) return;
|
||||
@@ -141,14 +144,16 @@ export function EditPolicyOtpEmailSectionForm({
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setIsExpanded(true)}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
{t("resourcePolicyOtpEmailAdd")}
|
||||
</Button>
|
||||
{!readonly && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setIsExpanded(true)}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
{t("resourcePolicyOtpEmailAdd")}
|
||||
</Button>
|
||||
)}
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
);
|
||||
@@ -186,7 +191,7 @@ export function EditPolicyOtpEmailSectionForm({
|
||||
onCheckedChange={(val) => {
|
||||
form.setValue("emailWhitelistEnabled", val);
|
||||
}}
|
||||
disabled={!emailEnabled}
|
||||
disabled={readonly || !emailEnabled}
|
||||
/>
|
||||
|
||||
{whitelistEnabled && emailEnabled && (
|
||||
@@ -268,7 +273,9 @@ export function EditPolicyOtpEmailSectionForm({
|
||||
<Button
|
||||
type="submit"
|
||||
loading={isSubmitting}
|
||||
disabled={isSubmitting || !emailEnabled}
|
||||
disabled={
|
||||
readonly || isSubmitting || !emailEnabled
|
||||
}
|
||||
>
|
||||
{t("otpEmailWhitelistSave")}
|
||||
</Button>
|
||||
|
||||
@@ -108,11 +108,13 @@ type LocalRule = {
|
||||
type PolicyRulesSectionProps = {
|
||||
isMaxmindAvailable: boolean;
|
||||
isMaxmindAsnAvailable: boolean;
|
||||
readonly?: boolean;
|
||||
};
|
||||
|
||||
export function EditPolicyRulesSectionForm({
|
||||
isMaxmindAvailable,
|
||||
isMaxmindAsnAvailable
|
||||
isMaxmindAsnAvailable,
|
||||
readonly
|
||||
}: PolicyRulesSectionProps) {
|
||||
const t = useTranslations();
|
||||
|
||||
@@ -331,6 +333,7 @@ export function EditPolicyRulesSectionForm({
|
||||
defaultValue={row.original.priority}
|
||||
className="w-[75px]"
|
||||
type="number"
|
||||
disabled={readonly}
|
||||
onClick={(e) => e.currentTarget.focus()}
|
||||
onBlur={(e) => {
|
||||
const parsed = z.coerce
|
||||
@@ -361,6 +364,7 @@ export function EditPolicyRulesSectionForm({
|
||||
cell: ({ row }) => (
|
||||
<Select
|
||||
defaultValue={row.original.action}
|
||||
disabled={readonly}
|
||||
onValueChange={(value: "ACCEPT" | "DROP" | "PASS") =>
|
||||
updateRule(row.original.ruleId, { action: value })
|
||||
}
|
||||
@@ -390,6 +394,7 @@ export function EditPolicyRulesSectionForm({
|
||||
cell: ({ row }) => (
|
||||
<Select
|
||||
defaultValue={row.original.match}
|
||||
disabled={readonly}
|
||||
onValueChange={(
|
||||
value: "CIDR" | "IP" | "PATH" | "COUNTRY" | "ASN"
|
||||
) =>
|
||||
@@ -439,6 +444,7 @@ export function EditPolicyRulesSectionForm({
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
disabled={readonly}
|
||||
className="min-w-50 justify-between"
|
||||
>
|
||||
{row.original.value
|
||||
@@ -494,6 +500,7 @@ export function EditPolicyRulesSectionForm({
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
disabled={readonly}
|
||||
className="min-w-50 justify-between"
|
||||
>
|
||||
{row.original.value
|
||||
@@ -579,6 +586,7 @@ export function EditPolicyRulesSectionForm({
|
||||
<Input
|
||||
defaultValue={row.original.value}
|
||||
className="min-w-50"
|
||||
disabled={readonly}
|
||||
onBlur={(e) =>
|
||||
updateRule(row.original.ruleId, {
|
||||
value: e.target.value
|
||||
@@ -593,6 +601,7 @@ export function EditPolicyRulesSectionForm({
|
||||
cell: ({ row }) => (
|
||||
<Switch
|
||||
defaultChecked={row.original.enabled}
|
||||
disabled={readonly}
|
||||
onCheckedChange={(val) =>
|
||||
updateRule(row.original.ruleId, { enabled: val })
|
||||
}
|
||||
@@ -606,6 +615,7 @@ export function EditPolicyRulesSectionForm({
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={readonly}
|
||||
onClick={() => removeRule(row.original.ruleId)}
|
||||
>
|
||||
{t("delete")}
|
||||
@@ -621,7 +631,8 @@ export function EditPolicyRulesSectionForm({
|
||||
isMaxmindAvailable,
|
||||
isMaxmindAsnAvailable,
|
||||
updateRule,
|
||||
removeRule
|
||||
removeRule,
|
||||
readonly
|
||||
]
|
||||
);
|
||||
|
||||
@@ -638,6 +649,8 @@ export function EditPolicyRulesSectionForm({
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
async function saveRules() {
|
||||
if (readonly) return;
|
||||
|
||||
const isValid = form.trigger();
|
||||
if (!isValid) return;
|
||||
|
||||
@@ -688,14 +701,16 @@ export function EditPolicyRulesSectionForm({
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setIsExpanded(true)}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
{t("resourcePolicyRulesAdd")}
|
||||
</Button>
|
||||
{!readonly && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setIsExpanded(true)}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
{t("resourcePolicyRulesAdd")}
|
||||
</Button>
|
||||
)}
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
);
|
||||
@@ -721,6 +736,7 @@ export function EditPolicyRulesSectionForm({
|
||||
onCheckedChange={(val) => {
|
||||
form.setValue("applyRules", val);
|
||||
}}
|
||||
disabled={readonly}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -741,6 +757,7 @@ export function EditPolicyRulesSectionForm({
|
||||
<FormControl>
|
||||
<Select
|
||||
value={field.value}
|
||||
disabled={readonly || !rulesEnabled}
|
||||
onValueChange={
|
||||
field.onChange
|
||||
}
|
||||
@@ -776,6 +793,7 @@ export function EditPolicyRulesSectionForm({
|
||||
<FormControl>
|
||||
<Select
|
||||
value={field.value}
|
||||
disabled={readonly || !rulesEnabled}
|
||||
onValueChange={
|
||||
field.onChange
|
||||
}
|
||||
@@ -842,6 +860,7 @@ export function EditPolicyRulesSectionForm({
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
disabled={readonly || !rulesEnabled}
|
||||
aria-expanded={
|
||||
openAddRuleCountrySelect
|
||||
}
|
||||
@@ -931,6 +950,7 @@ export function EditPolicyRulesSectionForm({
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
disabled={readonly || !rulesEnabled}
|
||||
aria-expanded={
|
||||
openAddRuleAsnSelect
|
||||
}
|
||||
@@ -1043,7 +1063,7 @@ export function EditPolicyRulesSectionForm({
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
) : (
|
||||
<Input {...field} />
|
||||
<Input {...field} disabled={readonly || !rulesEnabled} />
|
||||
)}
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
@@ -1053,7 +1073,7 @@ export function EditPolicyRulesSectionForm({
|
||||
<Button
|
||||
type="submit"
|
||||
variant="outline"
|
||||
disabled={!rulesEnabled}
|
||||
disabled={readonly || !rulesEnabled}
|
||||
>
|
||||
{t("ruleSubmit")}
|
||||
</Button>
|
||||
@@ -1134,7 +1154,7 @@ export function EditPolicyRulesSectionForm({
|
||||
<Button
|
||||
onClick={() => startTransition(() => saveRules())}
|
||||
loading={isPending}
|
||||
disabled={isPending}
|
||||
disabled={readonly || isPending}
|
||||
>
|
||||
{t("rulesSave")}
|
||||
</Button>
|
||||
|
||||
@@ -53,12 +53,14 @@ type PolicyUsersRolesSectionProps = {
|
||||
allRoles: { id: string; text: string }[];
|
||||
allUsers: { id: string; text: string }[];
|
||||
allIdps: { id: number; text: string }[];
|
||||
readonly?: boolean;
|
||||
};
|
||||
|
||||
export function EditPolicyUsersRolesSectionForm({
|
||||
allRoles,
|
||||
allUsers,
|
||||
allIdps
|
||||
allIdps,
|
||||
readonly
|
||||
}: PolicyUsersRolesSectionProps) {
|
||||
const t = useTranslations();
|
||||
|
||||
@@ -106,6 +108,8 @@ export function EditPolicyUsersRolesSectionForm({
|
||||
const [, formAction, isSubmitting] = useActionState(onSubmit, null);
|
||||
|
||||
async function onSubmit() {
|
||||
if (readonly) return;
|
||||
|
||||
const isValid = await form.trigger();
|
||||
|
||||
if (!isValid) return;
|
||||
@@ -172,6 +176,7 @@ export function EditPolicyUsersRolesSectionForm({
|
||||
console.log(`form.setValue("sso", ${val})`);
|
||||
form.setValue("sso", val);
|
||||
}}
|
||||
disabled={readonly}
|
||||
/>
|
||||
|
||||
{ssoEnabled && (
|
||||
@@ -221,6 +226,7 @@ export function EditPolicyUsersRolesSectionForm({
|
||||
true
|
||||
}
|
||||
sortTags={true}
|
||||
disabled={readonly}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
@@ -277,6 +283,7 @@ export function EditPolicyUsersRolesSectionForm({
|
||||
true
|
||||
}
|
||||
sortTags={true}
|
||||
disabled={readonly}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
@@ -292,6 +299,7 @@ export function EditPolicyUsersRolesSectionForm({
|
||||
{t("defaultIdentityProvider")}
|
||||
</label>
|
||||
<Select
|
||||
disabled={readonly}
|
||||
onValueChange={(value) => {
|
||||
if (value === "none") {
|
||||
form.setValue(
|
||||
@@ -347,7 +355,7 @@ export function EditPolicyUsersRolesSectionForm({
|
||||
<Button
|
||||
type="submit"
|
||||
loading={isSubmitting}
|
||||
disabled={isSubmitting}
|
||||
disabled={readonly || isSubmitting}
|
||||
>
|
||||
{t("resourceUsersRolesSubmit")}
|
||||
</Button>
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { ListClientsResponse } from "@server/routers/client";
|
||||
import type { ListDomainsResponse } from "@server/routers/domain";
|
||||
import type {
|
||||
GetResourceWhitelistResponse,
|
||||
GetResourcePoliciesResponse,
|
||||
GetDefaultResourcePolicyResponse,
|
||||
ListResourceNamesResponse,
|
||||
ListResourcesResponse,
|
||||
ListResourceRolesResponse,
|
||||
@@ -323,13 +323,13 @@ export const resourceQueries = {
|
||||
return res.data.data.whitelist;
|
||||
}
|
||||
}),
|
||||
policies: ({ resourceId }: { resourceId: number }) =>
|
||||
defaultPolicy: ({ resourceId }: { resourceId: number }) =>
|
||||
queryOptions({
|
||||
queryKey: ["RESOURCES", resourceId, "POLICIES"] as const,
|
||||
queryFn: async ({ signal, meta }) => {
|
||||
const res = await meta!.api.get<
|
||||
AxiosResponse<GetResourcePoliciesResponse>
|
||||
>(`/resource/${resourceId}/policies`, { signal });
|
||||
AxiosResponse<GetDefaultResourcePolicyResponse>
|
||||
>(`/resource/${resourceId}/default-policy`, { signal });
|
||||
|
||||
return res.data.data;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user