This commit is contained in:
Fred KISSIE
2026-03-11 00:27:27 +01:00
parent 8a39b3fd45
commit f80e212b07
13 changed files with 156 additions and 618 deletions

View File

@@ -147,7 +147,7 @@ export enum ActionsEnum {
setResourcePolicyHeaderAuth = "setResourcePolicyHeaderAuth",
setResourcePolicyWhitelist = "setResourcePolicyWhitelist",
setResourcePolicyRules = "setResourcePolicyRules",
getResourcePolicies = "getResourcePolicies"
}
export async function checkUserActionPermission(

View File

@@ -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(

View File

@@ -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(

View File

@@ -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",

View File

@@ -31,4 +31,4 @@ export * from "./addUserToResource";
export * from "./removeUserFromResource";
export * from "./listAllResourceNames";
export * from "./removeEmailFromResourceWhitelist";
export * from "./getResourcePolicies";
export * from "./getDefaultResourcePolicy";

View File

@@ -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>
);
}

View File

@@ -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>

View File

@@ -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>
);

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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;
}