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", setResourcePolicyHeaderAuth = "setResourcePolicyHeaderAuth",
setResourcePolicyWhitelist = "setResourcePolicyWhitelist", setResourcePolicyWhitelist = "setResourcePolicyWhitelist",
setResourcePolicyRules = "setResourcePolicyRules", setResourcePolicyRules = "setResourcePolicyRules",
getResourcePolicies = "getResourcePolicies"
} }
export async function checkUserActionPermission( export async function checkUserActionPermission(

View File

@@ -637,10 +637,10 @@ authenticated.get(
); );
authenticated.get( authenticated.get(
"/resource/:resourceId/policies", "/resource/:resourceId/default-policy",
verifyResourceAccess, verifyResourceAccess,
verifyUserHasAction(ActionsEnum.getResourcePolicies), verifyUserHasAction(ActionsEnum.getResourcePolicy),
resource.getResourcePolicies resource.getDefaultResourcePolicy
); );
authenticated.put( authenticated.put(

View File

@@ -454,10 +454,10 @@ authenticated.get(
); );
authenticated.get( authenticated.get(
"/resource/:resourceId/policies", "/resource/:resourceId/default-policy",
verifyApiKeyResourceAccess, verifyApiKeyResourceAccess,
verifyApiKeyHasAction(ActionsEnum.getResourcePolicies), verifyApiKeyHasAction(ActionsEnum.getResourcePolicy),
resource.getResourcePolicies resource.getDefaultResourcePolicy
); );
authenticated.post( authenticated.post(

View File

@@ -17,13 +17,11 @@ const getResourcePoliciesParamsSchema = z.strictObject({
resourceId: z.string().transform(Number).pipe(z.int().positive()) resourceId: z.string().transform(Number).pipe(z.int().positive())
}); });
export type GetResourcePoliciesResponse = { export type GetDefaultResourcePolicyResponse = GetResourcePolicyResponse;
defaultPolicy: GetResourcePolicyResponse | null;
};
registry.registerPath({ registry.registerPath({
method: "get", method: "get",
path: "/resource/{resourceId}/policies", path: "/resource/{resourceId}/default-policy",
description: "Get the default policy for a resource.", description: "Get the default policy for a resource.",
tags: [OpenAPITags.PublicResource, OpenAPITags.Policy], tags: [OpenAPITags.PublicResource, OpenAPITags.Policy],
request: { request: {
@@ -32,7 +30,7 @@ registry.registerPath({
responses: {} responses: {}
}); });
export async function getResourcePolicies( export async function getDefaultResourcePolicy(
req: Request, req: Request,
res: Response, res: Response,
next: NextFunction next: NextFunction
@@ -66,14 +64,20 @@ export async function getResourcePolicies(
); );
} }
const defaultPolicy = resource.defaultResourcePolicyId if (!resource.defaultResourcePolicyId) {
? await queryResourcePolicy({ return next(
resourcePolicyId: resource.defaultResourcePolicyId createHttpError(
}) HttpCode.NOT_FOUND,
: null; "Resource has no default policy"
)
);
}
return response<GetResourcePoliciesResponse>(res, { const defaultPolicy = await queryResourcePolicy({
data: { defaultPolicy }, resourcePolicyId: resource.defaultResourcePolicyId
});
return response<GetDefaultResourcePolicyResponse>(res, {
data: defaultPolicy,
success: true, success: true,
error: false, error: false,
message: "Resource policies retrieved successfully", message: "Resource policies retrieved successfully",

View File

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

View File

@@ -1,5 +1,6 @@
"use client"; "use client";
import { EditPolicyForm } from "@app/components/resource-policy/EditPolicyForm";
import SetResourceHeaderAuthForm from "@app/components/SetResourceHeaderAuthForm"; import SetResourceHeaderAuthForm from "@app/components/SetResourceHeaderAuthForm";
import SetResourcePincodeForm from "@app/components/SetResourcePincodeForm"; import SetResourcePincodeForm from "@app/components/SetResourcePincodeForm";
import { import {
@@ -46,7 +47,10 @@ import { toast } from "@app/hooks/useToast";
import { createApiClient, formatAxiosError } from "@app/lib/api"; import { createApiClient, formatAxiosError } from "@app/lib/api";
import { getUserDisplayName } from "@app/lib/getUserDisplayName"; import { getUserDisplayName } from "@app/lib/getUserDisplayName";
import { orgQueries, resourceQueries } from "@app/lib/queries"; 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 { zodResolver } from "@hookform/resolvers/zod";
import { build } from "@server/build"; import { build } from "@server/build";
import { tierMatrix } from "@server/lib/billing/tierMatrix"; import { tierMatrix } from "@server/lib/billing/tierMatrix";
@@ -64,32 +68,19 @@ import {
useState, useState,
useTransition useTransition
} from "react"; } from "react";
import { useForm } from "react-hook-form"; import { useForm, useWatch } from "react-hook-form";
import { z } from "zod"; import { z } from "zod";
const UsersRolesFormSchema = z.object({ const resourceTypeSchema = z
roles: z.array( .object({
type: z.literal("inline")
})
.or(
z.object({ z.object({
id: z.string(), type: z.literal("shared"),
text: z.string() 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">; type ResourcePolicyType = StrategyOption<"inline" | "shared">;
@@ -106,114 +97,14 @@ export default function ResourceAuthenticationPage() {
const { isPaidUser } = usePaidStatus(); const { isPaidUser } = usePaidStatus();
const queryClient = useQueryClient(); const { data: defaultPolicy, isLoading: isLoadingPolicies } = useQuery(
const { data: resourceRoles = [], isLoading: isLoadingResourceRoles } = resourceQueries.defaultPolicy({
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({
resourceId: resource.resourceId resourceId: resource.resourceId
}) })
); );
const { data: orgRoles = [], isLoading: isLoadingOrgRoles } = useQuery( const pageLoading = isLoadingPolicies || !defaultPolicy;
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 =
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 [ const [
loadingRemoveResourceHeaderAuth, loadingRemoveResourceHeaderAuth,
setLoadingRemoveResourceHeaderAuth setLoadingRemoveResourceHeaderAuth
@@ -223,209 +114,6 @@ export default function ResourceAuthenticationPage() {
const [isSetPincodeOpen, setIsSetPincodeOpen] = useState(false); const [isSetPincodeOpen, setIsSetPincodeOpen] = useState(false);
const [isSetHeaderAuthOpen, setIsSetHeaderAuthOpen] = 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> = [ const resourcePolicyTypes: Array<ResourcePolicyType> = [
{ {
id: "inline", id: "inline",
@@ -439,8 +127,17 @@ export default function ResourceAuthenticationPage() {
} }
]; ];
const [selectedResourceType, setSelectedResourceType] = const form = useForm({
useState<ResourcePolicyType["id"]>("inline"); resolver: zodResolver(resourceTypeSchema),
defaultValues: {
type: "inline"
}
});
const selectedResourceType = useWatch({
control: form.control,
name: "type"
});
if (pageLoading) { if (pageLoading) {
return <></>; return <></>;
@@ -503,13 +200,9 @@ export default function ResourceAuthenticationPage() {
<SettingsSectionBody> <SettingsSectionBody>
<StrategySelect <StrategySelect
options={resourcePolicyTypes} options={resourcePolicyTypes}
defaultValue="inline" value={selectedResourceType}
onChange={(value) => { onChange={(value) => {
// baseForm.setValue( form.setValue("type", value);
// "http",
// value === "http"
// );
// // Update method default when switching resource type
}} }}
cols={2} cols={2}
/> />
@@ -524,223 +217,10 @@ export default function ResourceAuthenticationPage() {
</Button> </Button>
</SettingsSectionFooter> </SettingsSectionFooter>
</SettingsSection> </SettingsSection>
{/* <ResourcePolicyContext value={policies?.defaultPolicy}> <ResourcePolicyProvider policy={defaultPolicy}>
<EditPolicyForm hidePolicyNameForm />
</ResourcePolicyContext> */} </ResourcePolicyProvider>
</SettingsContainer> </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() extendedCompatibility: z.boolean()
}); });
export function EditPolicyAuthMethodsSectionForm() { export function EditPolicyAuthMethodsSectionForm({
readonly
}: {
readonly?: boolean;
}) {
const { policy } = useResourcePolicyContext(); const { policy } = useResourcePolicyContext();
const router = useRouter(); const router = useRouter();
@@ -132,6 +136,7 @@ export function EditPolicyAuthMethodsSectionForm() {
const [, formAction, isSubmitting] = useActionState(onSubmit, null); const [, formAction, isSubmitting] = useActionState(onSubmit, null);
async function onSubmit() { async function onSubmit() {
if (readonly) return;
const isValid = await form.trigger(); const isValid = await form.trigger();
if (!isValid) return; if (!isValid) return;
@@ -237,14 +242,16 @@ export function EditPolicyAuthMethodsSectionForm() {
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
<SettingsSectionBody> <SettingsSectionBody>
<Button {!readonly && (
type="button" <Button
variant="outline" type="button"
onClick={() => setIsExpanded(true)} variant="outline"
> onClick={() => setIsExpanded(true)}
<Plus className="mr-2 h-4 w-4" /> >
{t("resourcePolicyAuthMethodAdd")} <Plus className="mr-2 h-4 w-4" />
</Button> {t("resourcePolicyAuthMethodAdd")}
</Button>
)}
</SettingsSectionBody> </SettingsSectionBody>
</SettingsSection> </SettingsSection>
); );
@@ -541,6 +548,7 @@ export function EditPolicyAuthMethodsSectionForm() {
type="button" type="button"
variant="secondary" variant="secondary"
size="sm" size="sm"
disabled={readonly}
onClick={ onClick={
hasPassword hasPassword
? () => ? () =>
@@ -579,6 +587,7 @@ export function EditPolicyAuthMethodsSectionForm() {
type="button" type="button"
variant="secondary" variant="secondary"
size="sm" size="sm"
disabled={readonly}
onClick={ onClick={
hasPincode hasPincode
? () => ? () =>
@@ -619,6 +628,7 @@ export function EditPolicyAuthMethodsSectionForm() {
type="button" type="button"
variant="secondary" variant="secondary"
size="sm" size="sm"
disabled={readonly}
onClick={ onClick={
hasHeaderAuth hasHeaderAuth
? () => ? () =>
@@ -644,7 +654,7 @@ export function EditPolicyAuthMethodsSectionForm() {
<Button <Button
type="submit" type="submit"
loading={isSubmitting} loading={isSubmitting}
disabled={isSubmitting} disabled={readonly || isSubmitting}
> >
{t("authMethodsSave")} {t("authMethodsSave")}
</Button> </Button>

View File

@@ -28,9 +28,13 @@ import { EditPolicyRulesSectionForm } from "./EditPolicyRulesSectionForm";
export type EditPolicyFormProps = { export type EditPolicyFormProps = {
hidePolicyNameForm?: boolean; hidePolicyNameForm?: boolean;
readonly?: boolean;
}; };
export function EditPolicyForm({ hidePolicyNameForm }: EditPolicyFormProps) { export function EditPolicyForm({
hidePolicyNameForm,
readonly
}: EditPolicyFormProps) {
const { org } = useOrgContext(); const { org } = useOrgContext();
const t = useTranslations(); const t = useTranslations();
const { env } = useEnvContext(); const { env } = useEnvContext();
@@ -100,23 +104,26 @@ export function EditPolicyForm({ hidePolicyNameForm }: EditPolicyFormProps) {
return ( return (
<SettingsContainer> <SettingsContainer>
{!hidePolicyNameForm && <EditPolicyNameSectionForm />} {!hidePolicyNameForm && <EditPolicyNameSectionForm readonly={readonly} />}
<EditPolicyUsersRolesSectionForm <EditPolicyUsersRolesSectionForm
allRoles={allRoles} allRoles={allRoles}
allUsers={allUsers} allUsers={allUsers}
allIdps={allIdps} allIdps={allIdps}
readonly={readonly}
/> />
<EditPolicyAuthMethodsSectionForm /> <EditPolicyAuthMethodsSectionForm readonly={readonly} />
<EditPolicyOtpEmailSectionForm <EditPolicyOtpEmailSectionForm
emailEnabled={env.email.emailEnabled} emailEnabled={env.email.emailEnabled}
readonly={readonly}
/> />
<EditPolicyRulesSectionForm <EditPolicyRulesSectionForm
isMaxmindAvailable={isMaxmindAvailable} isMaxmindAvailable={isMaxmindAvailable}
isMaxmindAsnAvailable={isMaxmindASNAvailable} isMaxmindAsnAvailable={isMaxmindASNAvailable}
readonly={readonly}
/> />
</SettingsContainer> </SettingsContainer>
); );

View File

@@ -40,7 +40,7 @@ import { useForm } from "react-hook-form";
// ─── PolicyNameSection ────────────────────────────────────────────────── // ─── PolicyNameSection ──────────────────────────────────────────────────
export function EditPolicyNameSectionForm() { export function EditPolicyNameSectionForm({ readonly }: { readonly?: boolean }) {
const t = useTranslations(); const t = useTranslations();
const api = createApiClient(useEnvContext()); const api = createApiClient(useEnvContext());
const router = useRouter(); const router = useRouter();
@@ -61,6 +61,7 @@ export function EditPolicyNameSectionForm() {
const [, formAction, isSubmitting] = useActionState(onSubmit, null); const [, formAction, isSubmitting] = useActionState(onSubmit, null);
async function onSubmit() { async function onSubmit() {
if (readonly) return;
const isValid = await form.trigger(); const isValid = await form.trigger();
if (!isValid) return; if (!isValid) return;
@@ -125,6 +126,7 @@ export function EditPolicyNameSectionForm() {
<FormControl> <FormControl>
<Input <Input
{...field} {...field}
disabled={readonly}
placeholder={t( placeholder={t(
"resourcePolicyNamePlaceholder" "resourcePolicyNamePlaceholder"
)} )}
@@ -141,7 +143,7 @@ export function EditPolicyNameSectionForm() {
<Button <Button
type="submit" type="submit"
loading={isSubmitting} loading={isSubmitting}
disabled={isSubmitting} disabled={readonly || isSubmitting}
> >
{t("saveSettings")} {t("saveSettings")}
</Button> </Button>

View File

@@ -46,10 +46,12 @@ import { useResourcePolicyContext } from "@app/providers/ResourcePolicyProvider"
type PolicyOtpEmailSectionProps = { type PolicyOtpEmailSectionProps = {
emailEnabled: boolean; emailEnabled: boolean;
readonly?: boolean;
}; };
export function EditPolicyOtpEmailSectionForm({ export function EditPolicyOtpEmailSectionForm({
emailEnabled emailEnabled,
readonly
}: PolicyOtpEmailSectionProps) { }: PolicyOtpEmailSectionProps) {
const t = useTranslations(); const t = useTranslations();
@@ -87,6 +89,7 @@ export function EditPolicyOtpEmailSectionForm({
const [, formAction, isSubmitting] = useActionState(onSubmit, null); const [, formAction, isSubmitting] = useActionState(onSubmit, null);
async function onSubmit() { async function onSubmit() {
if (readonly) return;
const isValid = await form.trigger(); const isValid = await form.trigger();
if (!isValid) return; if (!isValid) return;
@@ -141,14 +144,16 @@ export function EditPolicyOtpEmailSectionForm({
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
<SettingsSectionBody> <SettingsSectionBody>
<Button {!readonly && (
type="button" <Button
variant="outline" type="button"
onClick={() => setIsExpanded(true)} variant="outline"
> onClick={() => setIsExpanded(true)}
<Plus className="mr-2 h-4 w-4" /> >
{t("resourcePolicyOtpEmailAdd")} <Plus className="mr-2 h-4 w-4" />
</Button> {t("resourcePolicyOtpEmailAdd")}
</Button>
)}
</SettingsSectionBody> </SettingsSectionBody>
</SettingsSection> </SettingsSection>
); );
@@ -186,7 +191,7 @@ export function EditPolicyOtpEmailSectionForm({
onCheckedChange={(val) => { onCheckedChange={(val) => {
form.setValue("emailWhitelistEnabled", val); form.setValue("emailWhitelistEnabled", val);
}} }}
disabled={!emailEnabled} disabled={readonly || !emailEnabled}
/> />
{whitelistEnabled && emailEnabled && ( {whitelistEnabled && emailEnabled && (
@@ -268,7 +273,9 @@ export function EditPolicyOtpEmailSectionForm({
<Button <Button
type="submit" type="submit"
loading={isSubmitting} loading={isSubmitting}
disabled={isSubmitting || !emailEnabled} disabled={
readonly || isSubmitting || !emailEnabled
}
> >
{t("otpEmailWhitelistSave")} {t("otpEmailWhitelistSave")}
</Button> </Button>

View File

@@ -108,11 +108,13 @@ type LocalRule = {
type PolicyRulesSectionProps = { type PolicyRulesSectionProps = {
isMaxmindAvailable: boolean; isMaxmindAvailable: boolean;
isMaxmindAsnAvailable: boolean; isMaxmindAsnAvailable: boolean;
readonly?: boolean;
}; };
export function EditPolicyRulesSectionForm({ export function EditPolicyRulesSectionForm({
isMaxmindAvailable, isMaxmindAvailable,
isMaxmindAsnAvailable isMaxmindAsnAvailable,
readonly
}: PolicyRulesSectionProps) { }: PolicyRulesSectionProps) {
const t = useTranslations(); const t = useTranslations();
@@ -331,6 +333,7 @@ export function EditPolicyRulesSectionForm({
defaultValue={row.original.priority} defaultValue={row.original.priority}
className="w-[75px]" className="w-[75px]"
type="number" type="number"
disabled={readonly}
onClick={(e) => e.currentTarget.focus()} onClick={(e) => e.currentTarget.focus()}
onBlur={(e) => { onBlur={(e) => {
const parsed = z.coerce const parsed = z.coerce
@@ -361,6 +364,7 @@ export function EditPolicyRulesSectionForm({
cell: ({ row }) => ( cell: ({ row }) => (
<Select <Select
defaultValue={row.original.action} defaultValue={row.original.action}
disabled={readonly}
onValueChange={(value: "ACCEPT" | "DROP" | "PASS") => onValueChange={(value: "ACCEPT" | "DROP" | "PASS") =>
updateRule(row.original.ruleId, { action: value }) updateRule(row.original.ruleId, { action: value })
} }
@@ -390,6 +394,7 @@ export function EditPolicyRulesSectionForm({
cell: ({ row }) => ( cell: ({ row }) => (
<Select <Select
defaultValue={row.original.match} defaultValue={row.original.match}
disabled={readonly}
onValueChange={( onValueChange={(
value: "CIDR" | "IP" | "PATH" | "COUNTRY" | "ASN" value: "CIDR" | "IP" | "PATH" | "COUNTRY" | "ASN"
) => ) =>
@@ -439,6 +444,7 @@ export function EditPolicyRulesSectionForm({
<Button <Button
variant="outline" variant="outline"
role="combobox" role="combobox"
disabled={readonly}
className="min-w-50 justify-between" className="min-w-50 justify-between"
> >
{row.original.value {row.original.value
@@ -494,6 +500,7 @@ export function EditPolicyRulesSectionForm({
<Button <Button
variant="outline" variant="outline"
role="combobox" role="combobox"
disabled={readonly}
className="min-w-50 justify-between" className="min-w-50 justify-between"
> >
{row.original.value {row.original.value
@@ -579,6 +586,7 @@ export function EditPolicyRulesSectionForm({
<Input <Input
defaultValue={row.original.value} defaultValue={row.original.value}
className="min-w-50" className="min-w-50"
disabled={readonly}
onBlur={(e) => onBlur={(e) =>
updateRule(row.original.ruleId, { updateRule(row.original.ruleId, {
value: e.target.value value: e.target.value
@@ -593,6 +601,7 @@ export function EditPolicyRulesSectionForm({
cell: ({ row }) => ( cell: ({ row }) => (
<Switch <Switch
defaultChecked={row.original.enabled} defaultChecked={row.original.enabled}
disabled={readonly}
onCheckedChange={(val) => onCheckedChange={(val) =>
updateRule(row.original.ruleId, { enabled: val }) updateRule(row.original.ruleId, { enabled: val })
} }
@@ -606,6 +615,7 @@ export function EditPolicyRulesSectionForm({
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Button <Button
variant="outline" variant="outline"
disabled={readonly}
onClick={() => removeRule(row.original.ruleId)} onClick={() => removeRule(row.original.ruleId)}
> >
{t("delete")} {t("delete")}
@@ -621,7 +631,8 @@ export function EditPolicyRulesSectionForm({
isMaxmindAvailable, isMaxmindAvailable,
isMaxmindAsnAvailable, isMaxmindAsnAvailable,
updateRule, updateRule,
removeRule removeRule,
readonly
] ]
); );
@@ -638,6 +649,8 @@ export function EditPolicyRulesSectionForm({
const [isPending, startTransition] = useTransition(); const [isPending, startTransition] = useTransition();
async function saveRules() { async function saveRules() {
if (readonly) return;
const isValid = form.trigger(); const isValid = form.trigger();
if (!isValid) return; if (!isValid) return;
@@ -688,14 +701,16 @@ export function EditPolicyRulesSectionForm({
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
<SettingsSectionBody> <SettingsSectionBody>
<Button {!readonly && (
type="button" <Button
variant="outline" type="button"
onClick={() => setIsExpanded(true)} variant="outline"
> onClick={() => setIsExpanded(true)}
<Plus className="mr-2 h-4 w-4" /> >
{t("resourcePolicyRulesAdd")} <Plus className="mr-2 h-4 w-4" />
</Button> {t("resourcePolicyRulesAdd")}
</Button>
)}
</SettingsSectionBody> </SettingsSectionBody>
</SettingsSection> </SettingsSection>
); );
@@ -721,6 +736,7 @@ export function EditPolicyRulesSectionForm({
onCheckedChange={(val) => { onCheckedChange={(val) => {
form.setValue("applyRules", val); form.setValue("applyRules", val);
}} }}
disabled={readonly}
/> />
</div> </div>
@@ -741,6 +757,7 @@ export function EditPolicyRulesSectionForm({
<FormControl> <FormControl>
<Select <Select
value={field.value} value={field.value}
disabled={readonly || !rulesEnabled}
onValueChange={ onValueChange={
field.onChange field.onChange
} }
@@ -776,6 +793,7 @@ export function EditPolicyRulesSectionForm({
<FormControl> <FormControl>
<Select <Select
value={field.value} value={field.value}
disabled={readonly || !rulesEnabled}
onValueChange={ onValueChange={
field.onChange field.onChange
} }
@@ -842,6 +860,7 @@ export function EditPolicyRulesSectionForm({
<Button <Button
variant="outline" variant="outline"
role="combobox" role="combobox"
disabled={readonly || !rulesEnabled}
aria-expanded={ aria-expanded={
openAddRuleCountrySelect openAddRuleCountrySelect
} }
@@ -931,6 +950,7 @@ export function EditPolicyRulesSectionForm({
<Button <Button
variant="outline" variant="outline"
role="combobox" role="combobox"
disabled={readonly || !rulesEnabled}
aria-expanded={ aria-expanded={
openAddRuleAsnSelect openAddRuleAsnSelect
} }
@@ -1043,7 +1063,7 @@ export function EditPolicyRulesSectionForm({
</PopoverContent> </PopoverContent>
</Popover> </Popover>
) : ( ) : (
<Input {...field} /> <Input {...field} disabled={readonly || !rulesEnabled} />
)} )}
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
@@ -1053,7 +1073,7 @@ export function EditPolicyRulesSectionForm({
<Button <Button
type="submit" type="submit"
variant="outline" variant="outline"
disabled={!rulesEnabled} disabled={readonly || !rulesEnabled}
> >
{t("ruleSubmit")} {t("ruleSubmit")}
</Button> </Button>
@@ -1134,7 +1154,7 @@ export function EditPolicyRulesSectionForm({
<Button <Button
onClick={() => startTransition(() => saveRules())} onClick={() => startTransition(() => saveRules())}
loading={isPending} loading={isPending}
disabled={isPending} disabled={readonly || isPending}
> >
{t("rulesSave")} {t("rulesSave")}
</Button> </Button>

View File

@@ -53,12 +53,14 @@ type PolicyUsersRolesSectionProps = {
allRoles: { id: string; text: string }[]; allRoles: { id: string; text: string }[];
allUsers: { id: string; text: string }[]; allUsers: { id: string; text: string }[];
allIdps: { id: number; text: string }[]; allIdps: { id: number; text: string }[];
readonly?: boolean;
}; };
export function EditPolicyUsersRolesSectionForm({ export function EditPolicyUsersRolesSectionForm({
allRoles, allRoles,
allUsers, allUsers,
allIdps allIdps,
readonly
}: PolicyUsersRolesSectionProps) { }: PolicyUsersRolesSectionProps) {
const t = useTranslations(); const t = useTranslations();
@@ -106,6 +108,8 @@ export function EditPolicyUsersRolesSectionForm({
const [, formAction, isSubmitting] = useActionState(onSubmit, null); const [, formAction, isSubmitting] = useActionState(onSubmit, null);
async function onSubmit() { async function onSubmit() {
if (readonly) return;
const isValid = await form.trigger(); const isValid = await form.trigger();
if (!isValid) return; if (!isValid) return;
@@ -172,6 +176,7 @@ export function EditPolicyUsersRolesSectionForm({
console.log(`form.setValue("sso", ${val})`); console.log(`form.setValue("sso", ${val})`);
form.setValue("sso", val); form.setValue("sso", val);
}} }}
disabled={readonly}
/> />
{ssoEnabled && ( {ssoEnabled && (
@@ -221,6 +226,7 @@ export function EditPolicyUsersRolesSectionForm({
true true
} }
sortTags={true} sortTags={true}
disabled={readonly}
/> />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
@@ -277,6 +283,7 @@ export function EditPolicyUsersRolesSectionForm({
true true
} }
sortTags={true} sortTags={true}
disabled={readonly}
/> />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
@@ -292,6 +299,7 @@ export function EditPolicyUsersRolesSectionForm({
{t("defaultIdentityProvider")} {t("defaultIdentityProvider")}
</label> </label>
<Select <Select
disabled={readonly}
onValueChange={(value) => { onValueChange={(value) => {
if (value === "none") { if (value === "none") {
form.setValue( form.setValue(
@@ -347,7 +355,7 @@ export function EditPolicyUsersRolesSectionForm({
<Button <Button
type="submit" type="submit"
loading={isSubmitting} loading={isSubmitting}
disabled={isSubmitting} disabled={readonly || isSubmitting}
> >
{t("resourceUsersRolesSubmit")} {t("resourceUsersRolesSubmit")}
</Button> </Button>

View File

@@ -4,7 +4,7 @@ import type { ListClientsResponse } from "@server/routers/client";
import type { ListDomainsResponse } from "@server/routers/domain"; import type { ListDomainsResponse } from "@server/routers/domain";
import type { import type {
GetResourceWhitelistResponse, GetResourceWhitelistResponse,
GetResourcePoliciesResponse, GetDefaultResourcePolicyResponse,
ListResourceNamesResponse, ListResourceNamesResponse,
ListResourcesResponse, ListResourcesResponse,
ListResourceRolesResponse, ListResourceRolesResponse,
@@ -323,13 +323,13 @@ export const resourceQueries = {
return res.data.data.whitelist; return res.data.data.whitelist;
} }
}), }),
policies: ({ resourceId }: { resourceId: number }) => defaultPolicy: ({ resourceId }: { resourceId: number }) =>
queryOptions({ queryOptions({
queryKey: ["RESOURCES", resourceId, "POLICIES"] as const, queryKey: ["RESOURCES", resourceId, "POLICIES"] as const,
queryFn: async ({ signal, meta }) => { queryFn: async ({ signal, meta }) => {
const res = await meta!.api.get< const res = await meta!.api.get<
AxiosResponse<GetResourcePoliciesResponse> AxiosResponse<GetDefaultResourcePolicyResponse>
>(`/resource/${resourceId}/policies`, { signal }); >(`/resource/${resourceId}/default-policy`, { signal });
return res.data.data; return res.data.data;
} }