🚧 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", "policyErrorCreate": "Error creating policy",
"policyErrorCreateDescription": "An error occurred when creating the policy", "policyErrorCreateDescription": "An error occurred when creating the policy",
"policyErrorCreateMessageDescription": "An unexpected error occurred", "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", "policyCreatedSuccess": "Resource policy succesfully created",
"policyUpdatedSuccess": "Resource policy succesfully updated",
"resourceErrorCreate": "Error creating resource", "resourceErrorCreate": "Error creating resource",
"resourceErrorCreateDescription": "An error occurred when creating the resource", "resourceErrorCreateDescription": "An error occurred when creating the resource",
"resourceErrorCreateMessage": "Error creating resource:", "resourceErrorCreateMessage": "Error creating resource:",

View File

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

View File

@@ -1 +1,2 @@
export * from "./getResourcePolicy"; 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 HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { import { db, orgs, resourcePolicies, type ResourcePolicy } from "@server/db";
db,
orgs,
resourcePolicies,
rolePolicies,
userPolicies,
type ResourcePolicy
} from "@server/db";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import logger from "@server/logger"; import logger from "@server/logger";
import response from "@server/lib/response"; 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 { Button } from "@app/components/ui/button";
import { internal } from "@app/lib/api"; import { internal } from "@app/lib/api";
import { authCookieHeader } from "@app/lib/api/cookies"; 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 { GetResourcePolicyResponse } from "@server/routers/policy";
import type { AxiosResponse } from "axios"; import type { AxiosResponse } from "axios";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
@@ -18,7 +18,7 @@ export default async function EditPolicyPage(props: EditPolicyPageProps) {
const params = await props.params; const params = await props.params;
const t = await getTranslations(); const t = await getTranslations();
let policy: ResourcePolicy | null = null; let policyResponse: GetResourcePolicyResponse | null = null;
try { try {
const res = await internal.get< const res = await internal.get<
AxiosResponse<GetResourcePolicyResponse> AxiosResponse<GetResourcePolicyResponse>
@@ -26,12 +26,12 @@ export default async function EditPolicyPage(props: EditPolicyPageProps) {
`/org/${params.orgId}/resource-policy/${params.niceId}`, `/org/${params.orgId}/resource-policy/${params.niceId}`,
await authCookieHeader() await authCookieHeader()
); );
policy = res.data.data.policy; policyResponse = res.data.data;
} catch { } catch {
redirect(`/${params.orgId}/settings/policies/resource`); redirect(`/${params.orgId}/settings/policies/resource`);
} }
if (!policy) { if (!policyResponse) {
redirect(`/${params.orgId}/settings/policies/resource`); redirect(`/${params.orgId}/settings/policies/resource`);
} }
@@ -40,7 +40,7 @@ export default async function EditPolicyPage(props: EditPolicyPageProps) {
<div className="flex justify-between"> <div className="flex justify-between">
<SettingsSectionTitle <SettingsSectionTitle
title={t("resourcePolicySetting", { title={t("resourcePolicySetting", {
policyName: policy.name policyName: policyResponse.policy.name
})} })}
description={t("resourcePolicySettingDescription")} description={t("resourcePolicySettingDescription")}
/> />
@@ -52,7 +52,9 @@ export default async function EditPolicyPage(props: EditPolicyPageProps) {
</Button> </Button>
</div> </div>
<EditPolicyForm policy={policy} /> <ResourcePolicyProvider policy={policyResponse}>
<EditPolicyForm />
</ResourcePolicyProvider>
</> </>
); );
} }

View File

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

View File

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