🚧 wip: update resource policy form

This commit is contained in:
Fred KISSIE
2026-02-27 04:21:20 +01:00
parent c5231d37f6
commit d6a8021613
8 changed files with 272 additions and 149 deletions

View File

@@ -642,7 +642,11 @@
"policyErrorCreate": "Error creating policy",
"policyErrorCreateDescription": "An error occurred when creating the policy",
"policyErrorCreateMessageDescription": "An unexpected error occurred",
"policyErrorUpdate": "Error updating policy",
"policyErrorUpdateDescription": "An error occurred when updating the policy",
"policyErrorUpdateMessageDescription": "An unexpected error occurred",
"policyCreatedSuccess": "Resource policy succesfully created",
"policyUpdatedSuccess": "Resource policy succesfully updated",
"resourceErrorCreate": "Error creating resource",
"resourceErrorCreateDescription": "An error occurred when creating the resource",
"resourceErrorCreateMessage": "Error creating resource:",

View File

@@ -638,6 +638,13 @@ authenticated.get(
policy.getResourcePolicy
);
authenticated.put(
"/resource-policy/:resourcePolicyId",
verifyResourcePolicyAccess,
verifyUserHasAction(ActionsEnum.updateResourcePolicy),
policy.updateResourcePolicy
);
// authenticated.get(
// "/role/:roleId",
// verifyRoleAccess,

View File

@@ -1 +1,2 @@
export * from "./getResourcePolicy";
export * from "./updateResourcePolicy";

View File

@@ -4,14 +4,7 @@ import { OpenAPITags, registry } from "@server/openApi";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import { fromError } from "zod-validation-error";
import {
db,
orgs,
resourcePolicies,
rolePolicies,
userPolicies,
type ResourcePolicy
} from "@server/db";
import { db, orgs, resourcePolicies, type ResourcePolicy } from "@server/db";
import { and, eq } from "drizzle-orm";
import logger from "@server/logger";
import response from "@server/lib/response";

View File

@@ -3,7 +3,7 @@ import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import { Button } from "@app/components/ui/button";
import { internal } from "@app/lib/api";
import { authCookieHeader } from "@app/lib/api/cookies";
import type { ResourcePolicy } from "@server/db";
import { ResourcePolicyProvider } from "@app/providers/ResourcePolicyProvider";
import type { GetResourcePolicyResponse } from "@server/routers/policy";
import type { AxiosResponse } from "axios";
import { getTranslations } from "next-intl/server";
@@ -18,7 +18,7 @@ export default async function EditPolicyPage(props: EditPolicyPageProps) {
const params = await props.params;
const t = await getTranslations();
let policy: ResourcePolicy | null = null;
let policyResponse: GetResourcePolicyResponse | null = null;
try {
const res = await internal.get<
AxiosResponse<GetResourcePolicyResponse>
@@ -26,12 +26,12 @@ export default async function EditPolicyPage(props: EditPolicyPageProps) {
`/org/${params.orgId}/resource-policy/${params.niceId}`,
await authCookieHeader()
);
policy = res.data.data.policy;
policyResponse = res.data.data;
} catch {
redirect(`/${params.orgId}/settings/policies/resource`);
}
if (!policy) {
if (!policyResponse) {
redirect(`/${params.orgId}/settings/policies/resource`);
}
@@ -40,7 +40,7 @@ export default async function EditPolicyPage(props: EditPolicyPageProps) {
<div className="flex justify-between">
<SettingsSectionTitle
title={t("resourcePolicySetting", {
policyName: policy.name
policyName: policyResponse.policy.name
})}
description={t("resourcePolicySettingDescription")}
/>
@@ -52,7 +52,9 @@ export default async function EditPolicyPage(props: EditPolicyPageProps) {
</Button>
</div>
<EditPolicyForm policy={policy} />
<ResourcePolicyProvider policy={policyResponse}>
<EditPolicyForm />
</ResourcePolicyProvider>
</>
);
}

View File

@@ -204,14 +204,10 @@ export function CreatePolicyForm({}: CreatePolicyFormProps) {
});
if (res && res.status === 201) {
const id = res.data.data.resourcePolicyId;
const niceId = res.data.data.niceId;
router.push(`/${org.org.orgId}/settings/policies/resources/`);
// should redirect to the details page
// router.push(
// `/${org.org.orgId}/settings/policies/resources/${niceId}`
// );
router.push(
`/${org.org.orgId}/settings/policies/resource/${niceId}`
);
toast({
title: t("success"),
description: t("policyCreatedSuccess")

View File

@@ -121,23 +121,21 @@ import {
import { useCallback, useMemo, useState, useActionState } from "react";
import { UseFormReturn, useForm, useWatch } from "react-hook-form";
import router from "next/navigation";
import { useResourcePolicyContext } from "@app/providers/ResourcePolicyProvider";
// ─── EditPolicyForm ─────────────────────────────────────────────────────────
export type EditPolicyFormProps = {
policy: ResourcePolicy;
hidePolicyNameForm?: boolean;
};
export function EditPolicyForm({
hidePolicyNameForm,
policy
}: EditPolicyFormProps) {
export function EditPolicyForm({ hidePolicyNameForm }: EditPolicyFormProps) {
const { org } = useOrgContext();
const t = useTranslations();
const { env } = useEnvContext();
const api = createApiClient({ env });
const [, formAction, isSubmitting] = useActionState(onSubmit, null);
// const [, formAction, isSubmitting] = useActionState(onSubmit, null);
const { isPaidUser } = usePaidStatus();
const router = useRouter();
@@ -145,7 +143,7 @@ export function EditPolicyForm({
const isMaxmindAvailable = !!(
env.server.maxmind_db_path && env.server.maxmind_db_path.length > 0
);
const isMaxmindAsnAvailable = !!(
const isMaxmindASNAvailable = !!(
env.server.maxmind_asn_path && env.server.maxmind_asn_path.length > 0
);
@@ -162,75 +160,76 @@ export function EditPolicyForm({
})
);
const form = useForm<PolicyFormValues>({
resolver: zodResolver(createPolicySchema) as any,
defaultValues: {
name: policy.name,
sso: true,
skipToIdpId: null,
emailWhitelistEnabled: false,
roles: [],
users: [],
emails: [],
applyRules: false,
rules: [],
password: null,
headerAuth: null,
pincode: null
}
});
// const form = useForm<PolicyFormValues>({
// resolver: zodResolver(createPolicySchema) as any,
// defaultValues: {
// name: "",
// sso: true,
// skipToIdpId: null,
// emailWhitelistEnabled: false,
// roles: [],
// users: [],
// emails: [],
// applyRules: false,
// rules: [],
// password: null,
// headerAuth: null,
// pincode: null
// }
// });
async function onSubmit() {
const isValid = await form.trigger();
// async function onSubmit() {
// return;
// // const isValid = await form.trigger();
if (!isValid) return;
// // if (!isValid) return;
const payload = form.getValues();
// // const payload = form.getValues();
try {
const res = await api
.post<AxiosResponse<ResourcePolicy>>(
`/org/${org.org.orgId}/resource-policy/`,
{
name: payload.name,
sso: payload.sso,
roleIds: payload.roles.map((r) => r.id),
userIds: payload.users.map((u) => u.id)
}
)
.catch((e) => {
toast({
variant: "destructive",
title: t("policyErrorCreate"),
description: formatAxiosError(
e,
t("policyErrorCreateDescription")
)
});
});
// // try {
// // const res = await api
// // .post<AxiosResponse<ResourcePolicy>>(
// // `/org/${org.org.orgId}/resource-policy/`,
// // {
// // name: payload.name,
// // sso: payload.sso,
// // roleIds: payload.roles.map((r) => r.id),
// // userIds: payload.users.map((u) => u.id)
// // }
// // )
// // .catch((e) => {
// // toast({
// // variant: "destructive",
// // title: t("policyErrorCreate"),
// // description: formatAxiosError(
// // e,
// // t("policyErrorCreateDescription")
// // )
// // });
// // });
if (res && res.status === 201) {
const id = res.data.data.resourcePolicyId;
const niceId = res.data.data.niceId;
// // if (res && res.status === 201) {
// // const id = res.data.data.resourcePolicyId;
// // const niceId = res.data.data.niceId;
router.push(`/${org.org.orgId}/settings/policies/resources/`);
// should redirect to the details page
// router.push(
// `/${org.org.orgId}/settings/policies/resources/${niceId}`
// );
toast({
title: t("success"),
description: t("policyCreatedSuccess")
});
}
} catch (e) {
toast({
variant: "destructive",
title: t("policyErrorCreate"),
description: t("policyErrorCreateMessageDescription")
});
}
}
// // router.push(`/${org.org.orgId}/settings/policies/resources/`);
// // // should redirect to the details page
// // // router.push(
// // // `/${org.org.orgId}/settings/policies/resources/${niceId}`
// // // );
// // toast({
// // title: t("success"),
// // description: t("policyCreatedSuccess")
// // });
// // }
// // } catch (e) {
// // toast({
// // variant: "destructive",
// // title: t("policyErrorCreate"),
// // description: t("policyErrorCreateMessageDescription")
// // });
// // }
// }
const allRoles = useMemo(
() =>
@@ -271,12 +270,12 @@ export function EditPolicyForm({
}
return (
<Form {...form}>
<form action={formAction}>
<SettingsContainer>
{/* Name */}
{!hidePolicyNameForm && <PolicyNameSection form={form} />}
<PolicyUsersRolesSection
// <Form {...form}>
// <form action={formAction}>
<SettingsContainer>
{/* Name */}
{!hidePolicyNameForm && <PolicyNameSection />}
{/* <PolicyUsersRolesSection
form={form}
allRoles={allRoles}
allUsers={allUsers}
@@ -290,65 +289,123 @@ export function EditPolicyForm({
<PolicyRulesSection
form={form}
isMaxmindAvailable={isMaxmindAvailable}
isMaxmindAsnAvailable={isMaxmindAsnAvailable}
/>
</SettingsContainer>
</form>
</Form>
isMaxmindAsnAvailable={isMaxmindASNAvailable}
/> */}
</SettingsContainer>
// </form>
// </Form>
);
}
// ─── PolicyNameSection ──────────────────────────────────────────────────
type PolicyNameSectionProps = {
form: UseFormReturn<PolicyFormValues, any, any>;
isEditing?: boolean;
};
export function PolicyNameSection({ form }: PolicyNameSectionProps) {
export function PolicyNameSection() {
const t = useTranslations();
return (
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t("resourcePolicyName")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t("resourcePolicyNameDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<SettingsSectionForm>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>{t("name")}</FormLabel>
<FormControl>
<Input
{...field}
placeholder={t(
"resourcePolicyNamePlaceholder"
)}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</SettingsSectionForm>
</SettingsSectionBody>
const api = createApiClient(useEnvContext());
<div className="flex py-6 justify-end">
<Button
type="submit"
// loading={isSubmitting}
// disabled={isSubmitting}
>
{t("saveSettings")}
</Button>
</div>
</SettingsSection>
const { policy } = useResourcePolicyContext();
const { org } = useOrgContext();
const form = useForm({
resolver: zodResolver(
z.object({
name: z.string()
})
),
defaultValues: {
name: policy.name
}
});
const [, formAction, isSubmitting] = useActionState(onSubmit, null);
async function onSubmit() {
const isValid = await form.trigger();
if (!isValid) return;
const payload = form.getValues();
try {
const res = await api
.put<AxiosResponse<ResourcePolicy>>(
`/resource-policy/${policy.resourcePolicyId}`,
{
name: payload.name
}
)
.catch((e) => {
toast({
variant: "destructive",
title: t("policyErrorUpdate"),
description: formatAxiosError(
e,
t("policyErrorUpdateDescription")
)
});
});
if (res && res.status === 200) {
toast({
title: t("success"),
description: t("policyUpdatedSuccess")
});
}
} catch (e) {
toast({
variant: "destructive",
title: t("policyErrorUpdate"),
description: t("policyErrorUpdateMessageDescription")
});
}
}
return (
<Form {...form}>
<form action={formAction}>
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t("resourcePolicyName")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t("resourcePolicyNameDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<SettingsSectionForm>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>{t("name")}</FormLabel>
<FormControl>
<Input
{...field}
placeholder={t(
"resourcePolicyNamePlaceholder"
)}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</SettingsSectionForm>
</SettingsSectionBody>
<div className="flex py-6 justify-end">
<Button
type="submit"
loading={isSubmitting}
disabled={isSubmitting}
>
{t("saveSettings")}
</Button>
</div>
</SettingsSection>
</form>
</Form>
);
}

View File

@@ -0,0 +1,63 @@
"use client";
import { createContext, useContext, useState } from "react";
import { useTranslations } from "next-intl";
import type { GetResourcePolicyResponse } from "@server/routers/policy";
interface ResourcePolicyProviderProps {
children: React.ReactNode;
policy: GetResourcePolicyResponse;
}
export function ResourcePolicyProvider({
children,
policy: serverPolicy
}: ResourcePolicyProviderProps) {
const [policy, setPolicy] =
useState<GetResourcePolicyResponse>(serverPolicy);
const t = useTranslations();
const updatePolicy = (
updatedPolicy: Partial<GetResourcePolicyResponse>
) => {
if (!policy) {
throw new Error(t("resourceErrorNoUpdate"));
}
setPolicy((prev) => {
if (!prev) {
return prev;
}
return {
...prev,
...updatedPolicy
};
});
};
return (
<ResourcePolicyContext value={{ ...policy, updatePolicy }}>
{children}
</ResourcePolicyContext>
);
}
export type ResourcePolicyContextType = GetResourcePolicyResponse & {
updatePolicy: (updatedPolicy: Partial<GetResourcePolicyResponse>) => void;
};
export const ResourcePolicyContext = createContext<
ResourcePolicyContextType | undefined
>(undefined);
export function useResourcePolicyContext() {
const context = useContext(ResourcePolicyContext);
if (context === undefined) {
throw new Error(
"useResourcePolicyContext must be used within a ResourcePolicyProvider"
);
}
return context;
}