From 9db8f2a87d09c37aa069ed4a3fbcc87ffc9960d8 Mon Sep 17 00:00:00 2001 From: = Date: Tue, 21 Oct 2025 00:10:57 +0530 Subject: [PATCH] feat: updated settings page --- backend/src/ee/routes/v1/sub-org-router.ts | 55 +++++++++- .../ee/services/audit-log/audit-log-types.ts | 12 ++- .../ee/services/sub-org/sub-org-service.ts | 52 ++++++++- .../src/ee/services/sub-org/sub-org-types.ts | 6 ++ backend/src/lib/api-docs/constants.ts | 4 + .../server/plugins/auth/inject-identity.ts | 4 +- backend/src/services/smtp/emails/index.ts | 2 +- backend/src/services/smtp/smtp-service.ts | 4 +- .../OrganizationContext.tsx | 2 +- .../src/hooks/api/subOrganizations/index.tsx | 5 +- .../hooks/api/subOrganizations/mutations.tsx | 18 +++- .../src/hooks/api/subOrganizations/types.ts | 5 + .../ProjectsPage/components/MyProjectView.tsx | 2 +- .../OrgGeneralTab/OrgGeneralTab.tsx | 7 +- .../SubOrgNameChangeSection.tsx | 101 ++++++++++++++++++ .../components/OrgNameChangeSection/index.tsx | 1 + 16 files changed, 260 insertions(+), 20 deletions(-) create mode 100644 frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/SubOrgNameChangeSection.tsx diff --git a/backend/src/ee/routes/v1/sub-org-router.ts b/backend/src/ee/routes/v1/sub-org-router.ts index 185425cea..c89fa40a3 100644 --- a/backend/src/ee/routes/v1/sub-org-router.ts +++ b/backend/src/ee/routes/v1/sub-org-router.ts @@ -4,7 +4,7 @@ import { OrganizationsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { ApiDocsTags, SUB_ORGANIZATIONS } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; -import { GenericResourceNameSchema } from "@app/server/lib/schemas"; +import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -33,7 +33,7 @@ export const registerSubOrgRouter = async (server: FastifyZodProvider) => { } ], body: z.object({ - name: GenericResourceNameSchema.describe(SUB_ORGANIZATIONS.CREATE.name) + name: slugSchema().describe(SUB_ORGANIZATIONS.CREATE.name) }), response: { 200: z.object({ @@ -108,4 +108,55 @@ export const registerSubOrgRouter = async (server: FastifyZodProvider) => { return { organizations }; } }); + + server.route({ + method: "PATCH", + url: "/:subOrgId", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.SubOrganizations], + description: "Update a sub organization", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + subOrgId: z.string().trim().describe(SUB_ORGANIZATIONS.UPDATE.subOrgId) + }), + body: z.object({ + name: slugSchema().describe(SUB_ORGANIZATIONS.UPDATE.name) + }), + response: { + 200: z.object({ + organization: sanitizedSubOrganizationSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { organization } = await server.services.subOrganization.updateSubOrg({ + subOrgId: req.params.subOrgId, + name: req.body.name, + permissionActor: req.permission + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.UPDATE_SUB_ORGANIZATION, + metadata: { + name: req.body.name, + organizationId: organization.id + } + } + }); + + return { organization }; + } + }); }; diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index a933485ae..620e7b8fb 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -173,7 +173,8 @@ export enum EventType { UPDATE_TOKEN_IDENTITY_TOKEN_AUTH = "update-token-identity-token-auth", GET_TOKENS_IDENTITY_TOKEN_AUTH = "get-tokens-identity-token-auth", - CREATE_SUB_ORGANIZATION = "create-child-organization", + CREATE_SUB_ORGANIZATION = "create-sub-organization", + UPDATE_SUB_ORGANIZATION = "update-sub-organization", ADD_IDENTITY_TOKEN_AUTH = "add-identity-token-auth", UPDATE_IDENTITY_TOKEN_AUTH = "update-identity-token-auth", @@ -617,6 +618,14 @@ interface CreateSubOrganizationEvent { }; } +interface UpdateSubOrganizationEvent { + type: EventType.UPDATE_SUB_ORGANIZATION; + metadata: { + name: string; + organizationId: string; + }; +} + type TSecretMetadata = { key: string; value: string }[]; interface GetSecretEvent { @@ -3874,6 +3883,7 @@ interface PamResourceDeleteEvent { export type Event = | CreateSubOrganizationEvent + | UpdateSubOrganizationEvent | GetSecretsEvent | GetSecretEvent | CreateSecretEvent diff --git a/backend/src/ee/services/sub-org/sub-org-service.ts b/backend/src/ee/services/sub-org/sub-org-service.ts index fca60bb9b..d49a2036f 100644 --- a/backend/src/ee/services/sub-org/sub-org-service.ts +++ b/backend/src/ee/services/sub-org/sub-org-service.ts @@ -8,12 +8,19 @@ import { TMembershipRoleDALFactory } from "@app/services/membership/membership-r import { TOrgDALFactory } from "@app/services/org/org-dal"; import { TLicenseServiceFactory } from "../license/license-service"; -import { OrgPermissionChildOrgActions, OrgPermissionSubjects } from "../permission/org-permission"; +import { + OrgPermissionActions, + OrgPermissionChildOrgActions, + OrgPermissionSubjects +} from "../permission/org-permission"; import { TPermissionServiceFactory } from "../permission/permission-service-types"; -import { TCreateSubOrgDTO, TListSubOrgDTO } from "./sub-org-types"; +import { TCreateSubOrgDTO, TListSubOrgDTO, TUpdateSubOrgDTO } from "./sub-org-types"; type TSubOrgServiceFactoryDep = { - orgDAL: Pick; + orgDAL: Pick< + TOrgDALFactory, + "findOne" | "create" | "transaction" | "listSubOrganizations" | "updateById" | "findById" + >; permissionService: Pick; licenseService: Pick; membershipDAL: Pick; @@ -113,8 +120,45 @@ export const subOrgServiceFactory = ({ }; }; + const updateSubOrg = async ({ subOrgId, name, permissionActor }: TUpdateSubOrgDTO) => { + const subOrg = await orgDAL.findOne({ + rootOrgId: permissionActor.rootOrgId, + id: subOrgId + }); + if (!subOrg) { + throw new BadRequestError({ message: "Sub-organization not found" }); + } + + const { permission } = await permissionService.getOrgPermission({ + actorId: permissionActor.id, + actor: permissionActor.type, + orgId: subOrgId, + actorOrgId: subOrgId, + actorAuthMethod: permissionActor.authMethod, + scope: OrganizationActionScope.ChildOrganization + }); + + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); + + const existingSubOrg = await orgDAL.findOne({ + parentOrgId: subOrg.parentOrgId, + slug: name + }); + + if (existingSubOrg && existingSubOrg.id !== subOrgId) { + throw new BadRequestError({ message: `Sub-organization with name ${name} already exists` }); + } + + const organization = await orgDAL.updateById(subOrgId, { name, slug: name }); + + return { + organization + }; + }; + return { createSubOrg, - listSubOrgs + listSubOrgs, + updateSubOrg }; }; diff --git a/backend/src/ee/services/sub-org/sub-org-types.ts b/backend/src/ee/services/sub-org/sub-org-types.ts index fc2a47b59..a1af9878e 100644 --- a/backend/src/ee/services/sub-org/sub-org-types.ts +++ b/backend/src/ee/services/sub-org/sub-org-types.ts @@ -14,3 +14,9 @@ export type TListSubOrgDTO = { isAccessible?: boolean; }>; }; + +export type TUpdateSubOrgDTO = { + subOrgId: string; + name: string; + permissionActor: OrgServiceActor; +}; diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index e42eb9eff..19cf463f5 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -721,6 +721,10 @@ export const SUB_ORGANIZATIONS = { CREATE: { name: "The name of the sub organization to create." }, + UPDATE: { + name: "The name of the sub organization to update.", + subOrgId: "The id of the sub organization to update." + }, LIST: { limit: "The number of sub organizations to return.", offset: "The offset to start from. If you enter 10, it will start from the 10th sub organization.", diff --git a/backend/src/server/plugins/auth/inject-identity.ts b/backend/src/server/plugins/auth/inject-identity.ts index 2339d78be..b33f2fbe6 100644 --- a/backend/src/server/plugins/auth/inject-identity.ts +++ b/backend/src/server/plugins/auth/inject-identity.ts @@ -8,7 +8,7 @@ import { TScimTokenJwtPayload } from "@app/ee/services/scim/scim-types"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto"; import { BadRequestError } from "@app/lib/errors"; -import { GenericResourceNameSchema } from "@app/server/lib/schemas"; +import { slugSchema } from "@app/server/lib/schemas"; import { ActorType, AuthMethod, AuthMode, AuthModeJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type"; import { TIdentityAccessTokenJwtPayload } from "@app/services/identity-access-token/identity-access-token-types"; import { getServerCfg } from "@app/services/super-admin/super-admin-service"; @@ -149,7 +149,7 @@ export const injectIdentity = fp( const subOrganizationSelector = req.headers?.["x-infisical-org"] as string | undefined; if (subOrganizationSelector) { - await GenericResourceNameSchema.parseAsync(subOrganizationSelector); + await slugSchema().parseAsync(subOrganizationSelector); } switch (authMode) { diff --git a/backend/src/services/smtp/emails/index.ts b/backend/src/services/smtp/emails/index.ts index 78e415832..692cacbaf 100644 --- a/backend/src/services/smtp/emails/index.ts +++ b/backend/src/services/smtp/emails/index.ts @@ -31,5 +31,5 @@ export * from "./SecretScanningSecretsDetectedTemplate"; export * from "./SecretSyncFailedTemplate"; export * from "./ServiceTokenExpiryNoticeTemplate"; export * from "./SignupEmailVerificationTemplate"; -export * from "./UnlockAccountTemplate"; export * from "./SubOrganizationInvitationTemplate"; +export * from "./UnlockAccountTemplate"; diff --git a/backend/src/services/smtp/smtp-service.ts b/backend/src/services/smtp/smtp-service.ts index e5f83f66c..cef22009a 100644 --- a/backend/src/services/smtp/smtp-service.ts +++ b/backend/src/services/smtp/smtp-service.ts @@ -40,8 +40,8 @@ import { SecretSyncFailedTemplate, ServiceTokenExpiryNoticeTemplate, SignupEmailVerificationTemplate, - UnlockAccountTemplate, - SubOrganizationInvitationTemplate + SubOrganizationInvitationTemplate, + UnlockAccountTemplate } from "./emails"; export type TSmtpConfig = SMTPTransport.Options; diff --git a/frontend/src/context/OrganizationContext/OrganizationContext.tsx b/frontend/src/context/OrganizationContext/OrganizationContext.tsx index b657a827d..07216b6d7 100644 --- a/frontend/src/context/OrganizationContext/OrganizationContext.tsx +++ b/frontend/src/context/OrganizationContext/OrganizationContext.tsx @@ -1,8 +1,8 @@ +import { useMemo } from "react"; import { useSuspenseQuery } from "@tanstack/react-query"; import { useRouteContext, useSearch } from "@tanstack/react-router"; import { fetchOrganizationById, organizationKeys } from "@app/hooks/api/organization/queries"; -import { useMemo } from "react"; export const useOrganization = () => { const organizationId = useRouteContext({ diff --git a/frontend/src/hooks/api/subOrganizations/index.tsx b/frontend/src/hooks/api/subOrganizations/index.tsx index 85095fcc6..480377464 100644 --- a/frontend/src/hooks/api/subOrganizations/index.tsx +++ b/frontend/src/hooks/api/subOrganizations/index.tsx @@ -1,7 +1,8 @@ -export { useCreateSubOrganization } from "./mutations"; +export { useCreateSubOrganization, useUpdateSubOrganization } from "./mutations"; export { subOrganizationsQuery } from "./queries"; export type { TCreateSubOrganizationDTO, TListSubOrganizationsDTO, - TSubOrganization + TSubOrganization, + TUpdateSubOrganizationDTO } from "./types"; diff --git a/frontend/src/hooks/api/subOrganizations/mutations.tsx b/frontend/src/hooks/api/subOrganizations/mutations.tsx index 828aea6f4..f2b9ac7a8 100644 --- a/frontend/src/hooks/api/subOrganizations/mutations.tsx +++ b/frontend/src/hooks/api/subOrganizations/mutations.tsx @@ -3,7 +3,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; import { subOrganizationsQuery } from "./queries"; -import { TCreateSubOrganizationDTO, TSubOrganization } from "./types"; +import { TCreateSubOrganizationDTO, TSubOrganization, TUpdateSubOrganizationDTO } from "./types"; export const useCreateSubOrganization = () => { const queryClient = useQueryClient(); @@ -20,3 +20,19 @@ export const useCreateSubOrganization = () => { } }); }; + +export const useUpdateSubOrganization = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ subOrgId, name }: TUpdateSubOrganizationDTO) => { + const { data } = await apiRequest.patch<{ organization: TSubOrganization }>( + `/api/v1/sub-organizations/${subOrgId}`, + { name } + ); + return data; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: subOrganizationsQuery.allKey() }); + } + }); +}; diff --git a/frontend/src/hooks/api/subOrganizations/types.ts b/frontend/src/hooks/api/subOrganizations/types.ts index e6fa39e1e..6086830fb 100644 --- a/frontend/src/hooks/api/subOrganizations/types.ts +++ b/frontend/src/hooks/api/subOrganizations/types.ts @@ -15,3 +15,8 @@ export type TListSubOrganizationsDTO = { offset?: number; isAccessible?: boolean; }; + +export type TUpdateSubOrganizationDTO = { + subOrgId: string; + name: string; +}; diff --git a/frontend/src/pages/organization/ProjectsPage/components/MyProjectView.tsx b/frontend/src/pages/organization/ProjectsPage/components/MyProjectView.tsx index 020947f28..a86023b6c 100644 --- a/frontend/src/pages/organization/ProjectsPage/components/MyProjectView.tsx +++ b/frontend/src/pages/organization/ProjectsPage/components/MyProjectView.tsx @@ -378,7 +378,7 @@ export const MyProjectView = ({
{ const { hasOrgRole } = useOrgPermission(); + const { isSubOrganization } = useOrganization(); return (
- + {isSubOrganization ? : } {hasOrgRole(OrgMembershipRole.Admin) && }
diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/SubOrgNameChangeSection.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/SubOrgNameChangeSection.tsx new file mode 100644 index 000000000..38eb5f012 --- /dev/null +++ b/frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/SubOrgNameChangeSection.tsx @@ -0,0 +1,101 @@ +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useQueryClient } from "@tanstack/react-query"; +import { useNavigate, useRouter } from "@tanstack/react-router"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { OrgPermissionCan } from "@app/components/permissions"; +import { Button, FormControl, Input } from "@app/components/v2"; +import { + OrgPermissionActions, + OrgPermissionSubjects, + useOrganization, + useOrgPermission +} from "@app/context"; +import { useUpdateSubOrganization } from "@app/hooks/api"; + +const formSchema = z.object({ + name: z + .string() + .regex(/^[a-zA-Z0-9-]+$/, "Name must only contain alphanumeric characters or hyphens") +}); + +type FormData = z.infer; + +export const SubOrgNameChangeSection = (): JSX.Element => { + const { currentOrg } = useOrganization(); + const { permission } = useOrgPermission(); + const navigate = useNavigate(); + const router = useRouter(); + const queryClient = useQueryClient(); + + const { handleSubmit, control } = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + name: currentOrg?.subOrganization?.name || "" + } + }); + const { mutateAsync, isPending } = useUpdateSubOrganization(); + + const onFormSubmit = async ({ name }: FormData) => { + try { + await mutateAsync({ + name, + subOrgId: currentOrg.id + }); + + navigate({ to: "/organization/settings", search: { subOrganization: name } }); + queryClient.clear(); + await router.invalidate({ sync: true }); + createNotification({ + text: "Successfully updated sub-organization details", + type: "success" + }); + } catch (error) { + console.error(error); + createNotification({ + text: "Failed to update sub-organization details", + type: "error" + }); + } + }; + + return ( +
+
+

Organization Name

+ ( + + + + )} + control={control} + name="name" + /> +
+ + {(isAllowed) => ( + + )} + +
+ ); +}; diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/index.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/index.tsx index 4d86fcddb..70fe29455 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/index.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgNameChangeSection/index.tsx @@ -1 +1,2 @@ export { OrgNameChangeSection } from "./OrgNameChangeSection"; +export { SubOrgNameChangeSection } from "./SubOrgNameChangeSection";