diff --git a/backend/src/ee/routes/v1/identity-project-additional-privilege-router.ts b/backend/src/ee/routes/v1/identity-project-additional-privilege-router.ts index 622b003dd..86061d33c 100644 --- a/backend/src/ee/routes/v1/identity-project-additional-privilege-router.ts +++ b/backend/src/ee/routes/v1/identity-project-additional-privilege-router.ts @@ -1,8 +1,12 @@ +import { packRules } from "@casl/ability/extra"; +import slugify from "@sindresorhus/slugify"; import ms from "ms"; import { z } from "zod"; import { IdentityProjectAdditionalPrivilegeSchema } from "@app/db/schemas"; import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types"; +import { alphaNumericNanoId } from "@app/lib/nanoid"; +import { zpStr } from "@app/lib/zod"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -13,22 +17,42 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F schema: { body: z.union([ z.object({ - identityId: z.string(), - projectId: z.string(), - slug: z.string().max(60).trim(), - name: z.string().trim(), + identityId: z.string().min(1), + projectId: z.string().min(1), + // disallow empty string + slug: zpStr( + z + .string() + .max(60) + .trim() + .optional() + .default(`privilege-${slugify(alphaNumericNanoId(12))}`) + .refine((v) => slugify(v) === v, { + message: "Slug must be a valid slug" + }) + ), + name: z.string().trim().min(1), description: z.string().trim().optional(), permissions: z.any().array(), + isPackedPermission: z.boolean().optional().default(true), isTemporary: z.literal(false).default(false) }), z.object({ identityId: z.string(), projectId: z.string(), - slug: z.string().max(60).trim(), + slug: z + .string() + .max(60) + .trim() + .default(`privilege-${slugify(alphaNumericNanoId(12))}`) + .refine((v) => slugify(v) === v, { + message: "Slug must be a valid slug" + }), name: z.string().trim(), description: z.string().trim().optional(), permissions: z.any().array(), isTemporary: z.literal(true), + isPackedPermission: z.boolean().optional().default(true), temporaryMode: z.nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode), temporaryRange: z.string().refine((val) => ms(val) > 0, "Temporary range must be a positive number"), temporaryAccessStartTime: z.string().datetime() @@ -47,7 +71,9 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F actor: req.permission.type, actorOrgId: req.permission.orgId, ...req.body, - permissions: JSON.stringify(req.body.permissions) + permissions: JSON.stringify( + req.body.isPackedPermission ? req.body.permissions : packRules(req.body.permissions) + ) }); return { privilege }; } @@ -62,10 +88,18 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F }), body: z .object({ - slug: z.string().max(60).trim(), + // disallow empty string + slug: z + .string() + .max(60) + .trim() + .refine((v) => slugify(v) === v, { + message: "Slug must be a valid slug" + }), name: z.string().trim(), description: z.string().trim().optional(), permissions: z.any().array(), + isPackedPermission: z.boolean().optional().default(true), isTemporary: z.boolean(), temporaryMode: z.nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode), temporaryRange: z.string().refine((val) => ms(val) > 0, "Temporary range must be a positive number"), @@ -85,7 +119,9 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F actor: req.permission.type, actorOrgId: req.permission.orgId, ...req.body, - permissions: req.body.permissions ? JSON.stringify(req.body.permissions) : undefined, + permissions: req.body.permissions + ? JSON.stringify(req.body.isPackedPermission ? req.body.permissions : packRules(req.body.permissions)) + : undefined, privilegeId: req.params.privilegeId }); return { privilege }; diff --git a/backend/src/ee/routes/v1/user-additional-privilege-router.ts b/backend/src/ee/routes/v1/user-additional-privilege-router.ts index fe40ee259..cd161ab5a 100644 --- a/backend/src/ee/routes/v1/user-additional-privilege-router.ts +++ b/backend/src/ee/routes/v1/user-additional-privilege-router.ts @@ -1,8 +1,11 @@ +import slugify from "@sindresorhus/slugify"; import ms from "ms"; import { z } from "zod"; import { ProjectUserAdditionalPrivilegeSchema } from "@app/db/schemas"; import { ProjectUserAdditionalPrivilegeTemporaryMode } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-types"; +import { alphaNumericNanoId } from "@app/lib/nanoid"; +import { zpStr } from "@app/lib/zod"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -14,7 +17,18 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr body: z.union([ z.object({ projectMembershipId: z.string(), - slug: z.string().max(60).trim(), + // to disallow empty string + slug: zpStr( + z + .string() + .max(60) + .trim() + .optional() + .default(`privilege-${slugify(alphaNumericNanoId(12))}`) + .refine((v) => slugify(v) === v, { + message: "Slug must be a valid slug" + }) + ), name: z.string().trim(), description: z.string().trim().optional(), permissions: z.any().array(), @@ -22,7 +36,17 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr }), z.object({ projectMembershipId: z.string(), - slug: z.string().max(60).trim(), + slug: zpStr( + z + .string() + .max(60) + .trim() + .optional() + .default(`privilege-${slugify(alphaNumericNanoId(12))}`) + .refine((v) => slugify(v) === v, { + message: "Slug must be a valid slug" + }) + ), name: z.string().trim(), description: z.string().trim().optional(), permissions: z.any().array(), @@ -60,7 +84,13 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr }), body: z .object({ - slug: z.string().max(60).trim(), + slug: z + .string() + .max(60) + .trim() + .refine((v) => slugify(v) === v, { + message: "Slug must be a valid slug" + }), name: z.string().trim(), description: z.string().trim().optional(), permissions: z.any().array(), diff --git a/frontend/src/views/Project/MembersPage/components/AdditionalPrivilegeSection/AdditionalPrivilegeForm.tsx b/frontend/src/views/Project/MembersPage/components/AdditionalPrivilegeSection/AdditionalPrivilegeForm.tsx index 06d4f4b71..a6f1d2a48 100644 --- a/frontend/src/views/Project/MembersPage/components/AdditionalPrivilegeSection/AdditionalPrivilegeForm.tsx +++ b/frontend/src/views/Project/MembersPage/components/AdditionalPrivilegeSection/AdditionalPrivilegeForm.tsx @@ -19,7 +19,8 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; -import { Button, FormControl, Input } from "@app/components/v2"; +import guidGenerator from "@app/components/utilities/randomId"; +import { Button, FormControl, Input, Spinner } from "@app/components/v2"; import { ProjectPermissionSub } from "@app/context"; import { useCreateIdentityProjectAdditionalPrivilege, @@ -134,17 +135,16 @@ export const AdditionalPrivilegeForm = ({ isIdentity }: Props) => { const { createNotification } = useNotificationContext(); - const isNewRole = !privilegeId; + const isEdit = Boolean(privilegeId); - const { data: projectUserPrivilegeDetails } = useGetProjectUserPrivilegeDetails( - privilegeId && !isIdentity ? privilegeId : "" - ); + const { data: projectUserPrivilegeDetails, isLoading: isProjectUserPrivilegeLoading } = + useGetProjectUserPrivilegeDetails(privilegeId && !isIdentity ? privilegeId : ""); - const { data: identityProjectPrivilegeDetails } = useGetIdentityProjectPrivilegeDetails( - isIdentity && privilegeId ? privilegeId : "" - ); + const { data: identityProjectPrivilegeDetails, isLoading: isIdentityProjectPrivilegeLoading } = + useGetIdentityProjectPrivilegeDetails(isIdentity && privilegeId ? privilegeId : ""); const privileges = isIdentity ? identityProjectPrivilegeDetails : projectUserPrivilegeDetails; + const isLoading = isIdentity ? isIdentityProjectPrivilegeLoading : isProjectUserPrivilegeLoading; const { handleSubmit, @@ -155,6 +155,9 @@ export const AdditionalPrivilegeForm = ({ control } = useForm({ resolver: zodResolver(formSchema), + defaultValues: { + slug: `privilege-${guidGenerator().slice(0, 4).toLowerCase()}` + }, values: privileges && { ...privileges, description: privileges.description || "", @@ -194,7 +197,7 @@ export const AdditionalPrivilegeForm = ({ }; const handleFormSubmit = async (el: TFormSchema) => { - if (!isNewRole) { + if (isEdit) { await handleRoleUpdate(el); return; } @@ -223,12 +226,20 @@ export const AdditionalPrivilegeForm = ({ } }; + if (isEdit && isLoading) { + return ( +
+ +
+ ); + } + return (

- {isNewRole ? "New" : "Edit"} user additional privilege + {!isEdit ? "New" : "Edit"} user additional privilege